2025-12-18 13:50:39 +08:00
|
|
|
|
package repository
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
2026-03-03 15:01:10 +08:00
|
|
|
|
"database/sql"
|
2025-12-29 19:23:49 +08:00
|
|
|
|
"time"
|
2025-12-25 20:52:47 +08:00
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
dbent "github.com/Wei-Shaw/sub2api/ent"
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/ent/apikey"
|
2026-01-10 22:23:51 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/ent/group"
|
2025-12-29 19:23:49 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
2026-01-10 22:23:51 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/ent/user"
|
2025-12-25 20:52:47 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
|
|
|
2025-12-24 21:07:21 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
type apiKeyRepository struct {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
client *dbent.Client
|
2026-03-03 15:01:10 +08:00
|
|
|
|
sql sqlExecutor
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 15:01:10 +08:00
|
|
|
|
func NewAPIKeyRepository(client *dbent.Client, sqlDB *sql.DB) service.APIKeyRepository {
|
2026-03-03 16:00:51 +08:00
|
|
|
|
return newAPIKeyRepositoryWithSQL(client, sqlDB)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newAPIKeyRepositoryWithSQL(client *dbent.Client, sqlq sqlExecutor) *apiKeyRepository {
|
|
|
|
|
|
return &apiKeyRepository{client: client, sql: sqlq}
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (r *apiKeyRepository) activeQuery() *dbent.APIKeyQuery {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
// 默认过滤已软删除记录,避免删除后仍被查询到。
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return r.client.APIKey.Query().Where(apikey.DeletedAtIsNil())
|
2025-12-29 19:23:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (r *apiKeyRepository) Create(ctx context.Context, key *service.APIKey) error {
|
2026-01-09 21:59:32 +08:00
|
|
|
|
builder := r.client.APIKey.Create().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetUserID(key.UserID).
|
|
|
|
|
|
SetKey(key.Key).
|
|
|
|
|
|
SetName(key.Name).
|
|
|
|
|
|
SetStatus(key.Status).
|
2026-02-03 19:01:49 +08:00
|
|
|
|
SetNillableGroupID(key.GroupID).
|
2026-02-22 22:07:17 +08:00
|
|
|
|
SetNillableLastUsedAt(key.LastUsedAt).
|
2026-02-03 19:01:49 +08:00
|
|
|
|
SetQuota(key.Quota).
|
|
|
|
|
|
SetQuotaUsed(key.QuotaUsed).
|
2026-03-03 15:01:10 +08:00
|
|
|
|
SetNillableExpiresAt(key.ExpiresAt).
|
|
|
|
|
|
SetRateLimit5h(key.RateLimit5h).
|
|
|
|
|
|
SetRateLimit1d(key.RateLimit1d).
|
|
|
|
|
|
SetRateLimit7d(key.RateLimit7d)
|
2026-01-09 21:59:32 +08:00
|
|
|
|
|
|
|
|
|
|
if len(key.IPWhitelist) > 0 {
|
|
|
|
|
|
builder.SetIPWhitelist(key.IPWhitelist)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(key.IPBlacklist) > 0 {
|
|
|
|
|
|
builder.SetIPBlacklist(key.IPBlacklist)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
created, err := builder.Save(ctx)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if err == nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
key.ID = created.ID
|
2026-02-22 22:07:17 +08:00
|
|
|
|
key.LastUsedAt = created.LastUsedAt
|
2025-12-29 10:03:27 +08:00
|
|
|
|
key.CreatedAt = created.CreatedAt
|
|
|
|
|
|
key.UpdatedAt = created.UpdatedAt
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return translatePersistenceError(err, nil, service.ErrAPIKeyExists)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (r *apiKeyRepository) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
m, err := r.activeQuery().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(apikey.IDEQ(id)).
|
|
|
|
|
|
WithUser().
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Only(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if dbent.IsNotFound(err) {
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return nil, service.ErrAPIKeyNotFound
|
2025-12-29 10:03:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return apiKeyEntityToService(m), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 22:23:51 +08:00
|
|
|
|
// GetKeyAndOwnerID 根据 API Key ID 获取其 key 与所有者(用户)ID。
|
2025-12-29 14:06:38 +08:00
|
|
|
|
// 相比 GetByID,此方法性能更优,因为:
|
2026-01-10 22:23:51 +08:00
|
|
|
|
// - 使用 Select() 只查询必要字段,减少数据传输量
|
2026-01-04 19:27:53 +08:00
|
|
|
|
// - 不加载完整的 API Key 实体及其关联数据(User、Group 等)
|
2026-01-10 22:23:51 +08:00
|
|
|
|
// - 适用于删除等只需 key 与用户 ID 的场景
|
|
|
|
|
|
func (r *apiKeyRepository) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
m, err := r.activeQuery().
|
2025-12-29 14:06:38 +08:00
|
|
|
|
Where(apikey.IDEQ(id)).
|
2026-01-10 22:23:51 +08:00
|
|
|
|
Select(apikey.FieldKey, apikey.FieldUserID).
|
2025-12-29 14:06:38 +08:00
|
|
|
|
Only(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if dbent.IsNotFound(err) {
|
2026-01-10 22:23:51 +08:00
|
|
|
|
return "", 0, service.ErrAPIKeyNotFound
|
2025-12-29 14:06:38 +08:00
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
return "", 0, err
|
2025-12-29 14:06:38 +08:00
|
|
|
|
}
|
2026-01-10 22:23:51 +08:00
|
|
|
|
return m.Key, m.UserID, nil
|
2025-12-29 14:06:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (r *apiKeyRepository) GetByKey(ctx context.Context, key string) (*service.APIKey, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
m, err := r.activeQuery().
|
2025-12-29 10:03:27 +08:00
|
|
|
|
Where(apikey.KeyEQ(key)).
|
|
|
|
|
|
WithUser().
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Only(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if dbent.IsNotFound(err) {
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return nil, service.ErrAPIKeyNotFound
|
2025-12-29 10:03:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return apiKeyEntityToService(m), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 22:23:51 +08:00
|
|
|
|
func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*service.APIKey, error) {
|
|
|
|
|
|
m, err := r.activeQuery().
|
|
|
|
|
|
Where(apikey.KeyEQ(key)).
|
|
|
|
|
|
Select(
|
|
|
|
|
|
apikey.FieldID,
|
|
|
|
|
|
apikey.FieldUserID,
|
|
|
|
|
|
apikey.FieldGroupID,
|
|
|
|
|
|
apikey.FieldStatus,
|
|
|
|
|
|
apikey.FieldIPWhitelist,
|
|
|
|
|
|
apikey.FieldIPBlacklist,
|
2026-02-03 19:01:49 +08:00
|
|
|
|
apikey.FieldQuota,
|
|
|
|
|
|
apikey.FieldQuotaUsed,
|
|
|
|
|
|
apikey.FieldExpiresAt,
|
2026-03-03 15:01:10 +08:00
|
|
|
|
apikey.FieldRateLimit5h,
|
|
|
|
|
|
apikey.FieldRateLimit1d,
|
|
|
|
|
|
apikey.FieldRateLimit7d,
|
2026-01-10 22:23:51 +08:00
|
|
|
|
).
|
|
|
|
|
|
WithUser(func(q *dbent.UserQuery) {
|
|
|
|
|
|
q.Select(
|
|
|
|
|
|
user.FieldID,
|
|
|
|
|
|
user.FieldStatus,
|
|
|
|
|
|
user.FieldRole,
|
|
|
|
|
|
user.FieldBalance,
|
|
|
|
|
|
user.FieldConcurrency,
|
|
|
|
|
|
)
|
|
|
|
|
|
}).
|
|
|
|
|
|
WithGroup(func(q *dbent.GroupQuery) {
|
|
|
|
|
|
q.Select(
|
|
|
|
|
|
group.FieldID,
|
|
|
|
|
|
group.FieldName,
|
|
|
|
|
|
group.FieldPlatform,
|
|
|
|
|
|
group.FieldStatus,
|
|
|
|
|
|
group.FieldSubscriptionType,
|
|
|
|
|
|
group.FieldRateMultiplier,
|
|
|
|
|
|
group.FieldDailyLimitUsd,
|
|
|
|
|
|
group.FieldWeeklyLimitUsd,
|
|
|
|
|
|
group.FieldMonthlyLimitUsd,
|
|
|
|
|
|
group.FieldImagePrice1k,
|
|
|
|
|
|
group.FieldImagePrice2k,
|
|
|
|
|
|
group.FieldImagePrice4k,
|
2026-01-31 20:22:22 +08:00
|
|
|
|
group.FieldSoraImagePrice360,
|
|
|
|
|
|
group.FieldSoraImagePrice540,
|
|
|
|
|
|
group.FieldSoraVideoPricePerRequest,
|
|
|
|
|
|
group.FieldSoraVideoPricePerRequestHd,
|
2026-01-10 22:23:51 +08:00
|
|
|
|
group.FieldClaudeCodeOnly,
|
|
|
|
|
|
group.FieldFallbackGroupID,
|
2026-01-23 22:24:46 +08:00
|
|
|
|
group.FieldFallbackGroupIDOnInvalidRequest,
|
2026-01-16 17:26:05 +08:00
|
|
|
|
group.FieldModelRoutingEnabled,
|
|
|
|
|
|
group.FieldModelRouting,
|
2026-01-27 13:09:56 +08:00
|
|
|
|
group.FieldMcpXMLInject,
|
2026-02-27 01:54:54 +08:00
|
|
|
|
group.FieldSimulateClaudeMaxEnabled,
|
2026-02-02 22:20:08 +08:00
|
|
|
|
group.FieldSupportedModelScopes,
|
2026-03-07 23:18:19 +08:00
|
|
|
|
group.FieldAllowMessagesDispatch,
|
|
|
|
|
|
group.FieldDefaultMappedModel,
|
2026-01-10 22:23:51 +08:00
|
|
|
|
)
|
|
|
|
|
|
}).
|
|
|
|
|
|
Only(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if dbent.IsNotFound(err) {
|
|
|
|
|
|
return nil, service.ErrAPIKeyNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return apiKeyEntityToService(m), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (r *apiKeyRepository) Update(ctx context.Context, key *service.APIKey) error {
|
2025-12-29 19:59:36 +08:00
|
|
|
|
// 使用原子操作:将软删除检查与更新合并到同一语句,避免竞态条件。
|
|
|
|
|
|
// 之前的实现先检查 Exist 再 UpdateOneID,若在两步之间发生软删除,
|
|
|
|
|
|
// 则会更新已删除的记录。
|
|
|
|
|
|
// 这里选择 Update().Where(),确保只有未软删除记录能被更新。
|
|
|
|
|
|
// 同时显式设置 updated_at,避免二次查询带来的并发可见性问题。
|
2026-02-28 17:33:30 +08:00
|
|
|
|
client := clientFromContext(ctx, r.client)
|
2025-12-29 19:59:36 +08:00
|
|
|
|
now := time.Now()
|
2026-02-28 17:33:30 +08:00
|
|
|
|
builder := client.APIKey.Update().
|
2025-12-29 19:59:36 +08:00
|
|
|
|
Where(apikey.IDEQ(key.ID), apikey.DeletedAtIsNil()).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
SetName(key.Name).
|
2025-12-29 19:59:36 +08:00
|
|
|
|
SetStatus(key.Status).
|
2026-02-03 19:01:49 +08:00
|
|
|
|
SetQuota(key.Quota).
|
|
|
|
|
|
SetQuotaUsed(key.QuotaUsed).
|
2026-03-03 15:01:10 +08:00
|
|
|
|
SetRateLimit5h(key.RateLimit5h).
|
|
|
|
|
|
SetRateLimit1d(key.RateLimit1d).
|
|
|
|
|
|
SetRateLimit7d(key.RateLimit7d).
|
|
|
|
|
|
SetUsage5h(key.Usage5h).
|
|
|
|
|
|
SetUsage1d(key.Usage1d).
|
|
|
|
|
|
SetUsage7d(key.Usage7d).
|
2025-12-29 19:59:36 +08:00
|
|
|
|
SetUpdatedAt(now)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
if key.GroupID != nil {
|
|
|
|
|
|
builder.SetGroupID(*key.GroupID)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearGroupID()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 19:01:49 +08:00
|
|
|
|
// Expiration time
|
|
|
|
|
|
if key.ExpiresAt != nil {
|
|
|
|
|
|
builder.SetExpiresAt(*key.ExpiresAt)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearExpiresAt()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 15:01:10 +08:00
|
|
|
|
// Rate limit window start times
|
|
|
|
|
|
if key.Window5hStart != nil {
|
|
|
|
|
|
builder.SetWindow5hStart(*key.Window5hStart)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearWindow5hStart()
|
|
|
|
|
|
}
|
|
|
|
|
|
if key.Window1dStart != nil {
|
|
|
|
|
|
builder.SetWindow1dStart(*key.Window1dStart)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearWindow1dStart()
|
|
|
|
|
|
}
|
|
|
|
|
|
if key.Window7dStart != nil {
|
|
|
|
|
|
builder.SetWindow7dStart(*key.Window7dStart)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearWindow7dStart()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 21:59:32 +08:00
|
|
|
|
// IP 限制字段
|
|
|
|
|
|
if len(key.IPWhitelist) > 0 {
|
|
|
|
|
|
builder.SetIPWhitelist(key.IPWhitelist)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearIPWhitelist()
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(key.IPBlacklist) > 0 {
|
|
|
|
|
|
builder.SetIPBlacklist(key.IPBlacklist)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
builder.ClearIPBlacklist()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 19:59:36 +08:00
|
|
|
|
affected, err := builder.Save(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
2025-12-29 10:03:27 +08:00
|
|
|
|
}
|
2025-12-29 19:59:36 +08:00
|
|
|
|
if affected == 0 {
|
|
|
|
|
|
// 更新影响行数为 0,说明记录不存在或已被软删除。
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return service.ErrAPIKeyNotFound
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
2025-12-29 19:59:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用同一时间戳回填,避免并发删除导致二次查询失败。
|
|
|
|
|
|
key.UpdatedAt = now
|
|
|
|
|
|
return nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *apiKeyRepository) Delete(ctx context.Context, id int64) error {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
// 显式软删除:避免依赖 Hook 行为,确保 deleted_at 一定被设置。
|
2026-01-04 19:27:53 +08:00
|
|
|
|
affected, err := r.client.APIKey.Update().
|
2025-12-29 19:23:49 +08:00
|
|
|
|
Where(apikey.IDEQ(id), apikey.DeletedAtIsNil()).
|
|
|
|
|
|
SetDeletedAt(time.Now()).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if dbent.IsNotFound(err) {
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return service.ErrAPIKeyNotFound
|
2025-12-29 19:23:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if affected == 0 {
|
2026-01-04 19:27:53 +08:00
|
|
|
|
exists, err := r.client.APIKey.Query().
|
2025-12-29 19:23:49 +08:00
|
|
|
|
Where(apikey.IDEQ(id)).
|
|
|
|
|
|
Exist(mixins.SkipSoftDelete(ctx))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-01-04 19:27:53 +08:00
|
|
|
|
return service.ErrAPIKeyNotFound
|
2025-12-29 19:23:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
return nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 11:29:31 +08:00
|
|
|
|
func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, filters service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
q := r.activeQuery().Where(apikey.UserIDEQ(userID))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2026-03-04 11:29:31 +08:00
|
|
|
|
// Apply filters
|
|
|
|
|
|
if filters.Search != "" {
|
|
|
|
|
|
q = q.Where(apikey.Or(
|
|
|
|
|
|
apikey.NameContainsFold(filters.Search),
|
|
|
|
|
|
apikey.KeyContainsFold(filters.Search),
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
if filters.Status != "" {
|
|
|
|
|
|
q = q.Where(apikey.StatusEQ(filters.Status))
|
|
|
|
|
|
}
|
|
|
|
|
|
if filters.GroupID != nil {
|
|
|
|
|
|
if *filters.GroupID == 0 {
|
|
|
|
|
|
q = q.Where(apikey.GroupIDIsNil())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
q = q.Where(apikey.GroupIDEQ(*filters.GroupID))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
total, err := q.Count(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
keys, err := q.
|
|
|
|
|
|
WithGroup().
|
|
|
|
|
|
Offset(params.Offset()).
|
|
|
|
|
|
Limit(params.Limit()).
|
|
|
|
|
|
Order(dbent.Desc(apikey.FieldID)).
|
|
|
|
|
|
All(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
outKeys := make([]service.APIKey, 0, len(keys))
|
2025-12-26 15:40:24 +08:00
|
|
|
|
for i := range keys {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return outKeys, paginationResultFromTotal(int64(total), params), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 16:03:57 +08:00
|
|
|
|
func (r *apiKeyRepository) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
|
|
|
|
|
|
if len(apiKeyIDs) == 0 {
|
|
|
|
|
|
return []int64{}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
ids, err := r.client.APIKey.Query().
|
2025-12-29 19:23:49 +08:00
|
|
|
|
Where(apikey.UserIDEQ(userID), apikey.IDIn(apiKeyIDs...), apikey.DeletedAtIsNil()).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
IDs(ctx)
|
2025-12-27 16:03:57 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return ids, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *apiKeyRepository) CountByUserID(ctx context.Context, userID int64) (int64, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
count, err := r.activeQuery().Where(apikey.UserIDEQ(userID)).Count(ctx)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return int64(count), err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *apiKeyRepository) ExistsByKey(ctx context.Context, key string) (bool, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
count, err := r.activeQuery().Where(apikey.KeyEQ(key)).Count(ctx)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return count > 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (r *apiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
q := r.activeQuery().Where(apikey.GroupIDEQ(groupID))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
total, err := q.Count(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
keys, err := q.
|
|
|
|
|
|
WithUser().
|
|
|
|
|
|
Offset(params.Offset()).
|
|
|
|
|
|
Limit(params.Limit()).
|
|
|
|
|
|
Order(dbent.Desc(apikey.FieldID)).
|
|
|
|
|
|
All(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
outKeys := make([]service.APIKey, 0, len(keys))
|
2025-12-26 15:40:24 +08:00
|
|
|
|
for i := range keys {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return outKeys, paginationResultFromTotal(int64(total), params), nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
// SearchAPIKeys searches API keys by user ID and/or keyword (name)
|
|
|
|
|
|
func (r *apiKeyRepository) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
q := r.activeQuery()
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if userID > 0 {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
q = q.Where(apikey.UserIDEQ(userID))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if keyword != "" {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
q = q.Where(apikey.NameContainsFold(keyword))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
keys, err := q.Limit(limit).Order(dbent.Desc(apikey.FieldID)).All(ctx)
|
|
|
|
|
|
if err != nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
outKeys := make([]service.APIKey, 0, len(keys))
|
2025-12-26 15:40:24 +08:00
|
|
|
|
for i := range keys {
|
2025-12-29 10:03:27 +08:00
|
|
|
|
outKeys = append(outKeys, *apiKeyEntityToService(keys[i]))
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
return outKeys, nil
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ClearGroupIDByGroupID 将指定分组的所有 API Key 的 group_id 设为 nil
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *apiKeyRepository) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
2026-01-04 19:27:53 +08:00
|
|
|
|
n, err := r.client.APIKey.Update().
|
2025-12-29 19:23:49 +08:00
|
|
|
|
Where(apikey.GroupIDEQ(groupID), apikey.DeletedAtIsNil()).
|
2025-12-29 10:03:27 +08:00
|
|
|
|
ClearGroupID().
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
return int64(n), err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CountByGroupID 获取分组的 API Key 数量
|
2025-12-25 20:52:47 +08:00
|
|
|
|
func (r *apiKeyRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
2025-12-29 19:23:49 +08:00
|
|
|
|
count, err := r.activeQuery().Where(apikey.GroupIDEQ(groupID)).Count(ctx)
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return int64(count), err
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 22:23:51 +08:00
|
|
|
|
func (r *apiKeyRepository) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) {
|
|
|
|
|
|
keys, err := r.activeQuery().
|
|
|
|
|
|
Where(apikey.UserIDEQ(userID)).
|
|
|
|
|
|
Select(apikey.FieldKey).
|
|
|
|
|
|
Strings(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return keys, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *apiKeyRepository) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) {
|
|
|
|
|
|
keys, err := r.activeQuery().
|
|
|
|
|
|
Where(apikey.GroupIDEQ(groupID)).
|
|
|
|
|
|
Select(apikey.FieldKey).
|
|
|
|
|
|
Strings(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return keys, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 19:46:42 +08:00
|
|
|
|
// IncrementQuotaUsed 使用 Ent 原子递增 quota_used 字段并返回新值
|
2026-02-03 19:01:49 +08:00
|
|
|
|
func (r *apiKeyRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) {
|
2026-02-07 19:46:42 +08:00
|
|
|
|
updated, err := r.client.APIKey.UpdateOneID(id).
|
|
|
|
|
|
Where(apikey.DeletedAtIsNil()).
|
|
|
|
|
|
AddQuotaUsed(amount).
|
|
|
|
|
|
Save(ctx)
|
2026-02-03 19:01:49 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
if dbent.IsNotFound(err) {
|
|
|
|
|
|
return 0, service.ErrAPIKeyNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
2026-02-07 19:46:42 +08:00
|
|
|
|
return updated.QuotaUsed, nil
|
2026-02-03 19:01:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 13:53:19 +08:00
|
|
|
|
// IncrementQuotaUsedAndGetState atomically increments quota_used, conditionally marks the key
|
|
|
|
|
|
// as quota_exhausted, and returns the latest quota state in one round trip.
|
|
|
|
|
|
func (r *apiKeyRepository) IncrementQuotaUsedAndGetState(ctx context.Context, id int64, amount float64) (*service.APIKeyQuotaUsageState, error) {
|
|
|
|
|
|
query := `
|
|
|
|
|
|
UPDATE api_keys
|
|
|
|
|
|
SET
|
|
|
|
|
|
quota_used = quota_used + $1,
|
|
|
|
|
|
status = CASE
|
|
|
|
|
|
WHEN quota > 0 AND quota_used + $1 >= quota THEN $2
|
|
|
|
|
|
ELSE status
|
|
|
|
|
|
END,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = $3 AND deleted_at IS NULL
|
|
|
|
|
|
RETURNING quota_used, quota, key, status
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
state := &service.APIKeyQuotaUsageState{}
|
|
|
|
|
|
if err := scanSingleRow(ctx, r.sql, query, []any{amount, service.StatusAPIKeyQuotaExhausted, id}, &state.QuotaUsed, &state.Quota, &state.Key, &state.Status); err != nil {
|
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
|
return nil, service.ErrAPIKeyNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return state, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 22:07:17 +08:00
|
|
|
|
func (r *apiKeyRepository) UpdateLastUsed(ctx context.Context, id int64, usedAt time.Time) error {
|
|
|
|
|
|
affected, err := r.client.APIKey.Update().
|
|
|
|
|
|
Where(apikey.IDEQ(id), apikey.DeletedAtIsNil()).
|
|
|
|
|
|
SetLastUsedAt(usedAt).
|
|
|
|
|
|
SetUpdatedAt(usedAt).
|
|
|
|
|
|
Save(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if affected == 0 {
|
|
|
|
|
|
return service.ErrAPIKeyNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 15:01:10 +08:00
|
|
|
|
// IncrementRateLimitUsage atomically increments all rate limit usage counters and initializes
|
|
|
|
|
|
// window start times via COALESCE if not already set.
|
|
|
|
|
|
func (r *apiKeyRepository) IncrementRateLimitUsage(ctx context.Context, id int64, cost float64) error {
|
|
|
|
|
|
_, err := r.sql.ExecContext(ctx, `
|
|
|
|
|
|
UPDATE api_keys SET
|
2026-03-07 09:59:40 +08:00
|
|
|
|
usage_5h = CASE WHEN window_5h_start IS NOT NULL AND window_5h_start + INTERVAL '5 hours' <= NOW() THEN $1 ELSE usage_5h + $1 END,
|
|
|
|
|
|
usage_1d = CASE WHEN window_1d_start IS NOT NULL AND window_1d_start + INTERVAL '24 hours' <= NOW() THEN $1 ELSE usage_1d + $1 END,
|
|
|
|
|
|
usage_7d = CASE WHEN window_7d_start IS NOT NULL AND window_7d_start + INTERVAL '7 days' <= NOW() THEN $1 ELSE usage_7d + $1 END,
|
|
|
|
|
|
window_5h_start = CASE WHEN window_5h_start IS NULL OR window_5h_start + INTERVAL '5 hours' <= NOW() THEN NOW() ELSE window_5h_start END,
|
2026-03-09 10:22:24 +08:00
|
|
|
|
window_1d_start = CASE WHEN window_1d_start IS NULL OR window_1d_start + INTERVAL '24 hours' <= NOW() THEN date_trunc('day', NOW()) ELSE window_1d_start END,
|
|
|
|
|
|
window_7d_start = CASE WHEN window_7d_start IS NULL OR window_7d_start + INTERVAL '7 days' <= NOW() THEN date_trunc('day', NOW()) ELSE window_7d_start END,
|
2026-03-03 15:01:10 +08:00
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = $2 AND deleted_at IS NULL`,
|
|
|
|
|
|
cost, id)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ResetRateLimitWindows resets expired rate limit windows atomically.
|
|
|
|
|
|
func (r *apiKeyRepository) ResetRateLimitWindows(ctx context.Context, id int64) error {
|
|
|
|
|
|
_, err := r.sql.ExecContext(ctx, `
|
|
|
|
|
|
UPDATE api_keys SET
|
|
|
|
|
|
usage_5h = CASE WHEN window_5h_start IS NOT NULL AND window_5h_start + INTERVAL '5 hours' <= NOW() THEN 0 ELSE usage_5h END,
|
|
|
|
|
|
window_5h_start = CASE WHEN window_5h_start IS NOT NULL AND window_5h_start + INTERVAL '5 hours' <= NOW() THEN NOW() ELSE window_5h_start END,
|
|
|
|
|
|
usage_1d = CASE WHEN window_1d_start IS NOT NULL AND window_1d_start + INTERVAL '24 hours' <= NOW() THEN 0 ELSE usage_1d END,
|
2026-03-09 10:22:24 +08:00
|
|
|
|
window_1d_start = CASE WHEN window_1d_start IS NOT NULL AND window_1d_start + INTERVAL '24 hours' <= NOW() THEN date_trunc('day', NOW()) ELSE window_1d_start END,
|
2026-03-03 15:01:10 +08:00
|
|
|
|
usage_7d = CASE WHEN window_7d_start IS NOT NULL AND window_7d_start + INTERVAL '7 days' <= NOW() THEN 0 ELSE usage_7d END,
|
2026-03-09 10:22:24 +08:00
|
|
|
|
window_7d_start = CASE WHEN window_7d_start IS NOT NULL AND window_7d_start + INTERVAL '7 days' <= NOW() THEN date_trunc('day', NOW()) ELSE window_7d_start END,
|
2026-03-03 15:01:10 +08:00
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = $1 AND deleted_at IS NULL`,
|
|
|
|
|
|
id)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetRateLimitData returns the current rate limit usage and window start times for an API key.
|
2026-03-03 15:25:44 +08:00
|
|
|
|
func (r *apiKeyRepository) GetRateLimitData(ctx context.Context, id int64) (result *service.APIKeyRateLimitData, err error) {
|
2026-03-03 15:01:10 +08:00
|
|
|
|
rows, err := r.sql.QueryContext(ctx, `
|
|
|
|
|
|
SELECT usage_5h, usage_1d, usage_7d, window_5h_start, window_1d_start, window_7d_start
|
|
|
|
|
|
FROM api_keys
|
|
|
|
|
|
WHERE id = $1 AND deleted_at IS NULL`,
|
|
|
|
|
|
id)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-03-03 15:25:44 +08:00
|
|
|
|
defer func() {
|
|
|
|
|
|
if closeErr := rows.Close(); closeErr != nil && err == nil {
|
|
|
|
|
|
err = closeErr
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
2026-03-03 15:01:10 +08:00
|
|
|
|
if !rows.Next() {
|
|
|
|
|
|
return nil, service.ErrAPIKeyNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
data := &service.APIKeyRateLimitData{}
|
|
|
|
|
|
if err := rows.Scan(&data.Usage5h, &data.Usage1d, &data.Usage7d, &data.Window5hStart, &data.Window1dStart, &data.Window7dStart); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return data, rows.Err()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func apiKeyEntityToService(m *dbent.APIKey) *service.APIKey {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if m == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-01-04 19:27:53 +08:00
|
|
|
|
out := &service.APIKey{
|
2026-03-03 15:01:10 +08:00
|
|
|
|
ID: m.ID,
|
|
|
|
|
|
UserID: m.UserID,
|
|
|
|
|
|
Key: m.Key,
|
|
|
|
|
|
Name: m.Name,
|
|
|
|
|
|
Status: m.Status,
|
|
|
|
|
|
IPWhitelist: m.IPWhitelist,
|
|
|
|
|
|
IPBlacklist: m.IPBlacklist,
|
|
|
|
|
|
LastUsedAt: m.LastUsedAt,
|
|
|
|
|
|
CreatedAt: m.CreatedAt,
|
|
|
|
|
|
UpdatedAt: m.UpdatedAt,
|
|
|
|
|
|
GroupID: m.GroupID,
|
|
|
|
|
|
Quota: m.Quota,
|
|
|
|
|
|
QuotaUsed: m.QuotaUsed,
|
|
|
|
|
|
ExpiresAt: m.ExpiresAt,
|
|
|
|
|
|
RateLimit5h: m.RateLimit5h,
|
|
|
|
|
|
RateLimit1d: m.RateLimit1d,
|
|
|
|
|
|
RateLimit7d: m.RateLimit7d,
|
|
|
|
|
|
Usage5h: m.Usage5h,
|
|
|
|
|
|
Usage1d: m.Usage1d,
|
|
|
|
|
|
Usage7d: m.Usage7d,
|
|
|
|
|
|
Window5hStart: m.Window5hStart,
|
|
|
|
|
|
Window1dStart: m.Window1dStart,
|
|
|
|
|
|
Window7dStart: m.Window7dStart,
|
2025-12-29 10:03:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
if m.Edges.User != nil {
|
|
|
|
|
|
out.User = userEntityToService(m.Edges.User)
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.Edges.Group != nil {
|
|
|
|
|
|
out.Group = groupEntityToService(m.Edges.Group)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func userEntityToService(u *dbent.User) *service.User {
|
|
|
|
|
|
if u == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return &service.User{
|
2026-02-28 15:01:20 +08:00
|
|
|
|
ID: u.ID,
|
|
|
|
|
|
Email: u.Email,
|
|
|
|
|
|
Username: u.Username,
|
|
|
|
|
|
Notes: u.Notes,
|
|
|
|
|
|
PasswordHash: u.PasswordHash,
|
|
|
|
|
|
Role: u.Role,
|
|
|
|
|
|
Balance: u.Balance,
|
|
|
|
|
|
Concurrency: u.Concurrency,
|
|
|
|
|
|
Status: u.Status,
|
|
|
|
|
|
SoraStorageQuotaBytes: u.SoraStorageQuotaBytes,
|
|
|
|
|
|
SoraStorageUsedBytes: u.SoraStorageUsedBytes,
|
|
|
|
|
|
TotpSecretEncrypted: u.TotpSecretEncrypted,
|
|
|
|
|
|
TotpEnabled: u.TotpEnabled,
|
|
|
|
|
|
TotpEnabledAt: u.TotpEnabledAt,
|
|
|
|
|
|
CreatedAt: u.CreatedAt,
|
|
|
|
|
|
UpdatedAt: u.UpdatedAt,
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
func groupEntityToService(g *dbent.Group) *service.Group {
|
|
|
|
|
|
if g == nil {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return &service.Group{
|
2026-01-23 22:24:46 +08:00
|
|
|
|
ID: g.ID,
|
|
|
|
|
|
Name: g.Name,
|
|
|
|
|
|
Description: derefString(g.Description),
|
|
|
|
|
|
Platform: g.Platform,
|
|
|
|
|
|
RateMultiplier: g.RateMultiplier,
|
|
|
|
|
|
IsExclusive: g.IsExclusive,
|
|
|
|
|
|
Status: g.Status,
|
|
|
|
|
|
Hydrated: true,
|
|
|
|
|
|
SubscriptionType: g.SubscriptionType,
|
|
|
|
|
|
DailyLimitUSD: g.DailyLimitUsd,
|
|
|
|
|
|
WeeklyLimitUSD: g.WeeklyLimitUsd,
|
|
|
|
|
|
MonthlyLimitUSD: g.MonthlyLimitUsd,
|
|
|
|
|
|
ImagePrice1K: g.ImagePrice1k,
|
|
|
|
|
|
ImagePrice2K: g.ImagePrice2k,
|
|
|
|
|
|
ImagePrice4K: g.ImagePrice4k,
|
2026-02-04 20:35:09 +08:00
|
|
|
|
SoraImagePrice360: g.SoraImagePrice360,
|
|
|
|
|
|
SoraImagePrice540: g.SoraImagePrice540,
|
|
|
|
|
|
SoraVideoPricePerRequest: g.SoraVideoPricePerRequest,
|
|
|
|
|
|
SoraVideoPricePerRequestHD: g.SoraVideoPricePerRequestHd,
|
2026-02-28 15:01:20 +08:00
|
|
|
|
SoraStorageQuotaBytes: g.SoraStorageQuotaBytes,
|
2026-01-23 22:24:46 +08:00
|
|
|
|
DefaultValidityDays: g.DefaultValidityDays,
|
|
|
|
|
|
ClaudeCodeOnly: g.ClaudeCodeOnly,
|
|
|
|
|
|
FallbackGroupID: g.FallbackGroupID,
|
|
|
|
|
|
FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest,
|
|
|
|
|
|
ModelRouting: g.ModelRouting,
|
|
|
|
|
|
ModelRoutingEnabled: g.ModelRoutingEnabled,
|
2026-01-27 13:09:56 +08:00
|
|
|
|
MCPXMLInject: g.McpXMLInject,
|
2026-02-27 01:54:54 +08:00
|
|
|
|
SimulateClaudeMaxEnabled: g.SimulateClaudeMaxEnabled,
|
2026-02-02 22:20:08 +08:00
|
|
|
|
SupportedModelScopes: g.SupportedModelScopes,
|
2026-02-08 16:53:45 +08:00
|
|
|
|
SortOrder: g.SortOrder,
|
2026-03-07 17:02:19 +08:00
|
|
|
|
AllowMessagesDispatch: g.AllowMessagesDispatch,
|
|
|
|
|
|
DefaultMappedModel: g.DefaultMappedModel,
|
2026-01-23 22:24:46 +08:00
|
|
|
|
CreatedAt: g.CreatedAt,
|
|
|
|
|
|
UpdatedAt: g.UpdatedAt,
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-29 10:03:27 +08:00
|
|
|
|
func derefString(s *string) string {
|
|
|
|
|
|
if s == nil {
|
|
|
|
|
|
return ""
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|
2025-12-29 10:03:27 +08:00
|
|
|
|
return *s
|
2025-12-26 15:40:24 +08:00
|
|
|
|
}
|