@@ -94,9 +94,9 @@ func (h *GroupHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
outGroups := make([]dto.Group, 0, len(groups))
|
||||
outGroups := make([]dto.AdminGroup, 0, len(groups))
|
||||
for i := range groups {
|
||||
outGroups = append(outGroups, *dto.GroupFromService(&groups[i]))
|
||||
outGroups = append(outGroups, *dto.GroupFromServiceAdmin(&groups[i]))
|
||||
}
|
||||
response.Paginated(c, outGroups, total, page, pageSize)
|
||||
}
|
||||
@@ -120,9 +120,9 @@ func (h *GroupHandler) GetAll(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
outGroups := make([]dto.Group, 0, len(groups))
|
||||
outGroups := make([]dto.AdminGroup, 0, len(groups))
|
||||
for i := range groups {
|
||||
outGroups = append(outGroups, *dto.GroupFromService(&groups[i]))
|
||||
outGroups = append(outGroups, *dto.GroupFromServiceAdmin(&groups[i]))
|
||||
}
|
||||
response.Success(c, outGroups)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (h *GroupHandler) GetByID(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.GroupFromService(group))
|
||||
response.Success(c, dto.GroupFromServiceAdmin(group))
|
||||
}
|
||||
|
||||
// Create handles creating a new group
|
||||
@@ -177,7 +177,7 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.GroupFromService(group))
|
||||
response.Success(c, dto.GroupFromServiceAdmin(group))
|
||||
}
|
||||
|
||||
// Update handles updating a group
|
||||
@@ -219,7 +219,7 @@ func (h *GroupHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.GroupFromService(group))
|
||||
response.Success(c, dto.GroupFromServiceAdmin(group))
|
||||
}
|
||||
|
||||
// Delete handles deleting a group
|
||||
|
||||
@@ -54,9 +54,9 @@ func (h *RedeemHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.RedeemCode, 0, len(codes))
|
||||
out := make([]dto.AdminRedeemCode, 0, len(codes))
|
||||
for i := range codes {
|
||||
out = append(out, *dto.RedeemCodeFromService(&codes[i]))
|
||||
out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
|
||||
}
|
||||
response.Paginated(c, out, total, page, pageSize)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (h *RedeemHandler) GetByID(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.RedeemCodeFromService(code))
|
||||
response.Success(c, dto.RedeemCodeFromServiceAdmin(code))
|
||||
}
|
||||
|
||||
// Generate handles generating new redeem codes
|
||||
@@ -100,9 +100,9 @@ func (h *RedeemHandler) Generate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.RedeemCode, 0, len(codes))
|
||||
out := make([]dto.AdminRedeemCode, 0, len(codes))
|
||||
for i := range codes {
|
||||
out = append(out, *dto.RedeemCodeFromService(&codes[i]))
|
||||
out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
|
||||
}
|
||||
response.Success(c, out)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (h *RedeemHandler) Expire(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.RedeemCodeFromService(code))
|
||||
response.Success(c, dto.RedeemCodeFromServiceAdmin(code))
|
||||
}
|
||||
|
||||
// GetStats handles getting redeem code statistics
|
||||
|
||||
@@ -83,9 +83,9 @@ func (h *SubscriptionHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
||||
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]))
|
||||
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
||||
}
|
||||
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func (h *SubscriptionHandler) GetByID(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserSubscriptionFromService(subscription))
|
||||
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
||||
}
|
||||
|
||||
// GetProgress handles getting subscription usage progress
|
||||
@@ -150,7 +150,7 @@ func (h *SubscriptionHandler) Assign(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserSubscriptionFromService(subscription))
|
||||
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
||||
}
|
||||
|
||||
// BulkAssign handles bulk assigning subscriptions to multiple users
|
||||
@@ -201,7 +201,7 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserSubscriptionFromService(subscription))
|
||||
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
||||
}
|
||||
|
||||
// Revoke handles revoking a subscription
|
||||
@@ -239,9 +239,9 @@ func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
||||
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]))
|
||||
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
||||
}
|
||||
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
||||
}
|
||||
@@ -261,9 +261,9 @@ func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
||||
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
||||
for i := range subscriptions {
|
||||
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]))
|
||||
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
||||
}
|
||||
response.Success(c, out)
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.UsageLog, 0, len(records))
|
||||
out := make([]dto.AdminUsageLog, 0, len(records))
|
||||
for i := range records {
|
||||
out = append(out, *dto.UsageLogFromServiceAdmin(&records[i]))
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ func (h *UserHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.User, 0, len(users))
|
||||
out := make([]dto.AdminUser, 0, len(users))
|
||||
for i := range users {
|
||||
out = append(out, *dto.UserFromService(&users[i]))
|
||||
out = append(out, *dto.UserFromServiceAdmin(&users[i]))
|
||||
}
|
||||
response.Paginated(c, out, total, page, pageSize)
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func (h *UserHandler) GetByID(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserFromService(user))
|
||||
response.Success(c, dto.UserFromServiceAdmin(user))
|
||||
}
|
||||
|
||||
// Create handles creating a new user
|
||||
@@ -155,7 +155,7 @@ func (h *UserHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserFromService(user))
|
||||
response.Success(c, dto.UserFromServiceAdmin(user))
|
||||
}
|
||||
|
||||
// Update handles updating a user
|
||||
@@ -189,7 +189,7 @@ func (h *UserHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserFromService(user))
|
||||
response.Success(c, dto.UserFromServiceAdmin(user))
|
||||
}
|
||||
|
||||
// Delete handles deleting a user
|
||||
@@ -231,7 +231,7 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, dto.UserFromService(user))
|
||||
response.Success(c, dto.UserFromServiceAdmin(user))
|
||||
}
|
||||
|
||||
// GetUserAPIKeys handles getting user's API keys
|
||||
|
||||
@@ -15,7 +15,6 @@ func UserFromServiceShallow(u *service.User) *User {
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Notes: u.Notes,
|
||||
Role: u.Role,
|
||||
Balance: u.Balance,
|
||||
Concurrency: u.Concurrency,
|
||||
@@ -48,6 +47,22 @@ func UserFromService(u *service.User) *User {
|
||||
return out
|
||||
}
|
||||
|
||||
// UserFromServiceAdmin converts a service User to DTO for admin users.
|
||||
// It includes notes - user-facing endpoints must not use this.
|
||||
func UserFromServiceAdmin(u *service.User) *AdminUser {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
base := UserFromService(u)
|
||||
if base == nil {
|
||||
return nil
|
||||
}
|
||||
return &AdminUser{
|
||||
User: *base,
|
||||
Notes: u.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
func APIKeyFromService(k *service.APIKey) *APIKey {
|
||||
if k == nil {
|
||||
return nil
|
||||
@@ -72,36 +87,29 @@ func GroupFromServiceShallow(g *service.Group) *Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
return &Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
Description: g.Description,
|
||||
Platform: g.Platform,
|
||||
RateMultiplier: g.RateMultiplier,
|
||||
IsExclusive: g.IsExclusive,
|
||||
Status: g.Status,
|
||||
SubscriptionType: g.SubscriptionType,
|
||||
DailyLimitUSD: g.DailyLimitUSD,
|
||||
WeeklyLimitUSD: g.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: g.MonthlyLimitUSD,
|
||||
ImagePrice1K: g.ImagePrice1K,
|
||||
ImagePrice2K: g.ImagePrice2K,
|
||||
ImagePrice4K: g.ImagePrice4K,
|
||||
ClaudeCodeOnly: g.ClaudeCodeOnly,
|
||||
FallbackGroupID: g.FallbackGroupID,
|
||||
ModelRouting: g.ModelRouting,
|
||||
ModelRoutingEnabled: g.ModelRoutingEnabled,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
AccountCount: g.AccountCount,
|
||||
}
|
||||
out := groupFromServiceBase(g)
|
||||
return &out
|
||||
}
|
||||
|
||||
func GroupFromService(g *service.Group) *Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
out := GroupFromServiceShallow(g)
|
||||
return GroupFromServiceShallow(g)
|
||||
}
|
||||
|
||||
// GroupFromServiceAdmin converts a service Group to DTO for admin users.
|
||||
// It includes internal fields like model_routing and account_count.
|
||||
func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
out := &AdminGroup{
|
||||
Group: groupFromServiceBase(g),
|
||||
ModelRouting: g.ModelRouting,
|
||||
ModelRoutingEnabled: g.ModelRoutingEnabled,
|
||||
AccountCount: g.AccountCount,
|
||||
}
|
||||
if len(g.AccountGroups) > 0 {
|
||||
out.AccountGroups = make([]AccountGroup, 0, len(g.AccountGroups))
|
||||
for i := range g.AccountGroups {
|
||||
@@ -112,6 +120,29 @@ func GroupFromService(g *service.Group) *Group {
|
||||
return out
|
||||
}
|
||||
|
||||
func groupFromServiceBase(g *service.Group) Group {
|
||||
return Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
Description: g.Description,
|
||||
Platform: g.Platform,
|
||||
RateMultiplier: g.RateMultiplier,
|
||||
IsExclusive: g.IsExclusive,
|
||||
Status: g.Status,
|
||||
SubscriptionType: g.SubscriptionType,
|
||||
DailyLimitUSD: g.DailyLimitUSD,
|
||||
WeeklyLimitUSD: g.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: g.MonthlyLimitUSD,
|
||||
ImagePrice1K: g.ImagePrice1K,
|
||||
ImagePrice2K: g.ImagePrice2K,
|
||||
ImagePrice4K: g.ImagePrice4K,
|
||||
ClaudeCodeOnly: g.ClaudeCodeOnly,
|
||||
FallbackGroupID: g.FallbackGroupID,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func AccountFromServiceShallow(a *service.Account) *Account {
|
||||
if a == nil {
|
||||
return nil
|
||||
@@ -273,7 +304,24 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
|
||||
if rc == nil {
|
||||
return nil
|
||||
}
|
||||
return &RedeemCode{
|
||||
out := redeemCodeFromServiceBase(rc)
|
||||
return &out
|
||||
}
|
||||
|
||||
// RedeemCodeFromServiceAdmin converts a service RedeemCode to DTO for admin users.
|
||||
// It includes notes - user-facing endpoints must not use this.
|
||||
func RedeemCodeFromServiceAdmin(rc *service.RedeemCode) *AdminRedeemCode {
|
||||
if rc == nil {
|
||||
return nil
|
||||
}
|
||||
return &AdminRedeemCode{
|
||||
RedeemCode: redeemCodeFromServiceBase(rc),
|
||||
Notes: rc.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
func redeemCodeFromServiceBase(rc *service.RedeemCode) RedeemCode {
|
||||
return RedeemCode{
|
||||
ID: rc.ID,
|
||||
Code: rc.Code,
|
||||
Type: rc.Type,
|
||||
@@ -281,7 +329,6 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
|
||||
Status: rc.Status,
|
||||
UsedBy: rc.UsedBy,
|
||||
UsedAt: rc.UsedAt,
|
||||
Notes: rc.Notes,
|
||||
CreatedAt: rc.CreatedAt,
|
||||
GroupID: rc.GroupID,
|
||||
ValidityDays: rc.ValidityDays,
|
||||
@@ -302,14 +349,9 @@ func AccountSummaryFromService(a *service.Account) *AccountSummary {
|
||||
}
|
||||
}
|
||||
|
||||
// usageLogFromServiceBase is a helper that converts service UsageLog to DTO.
|
||||
// The account parameter allows caller to control what Account info is included.
|
||||
// The includeIPAddress parameter controls whether to include the IP address (admin-only).
|
||||
func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, includeIPAddress bool) *UsageLog {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
result := &UsageLog{
|
||||
func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
|
||||
// 普通用户 DTO:严禁包含管理员字段(例如 account_rate_multiplier、ip_address、account)。
|
||||
return UsageLog{
|
||||
ID: l.ID,
|
||||
UserID: l.UserID,
|
||||
APIKeyID: l.APIKeyID,
|
||||
@@ -331,7 +373,6 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
|
||||
TotalCost: l.TotalCost,
|
||||
ActualCost: l.ActualCost,
|
||||
RateMultiplier: l.RateMultiplier,
|
||||
AccountRateMultiplier: l.AccountRateMultiplier,
|
||||
BillingType: l.BillingType,
|
||||
Stream: l.Stream,
|
||||
DurationMs: l.DurationMs,
|
||||
@@ -342,30 +383,33 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
|
||||
CreatedAt: l.CreatedAt,
|
||||
User: UserFromServiceShallow(l.User),
|
||||
APIKey: APIKeyFromService(l.APIKey),
|
||||
Account: account,
|
||||
Group: GroupFromServiceShallow(l.Group),
|
||||
Subscription: UserSubscriptionFromService(l.Subscription),
|
||||
}
|
||||
// IP 地址仅对管理员可见
|
||||
if includeIPAddress {
|
||||
result.IPAddress = l.IPAddress
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UsageLogFromService converts a service UsageLog to DTO for regular users.
|
||||
// It excludes Account details and IP address - users should not see these.
|
||||
func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
return usageLogFromServiceBase(l, nil, false)
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
u := usageLogFromServiceUser(l)
|
||||
return &u
|
||||
}
|
||||
|
||||
// UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
|
||||
// It includes minimal Account info (ID, Name only) and IP address.
|
||||
func UsageLogFromServiceAdmin(l *service.UsageLog) *UsageLog {
|
||||
func UsageLogFromServiceAdmin(l *service.UsageLog) *AdminUsageLog {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return usageLogFromServiceBase(l, AccountSummaryFromService(l.Account), true)
|
||||
return &AdminUsageLog{
|
||||
UsageLog: usageLogFromServiceUser(l),
|
||||
AccountRateMultiplier: l.AccountRateMultiplier,
|
||||
IPAddress: l.IPAddress,
|
||||
Account: AccountSummaryFromService(l.Account),
|
||||
}
|
||||
}
|
||||
|
||||
func UsageCleanupTaskFromService(task *service.UsageCleanupTask) *UsageCleanupTask {
|
||||
@@ -414,7 +458,27 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
|
||||
if sub == nil {
|
||||
return nil
|
||||
}
|
||||
return &UserSubscription{
|
||||
out := userSubscriptionFromServiceBase(sub)
|
||||
return &out
|
||||
}
|
||||
|
||||
// UserSubscriptionFromServiceAdmin converts a service UserSubscription to DTO for admin users.
|
||||
// It includes assignment metadata and notes.
|
||||
func UserSubscriptionFromServiceAdmin(sub *service.UserSubscription) *AdminUserSubscription {
|
||||
if sub == nil {
|
||||
return nil
|
||||
}
|
||||
return &AdminUserSubscription{
|
||||
UserSubscription: userSubscriptionFromServiceBase(sub),
|
||||
AssignedBy: sub.AssignedBy,
|
||||
AssignedAt: sub.AssignedAt,
|
||||
Notes: sub.Notes,
|
||||
AssignedByUser: UserFromServiceShallow(sub.AssignedByUser),
|
||||
}
|
||||
}
|
||||
|
||||
func userSubscriptionFromServiceBase(sub *service.UserSubscription) UserSubscription {
|
||||
return UserSubscription{
|
||||
ID: sub.ID,
|
||||
UserID: sub.UserID,
|
||||
GroupID: sub.GroupID,
|
||||
@@ -427,14 +491,10 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
|
||||
DailyUsageUSD: sub.DailyUsageUSD,
|
||||
WeeklyUsageUSD: sub.WeeklyUsageUSD,
|
||||
MonthlyUsageUSD: sub.MonthlyUsageUSD,
|
||||
AssignedBy: sub.AssignedBy,
|
||||
AssignedAt: sub.AssignedAt,
|
||||
Notes: sub.Notes,
|
||||
CreatedAt: sub.CreatedAt,
|
||||
UpdatedAt: sub.UpdatedAt,
|
||||
User: UserFromServiceShallow(sub.User),
|
||||
Group: GroupFromServiceShallow(sub.Group),
|
||||
AssignedByUser: UserFromServiceShallow(sub.AssignedByUser),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,9 +502,9 @@ func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
subs := make([]UserSubscription, 0, len(r.Subscriptions))
|
||||
subs := make([]AdminUserSubscription, 0, len(r.Subscriptions))
|
||||
for i := range r.Subscriptions {
|
||||
subs = append(subs, *UserSubscriptionFromService(&r.Subscriptions[i]))
|
||||
subs = append(subs, *UserSubscriptionFromServiceAdmin(&r.Subscriptions[i]))
|
||||
}
|
||||
return &BulkAssignResult{
|
||||
SuccessCount: r.SuccessCount,
|
||||
|
||||
@@ -6,7 +6,6 @@ type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Notes string `json:"notes"`
|
||||
Role string `json:"role"`
|
||||
Balance float64 `json:"balance"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
@@ -19,6 +18,14 @@ type User struct {
|
||||
Subscriptions []UserSubscription `json:"subscriptions,omitempty"`
|
||||
}
|
||||
|
||||
// AdminUser 是管理员接口使用的 user DTO(包含敏感/内部字段)。
|
||||
// 注意:普通用户接口不得返回 notes 等管理员备注信息。
|
||||
type AdminUser struct {
|
||||
User
|
||||
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
@@ -58,13 +65,19 @@ type Group struct {
|
||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AdminGroup 是管理员接口使用的 group DTO(包含敏感/内部字段)。
|
||||
// 注意:普通用户接口不得返回 model_routing/account_count/account_groups 等内部信息。
|
||||
type AdminGroup struct {
|
||||
Group
|
||||
|
||||
// 模型路由配置(仅 anthropic 平台使用)
|
||||
ModelRouting map[string][]int64 `json:"model_routing"`
|
||||
ModelRoutingEnabled bool `json:"model_routing_enabled"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
||||
AccountCount int64 `json:"account_count,omitempty"`
|
||||
}
|
||||
@@ -180,7 +193,6 @@ type RedeemCode struct {
|
||||
Status string `json:"status"`
|
||||
UsedBy *int64 `json:"used_by"`
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
GroupID *int64 `json:"group_id"`
|
||||
@@ -190,6 +202,15 @@ type RedeemCode struct {
|
||||
Group *Group `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// AdminRedeemCode 是管理员接口使用的 redeem code DTO(包含 notes 等字段)。
|
||||
// 注意:普通用户接口不得返回 notes 等内部信息。
|
||||
type AdminRedeemCode struct {
|
||||
RedeemCode
|
||||
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// UsageLog 是普通用户接口使用的 usage log DTO(不包含管理员字段)。
|
||||
type UsageLog struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
@@ -209,14 +230,13 @@ type UsageLog struct {
|
||||
CacheCreation5mTokens int `json:"cache_creation_5m_tokens"`
|
||||
CacheCreation1hTokens int `json:"cache_creation_1h_tokens"`
|
||||
|
||||
InputCost float64 `json:"input_cost"`
|
||||
OutputCost float64 `json:"output_cost"`
|
||||
CacheCreationCost float64 `json:"cache_creation_cost"`
|
||||
CacheReadCost float64 `json:"cache_read_cost"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
ActualCost float64 `json:"actual_cost"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
AccountRateMultiplier *float64 `json:"account_rate_multiplier"`
|
||||
InputCost float64 `json:"input_cost"`
|
||||
OutputCost float64 `json:"output_cost"`
|
||||
CacheCreationCost float64 `json:"cache_creation_cost"`
|
||||
CacheReadCost float64 `json:"cache_read_cost"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
ActualCost float64 `json:"actual_cost"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
|
||||
BillingType int8 `json:"billing_type"`
|
||||
Stream bool `json:"stream"`
|
||||
@@ -230,18 +250,28 @@ type UsageLog struct {
|
||||
// User-Agent
|
||||
UserAgent *string `json:"user_agent"`
|
||||
|
||||
// IP 地址(仅管理员可见)
|
||||
IPAddress *string `json:"ip_address,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User *User `json:"user,omitempty"`
|
||||
APIKey *APIKey `json:"api_key,omitempty"`
|
||||
Account *AccountSummary `json:"account,omitempty"` // Use minimal AccountSummary to prevent data leakage
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Subscription *UserSubscription `json:"subscription,omitempty"`
|
||||
}
|
||||
|
||||
// AdminUsageLog 是管理员接口使用的 usage log DTO(包含管理员字段)。
|
||||
type AdminUsageLog struct {
|
||||
UsageLog
|
||||
|
||||
// AccountRateMultiplier 账号计费倍率快照(nil 表示按 1.0 处理)
|
||||
AccountRateMultiplier *float64 `json:"account_rate_multiplier"`
|
||||
|
||||
// IPAddress 用户请求 IP(仅管理员可见)
|
||||
IPAddress *string `json:"ip_address,omitempty"`
|
||||
|
||||
// Account 最小账号信息(避免泄露敏感字段)
|
||||
Account *AccountSummary `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
type UsageCleanupFilters struct {
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
@@ -300,23 +330,30 @@ type UserSubscription struct {
|
||||
WeeklyUsageUSD float64 `json:"weekly_usage_usd"`
|
||||
MonthlyUsageUSD float64 `json:"monthly_usage_usd"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
User *User `json:"user,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// AdminUserSubscription 是管理员接口使用的订阅 DTO(包含分配信息/备注等字段)。
|
||||
// 注意:普通用户接口不得返回 assigned_by/assigned_at/notes/assigned_by_user 等管理员字段。
|
||||
type AdminUserSubscription struct {
|
||||
UserSubscription
|
||||
|
||||
AssignedBy *int64 `json:"assigned_by"`
|
||||
AssignedAt time.Time `json:"assigned_at"`
|
||||
Notes string `json:"notes"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
User *User `json:"user,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
AssignedByUser *User `json:"assigned_by_user,omitempty"`
|
||||
AssignedByUser *User `json:"assigned_by_user,omitempty"`
|
||||
}
|
||||
|
||||
type BulkAssignResult struct {
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailedCount int `json:"failed_count"`
|
||||
Subscriptions []UserSubscription `json:"subscriptions"`
|
||||
Errors []string `json:"errors"`
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailedCount int `json:"failed_count"`
|
||||
Subscriptions []AdminUserSubscription `json:"subscriptions"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// PromoCode 注册优惠码
|
||||
|
||||
@@ -47,9 +47,6 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 清空notes字段,普通用户不应看到备注
|
||||
userData.Notes = ""
|
||||
|
||||
response.Success(c, dto.UserFromService(userData))
|
||||
}
|
||||
|
||||
@@ -105,8 +102,5 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 清空notes字段,普通用户不应看到备注
|
||||
updatedUser.Notes = ""
|
||||
|
||||
response.Success(c, dto.UserFromService(updatedUser))
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ func TestAPIContracts(t *testing.T) {
|
||||
"id": 1,
|
||||
"email": "alice@example.com",
|
||||
"username": "alice",
|
||||
"notes": "hello",
|
||||
"role": "user",
|
||||
"balance": 12.5,
|
||||
"concurrency": 5,
|
||||
@@ -131,6 +130,153 @@ func TestAPIContracts(t *testing.T) {
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "GET /api/v1/groups/available",
|
||||
setup: func(t *testing.T, deps *contractDeps) {
|
||||
t.Helper()
|
||||
// 普通用户可见的分组列表不应包含内部字段(如 model_routing/account_count)。
|
||||
deps.groupRepo.SetActive([]service.Group{
|
||||
{
|
||||
ID: 10,
|
||||
Name: "Group One",
|
||||
Description: "desc",
|
||||
Platform: service.PlatformAnthropic,
|
||||
RateMultiplier: 1.5,
|
||||
IsExclusive: false,
|
||||
Status: service.StatusActive,
|
||||
SubscriptionType: service.SubscriptionTypeStandard,
|
||||
ModelRoutingEnabled: true,
|
||||
ModelRouting: map[string][]int64{
|
||||
"claude-3-*": []int64{101, 102},
|
||||
},
|
||||
AccountCount: 2,
|
||||
CreatedAt: deps.now,
|
||||
UpdatedAt: deps.now,
|
||||
},
|
||||
})
|
||||
deps.userSubRepo.SetActiveByUserID(1, nil)
|
||||
},
|
||||
method: http.MethodGet,
|
||||
path: "/api/v1/groups/available",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: `{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Group One",
|
||||
"description": "desc",
|
||||
"platform": "anthropic",
|
||||
"rate_multiplier": 1.5,
|
||||
"is_exclusive": false,
|
||||
"status": "active",
|
||||
"subscription_type": "standard",
|
||||
"daily_limit_usd": null,
|
||||
"weekly_limit_usd": null,
|
||||
"monthly_limit_usd": null,
|
||||
"image_price_1k": null,
|
||||
"image_price_2k": null,
|
||||
"image_price_4k": null,
|
||||
"claude_code_only": false,
|
||||
"fallback_group_id": null,
|
||||
"created_at": "2025-01-02T03:04:05Z",
|
||||
"updated_at": "2025-01-02T03:04:05Z"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "GET /api/v1/subscriptions",
|
||||
setup: func(t *testing.T, deps *contractDeps) {
|
||||
t.Helper()
|
||||
// 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。
|
||||
deps.userSubRepo.SetByUserID(1, []service.UserSubscription{
|
||||
{
|
||||
ID: 501,
|
||||
UserID: 1,
|
||||
GroupID: 10,
|
||||
StartsAt: deps.now,
|
||||
ExpiresAt: deps.now.Add(24 * time.Hour),
|
||||
Status: service.SubscriptionStatusActive,
|
||||
DailyUsageUSD: 1.23,
|
||||
WeeklyUsageUSD: 2.34,
|
||||
MonthlyUsageUSD: 3.45,
|
||||
AssignedBy: ptr(int64(999)),
|
||||
AssignedAt: deps.now,
|
||||
Notes: "admin-note",
|
||||
CreatedAt: deps.now,
|
||||
UpdatedAt: deps.now,
|
||||
},
|
||||
})
|
||||
},
|
||||
method: http.MethodGet,
|
||||
path: "/api/v1/subscriptions",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: `{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 501,
|
||||
"user_id": 1,
|
||||
"group_id": 10,
|
||||
"starts_at": "2025-01-02T03:04:05Z",
|
||||
"expires_at": "2025-01-03T03:04:05Z",
|
||||
"status": "active",
|
||||
"daily_window_start": null,
|
||||
"weekly_window_start": null,
|
||||
"monthly_window_start": null,
|
||||
"daily_usage_usd": 1.23,
|
||||
"weekly_usage_usd": 2.34,
|
||||
"monthly_usage_usd": 3.45,
|
||||
"created_at": "2025-01-02T03:04:05Z",
|
||||
"updated_at": "2025-01-02T03:04:05Z"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "GET /api/v1/redeem/history",
|
||||
setup: func(t *testing.T, deps *contractDeps) {
|
||||
t.Helper()
|
||||
// 普通用户兑换历史不应包含 notes 等内部字段。
|
||||
deps.redeemRepo.SetByUser(1, []service.RedeemCode{
|
||||
{
|
||||
ID: 900,
|
||||
Code: "CODE-123",
|
||||
Type: service.RedeemTypeBalance,
|
||||
Value: 1.25,
|
||||
Status: service.StatusUsed,
|
||||
UsedBy: ptr(int64(1)),
|
||||
UsedAt: ptr(deps.now),
|
||||
Notes: "internal-note",
|
||||
CreatedAt: deps.now,
|
||||
},
|
||||
})
|
||||
},
|
||||
method: http.MethodGet,
|
||||
path: "/api/v1/redeem/history",
|
||||
wantStatus: http.StatusOK,
|
||||
wantJSON: `{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 900,
|
||||
"code": "CODE-123",
|
||||
"type": "balance",
|
||||
"value": 1.25,
|
||||
"status": "used",
|
||||
"used_by": 1,
|
||||
"used_at": "2025-01-02T03:04:05Z",
|
||||
"created_at": "2025-01-02T03:04:05Z",
|
||||
"group_id": null,
|
||||
"validity_days": 0
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "GET /api/v1/usage/stats",
|
||||
setup: func(t *testing.T, deps *contractDeps) {
|
||||
@@ -190,24 +336,25 @@ func TestAPIContracts(t *testing.T) {
|
||||
t.Helper()
|
||||
deps.usageRepo.SetUserLogs(1, []service.UsageLog{
|
||||
{
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
APIKeyID: 100,
|
||||
AccountID: 200,
|
||||
RequestID: "req_123",
|
||||
Model: "claude-3",
|
||||
InputTokens: 10,
|
||||
OutputTokens: 20,
|
||||
CacheCreationTokens: 1,
|
||||
CacheReadTokens: 2,
|
||||
TotalCost: 0.5,
|
||||
ActualCost: 0.5,
|
||||
RateMultiplier: 1,
|
||||
BillingType: service.BillingTypeBalance,
|
||||
Stream: true,
|
||||
DurationMs: ptr(100),
|
||||
FirstTokenMs: ptr(50),
|
||||
CreatedAt: deps.now,
|
||||
ID: 1,
|
||||
UserID: 1,
|
||||
APIKeyID: 100,
|
||||
AccountID: 200,
|
||||
AccountRateMultiplier: ptr(0.5),
|
||||
RequestID: "req_123",
|
||||
Model: "claude-3",
|
||||
InputTokens: 10,
|
||||
OutputTokens: 20,
|
||||
CacheCreationTokens: 1,
|
||||
CacheReadTokens: 2,
|
||||
TotalCost: 0.5,
|
||||
ActualCost: 0.5,
|
||||
RateMultiplier: 1,
|
||||
BillingType: service.BillingTypeBalance,
|
||||
Stream: true,
|
||||
DurationMs: ptr(100),
|
||||
FirstTokenMs: ptr(50),
|
||||
CreatedAt: deps.now,
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -238,10 +385,9 @@ func TestAPIContracts(t *testing.T) {
|
||||
"output_cost": 0,
|
||||
"cache_creation_cost": 0,
|
||||
"cache_read_cost": 0,
|
||||
"total_cost": 0.5,
|
||||
"total_cost": 0.5,
|
||||
"actual_cost": 0.5,
|
||||
"rate_multiplier": 1,
|
||||
"account_rate_multiplier": null,
|
||||
"billing_type": 0,
|
||||
"stream": true,
|
||||
"duration_ms": 100,
|
||||
@@ -386,8 +532,11 @@ type contractDeps struct {
|
||||
now time.Time
|
||||
router http.Handler
|
||||
apiKeyRepo *stubApiKeyRepo
|
||||
groupRepo *stubGroupRepo
|
||||
userSubRepo *stubUserSubscriptionRepo
|
||||
usageRepo *stubUsageLogRepo
|
||||
settingRepo *stubSettingRepo
|
||||
redeemRepo *stubRedeemCodeRepo
|
||||
}
|
||||
|
||||
func newContractDeps(t *testing.T) *contractDeps {
|
||||
@@ -415,11 +564,11 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
|
||||
apiKeyRepo := newStubApiKeyRepo(now)
|
||||
apiKeyCache := stubApiKeyCache{}
|
||||
groupRepo := stubGroupRepo{}
|
||||
userSubRepo := stubUserSubscriptionRepo{}
|
||||
groupRepo := &stubGroupRepo{}
|
||||
userSubRepo := &stubUserSubscriptionRepo{}
|
||||
accountRepo := stubAccountRepo{}
|
||||
proxyRepo := stubProxyRepo{}
|
||||
redeemRepo := stubRedeemCodeRepo{}
|
||||
redeemRepo := &stubRedeemCodeRepo{}
|
||||
|
||||
cfg := &config.Config{
|
||||
Default: config.DefaultConfig{
|
||||
@@ -434,6 +583,12 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
usageRepo := newStubUsageLogRepo()
|
||||
usageService := service.NewUsageService(usageRepo, userRepo, nil, nil)
|
||||
|
||||
subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil)
|
||||
subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
|
||||
|
||||
redeemService := service.NewRedeemService(redeemRepo, userRepo, subscriptionService, nil, nil, nil, nil)
|
||||
redeemHandler := handler.NewRedeemHandler(redeemService)
|
||||
|
||||
settingRepo := newStubSettingRepo()
|
||||
settingService := service.NewSettingService(settingRepo, cfg)
|
||||
|
||||
@@ -473,12 +628,21 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
v1Keys.Use(jwtAuth)
|
||||
v1Keys.GET("/keys", apiKeyHandler.List)
|
||||
v1Keys.POST("/keys", apiKeyHandler.Create)
|
||||
v1Keys.GET("/groups/available", apiKeyHandler.GetAvailableGroups)
|
||||
|
||||
v1Usage := v1.Group("")
|
||||
v1Usage.Use(jwtAuth)
|
||||
v1Usage.GET("/usage", usageHandler.List)
|
||||
v1Usage.GET("/usage/stats", usageHandler.Stats)
|
||||
|
||||
v1Subs := v1.Group("")
|
||||
v1Subs.Use(jwtAuth)
|
||||
v1Subs.GET("/subscriptions", subscriptionHandler.List)
|
||||
|
||||
v1Redeem := v1.Group("")
|
||||
v1Redeem.Use(jwtAuth)
|
||||
v1Redeem.GET("/redeem/history", redeemHandler.GetHistory)
|
||||
|
||||
v1Admin := v1.Group("/admin")
|
||||
v1Admin.Use(adminAuth)
|
||||
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
||||
@@ -488,8 +652,11 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
now: now,
|
||||
router: r,
|
||||
apiKeyRepo: apiKeyRepo,
|
||||
groupRepo: groupRepo,
|
||||
userSubRepo: userSubRepo,
|
||||
usageRepo: usageRepo,
|
||||
settingRepo: settingRepo,
|
||||
redeemRepo: redeemRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +794,13 @@ func (stubApiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handl
|
||||
return nil
|
||||
}
|
||||
|
||||
type stubGroupRepo struct{}
|
||||
type stubGroupRepo struct {
|
||||
active []service.Group
|
||||
}
|
||||
|
||||
func (r *stubGroupRepo) SetActive(groups []service.Group) {
|
||||
r.active = append([]service.Group(nil), groups...)
|
||||
}
|
||||
|
||||
func (stubGroupRepo) Create(ctx context.Context, group *service.Group) error {
|
||||
return errors.New("not implemented")
|
||||
@@ -661,12 +834,19 @@ func (stubGroupRepo) ListWithFilters(ctx context.Context, params pagination.Pagi
|
||||
return nil, nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (stubGroupRepo) ListActive(ctx context.Context) ([]service.Group, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
func (r *stubGroupRepo) ListActive(ctx context.Context) ([]service.Group, error) {
|
||||
return append([]service.Group(nil), r.active...), nil
|
||||
}
|
||||
|
||||
func (stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
func (r *stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) {
|
||||
out := make([]service.Group, 0, len(r.active))
|
||||
for i := range r.active {
|
||||
g := r.active[i]
|
||||
if g.Platform == platform {
|
||||
out = append(out, g)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error) {
|
||||
@@ -884,7 +1064,16 @@ func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
type stubRedeemCodeRepo struct{}
|
||||
type stubRedeemCodeRepo struct {
|
||||
byUser map[int64][]service.RedeemCode
|
||||
}
|
||||
|
||||
func (r *stubRedeemCodeRepo) SetByUser(userID int64, codes []service.RedeemCode) {
|
||||
if r.byUser == nil {
|
||||
r.byUser = make(map[int64][]service.RedeemCode)
|
||||
}
|
||||
r.byUser[userID] = append([]service.RedeemCode(nil), codes...)
|
||||
}
|
||||
|
||||
func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error {
|
||||
return errors.New("not implemented")
|
||||
@@ -922,11 +1111,35 @@ func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination
|
||||
return nil, nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
func (r *stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
|
||||
if r.byUser == nil {
|
||||
return nil, nil
|
||||
}
|
||||
codes := r.byUser[userID]
|
||||
if limit > 0 && len(codes) > limit {
|
||||
codes = codes[:limit]
|
||||
}
|
||||
return append([]service.RedeemCode(nil), codes...), nil
|
||||
}
|
||||
|
||||
type stubUserSubscriptionRepo struct{}
|
||||
type stubUserSubscriptionRepo struct {
|
||||
byUser map[int64][]service.UserSubscription
|
||||
activeByUser map[int64][]service.UserSubscription
|
||||
}
|
||||
|
||||
func (r *stubUserSubscriptionRepo) SetByUserID(userID int64, subs []service.UserSubscription) {
|
||||
if r.byUser == nil {
|
||||
r.byUser = make(map[int64][]service.UserSubscription)
|
||||
}
|
||||
r.byUser[userID] = append([]service.UserSubscription(nil), subs...)
|
||||
}
|
||||
|
||||
func (r *stubUserSubscriptionRepo) SetActiveByUserID(userID int64, subs []service.UserSubscription) {
|
||||
if r.activeByUser == nil {
|
||||
r.activeByUser = make(map[int64][]service.UserSubscription)
|
||||
}
|
||||
r.activeByUser[userID] = append([]service.UserSubscription(nil), subs...)
|
||||
}
|
||||
|
||||
func (stubUserSubscriptionRepo) Create(ctx context.Context, sub *service.UserSubscription) error {
|
||||
return errors.New("not implemented")
|
||||
@@ -946,11 +1159,17 @@ func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSub
|
||||
func (stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
|
||||
if r.byUser == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return append([]service.UserSubscription(nil), r.byUser[userID]...), nil
|
||||
}
|
||||
func (stubUserSubscriptionRepo) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
func (r *stubUserSubscriptionRepo) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
|
||||
if r.activeByUser == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return append([]service.UserSubscription(nil), r.activeByUser[userID]...), nil
|
||||
}
|
||||
func (stubUserSubscriptionRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.UserSubscription, *pagination.PaginationResult, error) {
|
||||
return nil, nil, errors.New("not implemented")
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { apiClient } from '../client'
|
||||
import type {
|
||||
Group,
|
||||
AdminGroup,
|
||||
GroupPlatform,
|
||||
CreateGroupRequest,
|
||||
UpdateGroupRequest,
|
||||
@@ -31,8 +31,8 @@ export async function list(
|
||||
options?: {
|
||||
signal?: AbortSignal
|
||||
}
|
||||
): Promise<PaginatedResponse<Group>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<Group>>('/admin/groups', {
|
||||
): Promise<PaginatedResponse<AdminGroup>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<AdminGroup>>('/admin/groups', {
|
||||
params: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
@@ -48,8 +48,8 @@ export async function list(
|
||||
* @param platform - Optional platform filter
|
||||
* @returns List of all active groups
|
||||
*/
|
||||
export async function getAll(platform?: GroupPlatform): Promise<Group[]> {
|
||||
const { data } = await apiClient.get<Group[]>('/admin/groups/all', {
|
||||
export async function getAll(platform?: GroupPlatform): Promise<AdminGroup[]> {
|
||||
const { data } = await apiClient.get<AdminGroup[]>('/admin/groups/all', {
|
||||
params: platform ? { platform } : undefined
|
||||
})
|
||||
return data
|
||||
@@ -60,7 +60,7 @@ export async function getAll(platform?: GroupPlatform): Promise<Group[]> {
|
||||
* @param platform - Platform to filter by
|
||||
* @returns List of groups for the specified platform
|
||||
*/
|
||||
export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> {
|
||||
export async function getByPlatform(platform: GroupPlatform): Promise<AdminGroup[]> {
|
||||
return getAll(platform)
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> {
|
||||
* @param id - Group ID
|
||||
* @returns Group details
|
||||
*/
|
||||
export async function getById(id: number): Promise<Group> {
|
||||
const { data } = await apiClient.get<Group>(`/admin/groups/${id}`)
|
||||
export async function getById(id: number): Promise<AdminGroup> {
|
||||
const { data } = await apiClient.get<AdminGroup>(`/admin/groups/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ export async function getById(id: number): Promise<Group> {
|
||||
* @param groupData - Group data
|
||||
* @returns Created group
|
||||
*/
|
||||
export async function create(groupData: CreateGroupRequest): Promise<Group> {
|
||||
const { data } = await apiClient.post<Group>('/admin/groups', groupData)
|
||||
export async function create(groupData: CreateGroupRequest): Promise<AdminGroup> {
|
||||
const { data } = await apiClient.post<AdminGroup>('/admin/groups', groupData)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ export async function create(groupData: CreateGroupRequest): Promise<Group> {
|
||||
* @param updates - Fields to update
|
||||
* @returns Updated group
|
||||
*/
|
||||
export async function update(id: number, updates: UpdateGroupRequest): Promise<Group> {
|
||||
const { data } = await apiClient.put<Group>(`/admin/groups/${id}`, updates)
|
||||
export async function update(id: number, updates: UpdateGroupRequest): Promise<AdminGroup> {
|
||||
const { data } = await apiClient.put<AdminGroup>(`/admin/groups/${id}`, updates)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export async function deleteGroup(id: number): Promise<{ message: string }> {
|
||||
* @param status - New status
|
||||
* @returns Updated group
|
||||
*/
|
||||
export async function toggleStatus(id: number, status: 'active' | 'inactive'): Promise<Group> {
|
||||
export async function toggleStatus(id: number, status: 'active' | 'inactive'): Promise<AdminGroup> {
|
||||
return update(id, { status })
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client'
|
||||
import type { UsageLog, UsageQueryParams, PaginatedResponse } from '@/types'
|
||||
import type { AdminUsageLog, UsageQueryParams, PaginatedResponse } from '@/types'
|
||||
|
||||
// ==================== Types ====================
|
||||
|
||||
@@ -85,8 +85,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
|
||||
export async function list(
|
||||
params: AdminUsageQueryParams,
|
||||
options?: { signal?: AbortSignal }
|
||||
): Promise<PaginatedResponse<UsageLog>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<UsageLog>>('/admin/usage', {
|
||||
): Promise<PaginatedResponse<AdminUsageLog>> {
|
||||
const { data } = await apiClient.get<PaginatedResponse<AdminUsageLog>>('/admin/usage', {
|
||||
params,
|
||||
signal: options?.signal
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client'
|
||||
import type { User, UpdateUserRequest, PaginatedResponse } from '@/types'
|
||||
import type { AdminUser, UpdateUserRequest, PaginatedResponse } from '@/types'
|
||||
|
||||
/**
|
||||
* List all users with pagination
|
||||
@@ -26,7 +26,7 @@ export async function list(
|
||||
options?: {
|
||||
signal?: AbortSignal
|
||||
}
|
||||
): Promise<PaginatedResponse<User>> {
|
||||
): Promise<PaginatedResponse<AdminUser>> {
|
||||
// Build params with attribute filters in attr[id]=value format
|
||||
const params: Record<string, any> = {
|
||||
page,
|
||||
@@ -44,8 +44,7 @@ export async function list(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await apiClient.get<PaginatedResponse<User>>('/admin/users', {
|
||||
const { data } = await apiClient.get<PaginatedResponse<AdminUser>>('/admin/users', {
|
||||
params,
|
||||
signal: options?.signal
|
||||
})
|
||||
@@ -57,8 +56,8 @@ export async function list(
|
||||
* @param id - User ID
|
||||
* @returns User details
|
||||
*/
|
||||
export async function getById(id: number): Promise<User> {
|
||||
const { data } = await apiClient.get<User>(`/admin/users/${id}`)
|
||||
export async function getById(id: number): Promise<AdminUser> {
|
||||
const { data } = await apiClient.get<AdminUser>(`/admin/users/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -73,8 +72,8 @@ export async function create(userData: {
|
||||
balance?: number
|
||||
concurrency?: number
|
||||
allowed_groups?: number[] | null
|
||||
}): Promise<User> {
|
||||
const { data } = await apiClient.post<User>('/admin/users', userData)
|
||||
}): Promise<AdminUser> {
|
||||
const { data } = await apiClient.post<AdminUser>('/admin/users', userData)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -84,8 +83,8 @@ export async function create(userData: {
|
||||
* @param updates - Fields to update
|
||||
* @returns Updated user
|
||||
*/
|
||||
export async function update(id: number, updates: UpdateUserRequest): Promise<User> {
|
||||
const { data } = await apiClient.put<User>(`/admin/users/${id}`, updates)
|
||||
export async function update(id: number, updates: UpdateUserRequest): Promise<AdminUser> {
|
||||
const { data } = await apiClient.put<AdminUser>(`/admin/users/${id}`, updates)
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -112,8 +111,8 @@ export async function updateBalance(
|
||||
balance: number,
|
||||
operation: 'set' | 'add' | 'subtract' = 'set',
|
||||
notes?: string
|
||||
): Promise<User> {
|
||||
const { data } = await apiClient.post<User>(`/admin/users/${id}/balance`, {
|
||||
): Promise<AdminUser> {
|
||||
const { data } = await apiClient.post<AdminUser>(`/admin/users/${id}/balance`, {
|
||||
balance,
|
||||
operation,
|
||||
notes: notes || ''
|
||||
@@ -127,7 +126,7 @@ export async function updateBalance(
|
||||
* @param concurrency - New concurrency limit
|
||||
* @returns Updated user
|
||||
*/
|
||||
export async function updateConcurrency(id: number, concurrency: number): Promise<User> {
|
||||
export async function updateConcurrency(id: number, concurrency: number): Promise<AdminUser> {
|
||||
return update(id, { concurrency })
|
||||
}
|
||||
|
||||
@@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis
|
||||
* @param status - New status
|
||||
* @returns Updated user
|
||||
*/
|
||||
export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<User> {
|
||||
export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise<AdminUser> {
|
||||
return update(id, { status })
|
||||
}
|
||||
|
||||
|
||||
@@ -648,7 +648,7 @@ import { ref, watch, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Proxy, Group } from '@/types'
|
||||
import type { Proxy, AdminGroup } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||
@@ -659,7 +659,7 @@ interface Props {
|
||||
show: boolean
|
||||
accountIds: number[]
|
||||
proxies: Proxy[]
|
||||
groups: Group[]
|
||||
groups: AdminGroup[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -1816,7 +1816,7 @@ import {
|
||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
|
||||
import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||
@@ -1862,7 +1862,7 @@ const apiKeyHint = computed(() => {
|
||||
interface Props {
|
||||
show: boolean
|
||||
proxies: Proxy[]
|
||||
groups: Group[]
|
||||
groups: AdminGroup[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -883,7 +883,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, Proxy, Group } from '@/types'
|
||||
import type { Account, Proxy, AdminGroup } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -901,7 +901,7 @@ interface Props {
|
||||
show: boolean
|
||||
account: Account | null
|
||||
proxies: Proxy[]
|
||||
groups: Group[]
|
||||
groups: AdminGroup[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -239,7 +239,7 @@ import { formatDateTime } from '@/utils/format'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import type { UsageLog } from '@/types'
|
||||
import type { AdminUsageLog } from '@/types'
|
||||
|
||||
defineProps(['data', 'loading'])
|
||||
const { t } = useI18n()
|
||||
@@ -247,12 +247,12 @@ const { t } = useI18n()
|
||||
// Tooltip state - cost
|
||||
const tooltipVisible = ref(false)
|
||||
const tooltipPosition = ref({ x: 0, y: 0 })
|
||||
const tooltipData = ref<UsageLog | null>(null)
|
||||
const tooltipData = ref<AdminUsageLog | null>(null)
|
||||
|
||||
// Tooltip state - token
|
||||
const tokenTooltipVisible = ref(false)
|
||||
const tokenTooltipPosition = ref({ x: 0, y: 0 })
|
||||
const tokenTooltipData = ref<UsageLog | null>(null)
|
||||
const tokenTooltipData = ref<AdminUsageLog | null>(null)
|
||||
|
||||
const cols = computed(() => [
|
||||
{ key: 'user', label: t('admin.usage.user'), sortable: false },
|
||||
@@ -296,7 +296,7 @@ const formatDuration = (ms: number | null | undefined): string => {
|
||||
}
|
||||
|
||||
// Cost tooltip functions
|
||||
const showTooltip = (event: MouseEvent, row: UsageLog) => {
|
||||
const showTooltip = (event: MouseEvent, row: AdminUsageLog) => {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
tooltipData.value = row
|
||||
@@ -311,7 +311,7 @@ const hideTooltip = () => {
|
||||
}
|
||||
|
||||
// Token tooltip functions
|
||||
const showTokenTooltip = (event: MouseEvent, row: UsageLog) => {
|
||||
const showTokenTooltip = (event: MouseEvent, row: AdminUsageLog) => {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
tokenTooltipData.value = row
|
||||
|
||||
@@ -39,10 +39,10 @@ import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { User, Group } from '@/types'
|
||||
import type { AdminUser, Group } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean, user: User | null }>()
|
||||
const props = defineProps<{ show: boolean, user: AdminUser | null }>()
|
||||
const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore()
|
||||
|
||||
const groups = ref<Group[]>([]); const selectedIds = ref<number[]>([]); const loading = ref(false); const submitting = ref(false)
|
||||
@@ -56,4 +56,4 @@ const handleSave = async () => {
|
||||
appStore.showSuccess(t('admin.users.allowedGroupsUpdated')); emit('success'); emit('close')
|
||||
} catch (error) { console.error('Failed to update allowed groups:', error) } finally { submitting.value = false }
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -32,10 +32,10 @@ import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import type { User, ApiKey } from '@/types'
|
||||
import type { AdminUser, ApiKey } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean, user: User | null }>()
|
||||
const props = defineProps<{ show: boolean, user: AdminUser | null }>()
|
||||
defineEmits(['close']); const { t } = useI18n()
|
||||
const apiKeys = ref<ApiKey[]>([]); const loading = ref(false)
|
||||
|
||||
@@ -44,4 +44,4 @@ const load = async () => {
|
||||
if (!props.user) return; loading.value = true
|
||||
try { const res = await adminAPI.users.getUserApiKeys(props.user.id); apiKeys.value = res.items || [] } catch (error) { console.error('Failed to load API keys:', error) } finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -29,10 +29,10 @@ import { reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { User } from '@/types'
|
||||
import type { AdminUser } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean, user: User | null, operation: 'add' | 'subtract' }>()
|
||||
const props = defineProps<{ show: boolean, user: AdminUser | null, operation: 'add' | 'subtract' }>()
|
||||
const emit = defineEmits(['close', 'success']); const { t } = useI18n(); const appStore = useAppStore()
|
||||
|
||||
const submitting = ref(false); const form = reactive({ amount: 0, notes: '' })
|
||||
|
||||
@@ -56,12 +56,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { User, UserAttributeValuesMap } from '@/types'
|
||||
import type { AdminUser, UserAttributeValuesMap } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import UserAttributeForm from '@/components/user/UserAttributeForm.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const props = defineProps<{ show: boolean, user: User | null }>()
|
||||
const props = defineProps<{ show: boolean, user: AdminUser | null }>()
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
const { t } = useI18n(); const appStore = useAppStore(); const { copyToClipboard } = useClipboard()
|
||||
|
||||
|
||||
@@ -42,13 +42,13 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import GroupBadge from './GroupBadge.vue'
|
||||
import type { Group, GroupPlatform } from '@/types'
|
||||
import type { AdminGroup, GroupPlatform } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
modelValue: number[]
|
||||
groups: Group[]
|
||||
groups: AdminGroup[]
|
||||
platform?: GroupPlatform // Optional platform filter
|
||||
mixedScheduling?: boolean // For antigravity accounts: allow anthropic/gemini groups
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface FetchOptions {
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
notes: string
|
||||
email: string
|
||||
role: 'admin' | 'user' // User role for authorization
|
||||
balance: number // User balance for API usage
|
||||
@@ -39,6 +38,11 @@ export interface User {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminUser extends User {
|
||||
// 管理员备注(普通用户接口不返回)
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string
|
||||
password: string
|
||||
@@ -270,14 +274,19 @@ export interface Group {
|
||||
// Claude Code 客户端限制
|
||||
claude_code_only: boolean
|
||||
fallback_group_id: number | null
|
||||
// 模型路由配置(仅 anthropic 平台使用)
|
||||
model_routing: Record<string, number[]> | null
|
||||
model_routing_enabled: boolean
|
||||
account_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminGroup extends Group {
|
||||
// 模型路由配置(仅管理员可见,内部信息)
|
||||
model_routing: Record<string, number[]> | null
|
||||
model_routing_enabled: boolean
|
||||
|
||||
// 分组下账号数量(仅管理员可见)
|
||||
account_count?: number
|
||||
}
|
||||
|
||||
export interface ApiKey {
|
||||
id: number
|
||||
user_id: number
|
||||
@@ -637,7 +646,6 @@ export interface UsageLog {
|
||||
total_cost: number
|
||||
actual_cost: number
|
||||
rate_multiplier: number
|
||||
account_rate_multiplier?: number | null
|
||||
billing_type: number
|
||||
|
||||
stream: boolean
|
||||
@@ -651,18 +659,30 @@ export interface UsageLog {
|
||||
// User-Agent
|
||||
user_agent: string | null
|
||||
|
||||
// IP 地址(仅管理员可见)
|
||||
ip_address: string | null
|
||||
|
||||
created_at: string
|
||||
|
||||
user?: User
|
||||
api_key?: ApiKey
|
||||
account?: Account
|
||||
group?: Group
|
||||
subscription?: UserSubscription
|
||||
}
|
||||
|
||||
export interface UsageLogAccountSummary {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface AdminUsageLog extends UsageLog {
|
||||
// 账号计费倍率(仅管理员可见)
|
||||
account_rate_multiplier?: number | null
|
||||
|
||||
// 用户请求 IP(仅管理员可见)
|
||||
ip_address?: string | null
|
||||
|
||||
// 最小账号信息(仅管理员接口返回)
|
||||
account?: UsageLogAccountSummary
|
||||
}
|
||||
|
||||
export interface UsageCleanupFilters {
|
||||
start_time: string
|
||||
end_time: string
|
||||
|
||||
@@ -187,14 +187,14 @@ import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
|
||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||
import type { Account, Proxy, Group } from '@/types'
|
||||
import type { Account, Proxy, AdminGroup } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const proxies = ref<Proxy[]>([])
|
||||
const groups = ref<Group[]>([])
|
||||
const groups = ref<AdminGroup[]>([])
|
||||
const selIds = ref<number[]>([])
|
||||
const showCreate = ref(false)
|
||||
const showEdit = ref(false)
|
||||
|
||||
@@ -1107,7 +1107,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useOnboardingStore } from '@/stores/onboarding'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Group, GroupPlatform, SubscriptionType } from '@/types'
|
||||
import type { AdminGroup, GroupPlatform, SubscriptionType } from '@/types'
|
||||
import type { Column } from '@/components/common/types'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
||||
@@ -1202,7 +1202,7 @@ const fallbackGroupOptionsForEdit = computed(() => {
|
||||
return options
|
||||
})
|
||||
|
||||
const groups = ref<Group[]>([])
|
||||
const groups = ref<AdminGroup[]>([])
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const filters = reactive({
|
||||
@@ -1223,8 +1223,8 @@ const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingGroup = ref<Group | null>(null)
|
||||
const deletingGroup = ref<Group | null>(null)
|
||||
const editingGroup = ref<AdminGroup | null>(null)
|
||||
const deletingGroup = ref<AdminGroup | null>(null)
|
||||
|
||||
const createForm = reactive({
|
||||
name: '',
|
||||
@@ -1529,7 +1529,7 @@ const handleCreateGroup = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = async (group: Group) => {
|
||||
const handleEdit = async (group: AdminGroup) => {
|
||||
editingGroup.value = group
|
||||
editForm.name = group.name
|
||||
editForm.description = group.description || ''
|
||||
@@ -1585,7 +1585,7 @@ const handleUpdateGroup = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (group: Group) => {
|
||||
const handleDelete = (group: AdminGroup) => {
|
||||
deletingGroup.value = group
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; impo
|
||||
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
|
||||
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
|
||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'; import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
||||
import type { UsageLog, TrendDataPoint, ModelStat } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
import type { AdminUsageLog, TrendDataPoint, ModelStat } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<UsageLog[]>([]); const loading = ref(false); const exporting = ref(false)
|
||||
const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<AdminUsageLog[]>([]); const loading = ref(false); const exporting = ref(false)
|
||||
const trendData = ref<TrendDataPoint[]>([]); const modelStats = ref<ModelStat[]>([]); const chartsLoading = ref(false); const granularity = ref<'day' | 'hour'>('day')
|
||||
let abortController: AbortController | null = null; let exportAbortController: AbortController | null = null
|
||||
const exportProgress = reactive({ show: false, progress: 0, current: 0, total: 0, estimatedTime: '' })
|
||||
@@ -92,7 +92,7 @@ const exportToExcel = async () => {
|
||||
if (exporting.value) return; exporting.value = true; exportProgress.show = true
|
||||
const c = new AbortController(); exportAbortController = c
|
||||
try {
|
||||
const all: UsageLog[] = []; let p = 1; let total = pagination.total
|
||||
const all: AdminUsageLog[] = []; let p = 1; let total = pagination.total
|
||||
while (true) {
|
||||
const res = await adminUsageAPI.list({ page: p, page_size: 100, ...filters.value }, { signal: c.signal })
|
||||
if (c.signal.aborted) break; if (p === 1) { total = res.total; exportProgress.total = total }
|
||||
|
||||
@@ -492,7 +492,7 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { User, UserAttributeDefinition } from '@/types'
|
||||
import type { AdminUser, UserAttributeDefinition } from '@/types'
|
||||
import type { BatchUserUsageStats } from '@/api/admin/dashboard'
|
||||
import type { Column } from '@/components/common/types'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
@@ -637,7 +637,7 @@ const columns = computed<Column[]>(() =>
|
||||
)
|
||||
)
|
||||
|
||||
const users = ref<User[]>([])
|
||||
const users = ref<AdminUser[]>([])
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
|
||||
@@ -736,16 +736,16 @@ const showEditModal = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const showApiKeysModal = ref(false)
|
||||
const showAttributesModal = ref(false)
|
||||
const editingUser = ref<User | null>(null)
|
||||
const deletingUser = ref<User | null>(null)
|
||||
const viewingUser = ref<User | null>(null)
|
||||
const editingUser = ref<AdminUser | null>(null)
|
||||
const deletingUser = ref<AdminUser | null>(null)
|
||||
const viewingUser = ref<AdminUser | null>(null)
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
// Action Menu State
|
||||
const activeMenuId = ref<number | null>(null)
|
||||
const menuPosition = ref<{ top: number; left: number } | null>(null)
|
||||
|
||||
const openActionMenu = (user: User, e: MouseEvent) => {
|
||||
const openActionMenu = (user: AdminUser, e: MouseEvent) => {
|
||||
if (activeMenuId.value === user.id) {
|
||||
closeActionMenu()
|
||||
} else {
|
||||
@@ -821,11 +821,11 @@ const handleClickOutside = (event: MouseEvent) => {
|
||||
|
||||
// Allowed groups modal state
|
||||
const showAllowedGroupsModal = ref(false)
|
||||
const allowedGroupsUser = ref<User | null>(null)
|
||||
const allowedGroupsUser = ref<AdminUser | null>(null)
|
||||
|
||||
// Balance (Deposit/Withdraw) modal state
|
||||
const showBalanceModal = ref(false)
|
||||
const balanceUser = ref<User | null>(null)
|
||||
const balanceUser = ref<AdminUser | null>(null)
|
||||
const balanceOperation = ref<'add' | 'subtract'>('add')
|
||||
|
||||
// 计算剩余天数
|
||||
@@ -998,7 +998,7 @@ const applyFilter = () => {
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
const handleEdit = (user: AdminUser) => {
|
||||
editingUser.value = user
|
||||
showEditModal.value = true
|
||||
}
|
||||
@@ -1008,7 +1008,7 @@ const closeEditModal = () => {
|
||||
editingUser.value = null
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (user: User) => {
|
||||
const handleToggleStatus = async (user: AdminUser) => {
|
||||
const newStatus = user.status === 'active' ? 'disabled' : 'active'
|
||||
try {
|
||||
await adminAPI.users.toggleStatus(user.id, newStatus)
|
||||
@@ -1022,7 +1022,7 @@ const handleToggleStatus = async (user: User) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewApiKeys = (user: User) => {
|
||||
const handleViewApiKeys = (user: AdminUser) => {
|
||||
viewingUser.value = user
|
||||
showApiKeysModal.value = true
|
||||
}
|
||||
@@ -1032,7 +1032,7 @@ const closeApiKeysModal = () => {
|
||||
viewingUser.value = null
|
||||
}
|
||||
|
||||
const handleAllowedGroups = (user: User) => {
|
||||
const handleAllowedGroups = (user: AdminUser) => {
|
||||
allowedGroupsUser.value = user
|
||||
showAllowedGroupsModal.value = true
|
||||
}
|
||||
@@ -1042,7 +1042,7 @@ const closeAllowedGroupsModal = () => {
|
||||
allowedGroupsUser.value = null
|
||||
}
|
||||
|
||||
const handleDelete = (user: User) => {
|
||||
const handleDelete = (user: AdminUser) => {
|
||||
deletingUser.value = user
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
@@ -1061,13 +1061,13 @@ const confirmDelete = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeposit = (user: User) => {
|
||||
const handleDeposit = (user: AdminUser) => {
|
||||
balanceUser.value = user
|
||||
balanceOperation.value = 'add'
|
||||
showBalanceModal.value = true
|
||||
}
|
||||
|
||||
const handleWithdraw = (user: User) => {
|
||||
const handleWithdraw = (user: AdminUser) => {
|
||||
balanceUser.value = user
|
||||
balanceOperation.value = 'subtract'
|
||||
showBalanceModal.value = true
|
||||
|
||||
Reference in New Issue
Block a user