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