From 941ba902f6efe83746d994f00304596f2c744279 Mon Sep 17 00:00:00 2001 From: qian Date: Thu, 28 May 2026 12:42:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E5=8F=91=E7=8E=B0=E7=9A=84?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/v1/channel.go | 2 +- api/admin/v1/usage_log_cleanup.go | 4 +- api/tenant/v1/project_tenant.go | 5 ++- internal/logic/admin/dashboard.go | 48 ++++++++++++----------- internal/logic/admin/error_log.go | 9 ++++- internal/logic/admin/notification.go | 9 ++++- internal/logic/admin/order.go | 11 +++--- internal/logic/admin/promo_code.go | 9 ++++- internal/logic/admin/redemption.go | 9 ++++- internal/logic/admin/tenant.go | 2 +- internal/logic/admin/usage_log_cleanup.go | 13 +++++- internal/logic/tenant/auth.go | 2 +- internal/logic/tenant/member.go | 20 ++++++++++ internal/logic/tenant/member_quota.go | 5 +++ internal/logic/tenant/project.go | 22 ++++++++--- 15 files changed, 124 insertions(+), 46 deletions(-) diff --git a/api/admin/v1/channel.go b/api/admin/v1/channel.go index d3860c0..790ef3d 100644 --- a/api/admin/v1/channel.go +++ b/api/admin/v1/channel.go @@ -49,7 +49,7 @@ type ChannelCreateReq struct { BaseURL string `json:"base_url" dc:"API 基础地址(留空使用供应商默认地址)"` ApiKey string `json:"api_key" v:"required#请输入 API Key" dc:"API Key"` Priority int `json:"priority" d:"0" dc:"优先级"` - Weight int `json:"weight" d:"100" v:"between:1,100" dc:"权重"` + Weight int `json:"weight" d:"100" v:"between:0,100" dc:"权重"` TestModel string `json:"test_model" dc:"测试模型名"` Remark string `json:"remark" dc:"备注"` IsVIP bool `json:"is_vip" d:"false" dc:"是否VIP专属渠道"` diff --git a/api/admin/v1/usage_log_cleanup.go b/api/admin/v1/usage_log_cleanup.go index 10bed8f..0936140 100644 --- a/api/admin/v1/usage_log_cleanup.go +++ b/api/admin/v1/usage_log_cleanup.go @@ -6,8 +6,8 @@ import "github.com/gogf/gf/v2/frame/g" type UsageLogCleanupCreateReq struct { g.Meta `path:"/usage-logs/cleanup" method:"post" mime:"json" tags:"管理后台-日志清理" summary:"创建清理任务"` - StartTime string `json:"start_time" v:"required|datetime" dc:"起始时间(RFC3339)"` - EndTime string `json:"end_time" v:"required|datetime" dc:"截止时间(RFC3339)"` + StartTime string `json:"start_time" v:"required" dc:"起始时间(RFC3339)"` + EndTime string `json:"end_time" v:"required" dc:"截止时间(RFC3339)"` TenantID *int64 `json:"tenant_id,omitempty" dc:"指定租户(可选)"` ModelName string `json:"model_name,omitempty" dc:"指定模型(可选)"` Status string `json:"status,omitempty" dc:"指定状态(可选)"` diff --git a/api/tenant/v1/project_tenant.go b/api/tenant/v1/project_tenant.go index ff0e63d..242dc19 100644 --- a/api/tenant/v1/project_tenant.go +++ b/api/tenant/v1/project_tenant.go @@ -90,7 +90,10 @@ type TenantProjectApiKeyCreateReq struct { } type TenantProjectApiKeyCreateRes struct { - Data map[string]any `json:"data"` + ID int64 `json:"id"` + Name string `json:"name"` + Key string `json:"key"` + KeyPrefix string `json:"key_prefix"` } type TenantProjectApiKeyDeleteReq struct { diff --git a/internal/logic/admin/dashboard.go b/internal/logic/admin/dashboard.go index b7c422b..32e3166 100644 --- a/internal/logic/admin/dashboard.go +++ b/internal/logic/admin/dashboard.go @@ -388,43 +388,45 @@ func (s *sAdmin) AdjustBalance(ctx context.Context, req *v1.AdminWalletAdjustReq amount := req.Amount description := req.Description - type walletRow struct { + // 原子更新余额,避免并发竞态 + db := g.DB() + updateQuery := "UPDATE bil_wallets SET balance = balance + ?, updated_at = ? WHERE tenant_id = ?" + args := []interface{}{amount, gtime.Now(), tenantID} + if amount < 0 { + updateQuery += " AND balance >= ?" + args = append(args, -amount) + } + result, err := db.Exec(ctx, updateQuery, args...) + if err != nil { + return nil, err + } + affected, _ := result.RowsAffected() + if affected == 0 { + return nil, gerror.New("钱包不存在或余额不足") + } + + // 查询更新后的余额,用于记录流水 + var wallet struct { ID int64 `json:"id"` Balance float64 `json:"balance"` FrozenBalance float64 `json:"frozen_balance"` } - var w *walletRow - err := dao.BilWallets.Ctx(ctx). + err = dao.BilWallets.Ctx(ctx). Where("tenant_id", tenantID). Fields("id, balance, frozen_balance"). - Scan(&w) - if err != nil || w == nil { - return nil, gerror.Newf("wallet not found for tenant %d", tenantID) - } - - // 检查扣减后余额是否为负 - if amount < 0 && w.Balance+amount < 0 { - return nil, gerror.Newf("余额不足,当前余额 %.2f,无法扣减 %.2f", w.Balance, -amount) - } - - _, err = dao.BilWallets.Ctx(ctx). - Where("id", w.ID). - Data(do.BilWallets{ - Balance: w.Balance + amount, - }).Update() + Scan(&wallet) if err != nil { return nil, err } - // 记录流水(含变动后余额) - balanceAfter := w.Balance + amount + // 记录流水 dao.BilTransactions.Ctx(ctx).Insert(do.BilTransactions{ TenantId: tenantID, - WalletId: w.ID, + WalletId: wallet.ID, Type: "adjust", Amount: amount, - BalanceAfter: balanceAfter, - FrozenAfter: w.FrozenBalance, + BalanceAfter: wallet.Balance, + FrozenAfter: wallet.FrozenBalance, Description: description, }) diff --git a/internal/logic/admin/error_log.go b/internal/logic/admin/error_log.go index 87c675e..3046ace 100644 --- a/internal/logic/admin/error_log.go +++ b/internal/logic/admin/error_log.go @@ -91,8 +91,15 @@ func (s *sAdmin) ErrorLogDetail(ctx context.Context, req *v1.ErrorLogDetailReq) // ErrorLogResolve marks an error log as resolved. func (s *sAdmin) ErrorLogResolve(ctx context.Context, req *v1.ErrorLogResolveReq) (*v1.ErrorLogResolveRes, error) { + count, err := dao.SysErrorLogs.Ctx(ctx).Where("id", req.Id).Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("错误日志") + } userID := common.GetCtxUserID(ctx) - _, err := dao.SysErrorLogs.Ctx(ctx). + _, err = dao.SysErrorLogs.Ctx(ctx). Where("id", req.Id). Data(g.Map{ "resolved": true, diff --git a/internal/logic/admin/notification.go b/internal/logic/admin/notification.go index bb56424..c332c9a 100644 --- a/internal/logic/admin/notification.go +++ b/internal/logic/admin/notification.go @@ -376,7 +376,14 @@ func (s *sAdmin) ListAnnouncements(ctx context.Context, req *v1.AnnouncementList // PublishAnnouncement 发布公告 func (s *sAdmin) PublishAnnouncement(ctx context.Context, req *v1.AnnouncementPublishReq) (*v1.AnnouncementPublishRes, error) { - _, err := dao.NtfAnnouncements.Ctx(ctx). + count, err := dao.NtfAnnouncements.Ctx(ctx).Where("id", req.Id).Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("公告") + } + _, err = dao.NtfAnnouncements.Ctx(ctx). Where("id", req.Id). Data(do.NtfAnnouncements{ Status: "published", diff --git a/internal/logic/admin/order.go b/internal/logic/admin/order.go index 71e9d25..85fe350 100644 --- a/internal/logic/admin/order.go +++ b/internal/logic/admin/order.go @@ -45,17 +45,16 @@ func (s *sAdmin) ListOrders(ctx context.Context, req *v1.OrderListReq) (*v1.Orde // GetOrder 获取订单详情 func (s *sAdmin) GetOrder(ctx context.Context, req *v1.OrderDetailReq) (*v1.OrderDetailRes, error) { - var order map[string]any - err := dao.OrdOrders.Ctx(ctx). + record, err := dao.OrdOrders.Ctx(ctx). Where("id", req.Id). - Scan(&order) - if err = common.IgnoreScanNoRows(err); err != nil { + One() + if err != nil { return nil, err } - if order == nil { + if record == nil { return nil, common.NewNotFoundError("订单") } - return &v1.OrderDetailRes{Data: order}, nil + return &v1.OrderDetailRes{Data: record.Map()}, nil } // RefundOrder 发起退款 diff --git a/internal/logic/admin/promo_code.go b/internal/logic/admin/promo_code.go index cef1a63..3c7831b 100644 --- a/internal/logic/admin/promo_code.go +++ b/internal/logic/admin/promo_code.go @@ -45,7 +45,14 @@ func (s *sAdmin) CreatePromoCode(ctx context.Context, req *v1.PromoCodeCreateReq // UpdatePromoCode 更新优惠码 func (s *sAdmin) UpdatePromoCode(ctx context.Context, req *v1.PromoCodeUpdateReq) (*v1.PromoCodeUpdateRes, error) { - _, err := dao.OrdPromoCodes.Ctx(ctx). + count, err := dao.OrdPromoCodes.Ctx(ctx).Where("id", req.Id).Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("优惠码") + } + _, err = dao.OrdPromoCodes.Ctx(ctx). Where("id", req.Id). Data(req.Update). Update() diff --git a/internal/logic/admin/redemption.go b/internal/logic/admin/redemption.go index 6f52fa6..912cd18 100644 --- a/internal/logic/admin/redemption.go +++ b/internal/logic/admin/redemption.go @@ -85,7 +85,14 @@ func (s *sAdmin) BatchCreateRedemptions(ctx context.Context, req *v1.RedemptionC // DisableRedemption 禁用兑换码 func (s *sAdmin) DisableRedemption(ctx context.Context, req *v1.RedemptionDisableReq) (*v1.RedemptionDisableRes, error) { - _, err := dao.OrdRedemptions.Ctx(ctx). + count, err := dao.OrdRedemptions.Ctx(ctx).Where("id", req.Id).Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("兑换码") + } + _, err = dao.OrdRedemptions.Ctx(ctx). Where("id", req.Id). Where("status", "active"). Data(do.OrdRedemptions{ diff --git a/internal/logic/admin/tenant.go b/internal/logic/admin/tenant.go index 906c9e4..75a365f 100644 --- a/internal/logic/admin/tenant.go +++ b/internal/logic/admin/tenant.go @@ -160,7 +160,7 @@ func (s *sAdmin) CreateTenant(ctx context.Context, req *v1.TenantCreateReq) (*v1 TenantId: tenantID, Balance: 0, FrozenBalance: 0, - Currency: "CNY", + Currency: "USD", }).Insert() if err != nil { return gerror.Wrapf(err, "create wallet") diff --git a/internal/logic/admin/usage_log_cleanup.go b/internal/logic/admin/usage_log_cleanup.go index a17a15e..431d5f5 100644 --- a/internal/logic/admin/usage_log_cleanup.go +++ b/internal/logic/admin/usage_log_cleanup.go @@ -10,6 +10,7 @@ import ( v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/logic/common" do "github.com/qianfree/team-api/internal/model/do" ) @@ -22,6 +23,9 @@ func (s *sAdmin) UsageLogCleanupCreate(ctx context.Context, req *v1.UsageLogClea if err != nil { return nil, err } + if startTime.After(endTime) { + return nil, common.NewBadRequestError("起始时间不能晚于截止时间") + } batchSize := req.BatchSize if batchSize <= 0 { @@ -125,7 +129,14 @@ func (s *sAdmin) UsageLogCleanupList(ctx context.Context, req *v1.UsageLogCleanu } func (s *sAdmin) UsageLogCleanupCancel(ctx context.Context, req *v1.UsageLogCleanupCancelReq) (*v1.UsageLogCleanupCancelRes, error) { - _, err := dao.TskTasks.Ctx(ctx). + count, err := dao.TskTasks.Ctx(ctx).Where("id", req.ID).Where("handler", "usage_log_cleanup").Count() + if err != nil { + return nil, err + } + if count == 0 { + return nil, common.NewNotFoundError("清理任务") + } + _, err = dao.TskTasks.Ctx(ctx). Where("id", req.ID). Where("handler", "usage_log_cleanup"). Where("status", "pending"). diff --git a/internal/logic/tenant/auth.go b/internal/logic/tenant/auth.go index 54a9169..236bc92 100644 --- a/internal/logic/tenant/auth.go +++ b/internal/logic/tenant/auth.go @@ -129,7 +129,7 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1. TenantId: tenantID, Balance: 0, FrozenBalance: 0, - Currency: "CNY", + Currency: "USD", }).Insert() if err != nil { return gerror.Wrapf(err, "create wallet") diff --git a/internal/logic/tenant/member.go b/internal/logic/tenant/member.go index 4c5566b..6589cf5 100644 --- a/internal/logic/tenant/member.go +++ b/internal/logic/tenant/member.go @@ -84,6 +84,10 @@ func (s *sTenant) ListMembers(ctx context.Context, req *v1.TenantMemberListReq) // InviteMember generates an invitation link. func (s *sTenant) InviteMember(ctx context.Context, req *v1.TenantMemberInviteReq) (*v1.TenantMemberInviteRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) creatorID := middleware.GetUserID(ctx) @@ -359,6 +363,10 @@ func (s *sTenant) JoinByInvite(ctx context.Context, req *v1.TenantMemberJoinReq) // CreateMember directly creates a member account within the tenant. func (s *sTenant) CreateMember(ctx context.Context, req *v1.TenantMemberCreateReq) (*v1.TenantMemberCreateRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) // Validate role @@ -465,6 +473,10 @@ func (s *sTenant) CreateMember(ctx context.Context, req *v1.TenantMemberCreateRe // RemoveMember removes a member from the tenant. // Revokes all API keys, anonymizes personal data, releases member model scopes. func (s *sTenant) RemoveMember(ctx context.Context, req *v1.TenantMemberRemoveReq) (*v1.TenantMemberRemoveRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) currentUserID := middleware.GetUserID(ctx) memberID := req.Id @@ -597,6 +609,10 @@ func (s *sTenant) EnableMember(ctx context.Context, tenantID, userID int64) erro // UpdateMemberRole updates a member's role. func (s *sTenant) UpdateMemberRole(ctx context.Context, req *v1.TenantMemberUpdateRoleReq) (*v1.TenantMemberUpdateRoleRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) currentUserID := middleware.GetUserID(ctx) memberID := req.Id @@ -844,6 +860,10 @@ func (s *sTenant) ListMemberApiKeys(ctx context.Context, req *v1.TenantMemberApi // ExportMembers exports the tenant member list as CSV or Excel. func (s *sTenant) ExportMembers(ctx context.Context, req *v1.TenantMemberExportReq) (*v1.TenantMemberExportRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) columns := []export.Column{ diff --git a/internal/logic/tenant/member_quota.go b/internal/logic/tenant/member_quota.go index dbd2441..ec07f7f 100644 --- a/internal/logic/tenant/member_quota.go +++ b/internal/logic/tenant/member_quota.go @@ -10,6 +10,7 @@ import ( v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/dao" + "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/middleware" ) @@ -50,6 +51,10 @@ func (s *sTenant) MemberQuota(ctx context.Context, req *v1.TenantMemberQuotaReq) } func (s *sTenant) MemberQuotaSet(ctx context.Context, req *v1.TenantMemberQuotaSetReq) (*v1.TenantMemberQuotaSetRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) if req.QuotaType == "periodic" && req.Period == "" { diff --git a/internal/logic/tenant/project.go b/internal/logic/tenant/project.go index 6917e54..c10e206 100644 --- a/internal/logic/tenant/project.go +++ b/internal/logic/tenant/project.go @@ -79,6 +79,10 @@ func (s *sTenant) ProjectList(ctx context.Context, req *v1.TenantProjectListReq) // ProjectCreate creates a new project for a tenant. func (s *sTenant) ProjectCreate(ctx context.Context, req *v1.TenantProjectCreateReq) (*v1.TenantProjectCreateRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) userID := middleware.GetUserID(ctx) @@ -113,6 +117,10 @@ func (s *sTenant) ProjectCreate(ctx context.Context, req *v1.TenantProjectCreate // ProjectUpdate updates a project. func (s *sTenant) ProjectUpdate(ctx context.Context, req *v1.TenantProjectUpdateReq) (*v1.TenantProjectUpdateRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) var project *struct { @@ -162,6 +170,10 @@ func (s *sTenant) ProjectUpdate(ctx context.Context, req *v1.TenantProjectUpdate // ProjectArchive archives a project and revokes all its keys. func (s *sTenant) ProjectArchive(ctx context.Context, req *v1.TenantProjectArchiveReq) (*v1.TenantProjectArchiveRes, error) { + role := middleware.GetUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, common.NewForbiddenError("需要 owner 或 admin 权限") + } tenantID := middleware.GetTenantID(ctx) var project *struct { @@ -447,12 +459,10 @@ func (s *sTenant) ProjectApiKeyCreate(ctx context.Context, req *v1.TenantProject } return &v1.TenantProjectApiKeyCreateRes{ - Data: map[string]any{ - "id": id, - "name": req.Name, - "key": rawKey, - "key_prefix": prefix, - }, + ID: id, + Name: req.Name, + Key: rawKey, + KeyPrefix: prefix, }, nil }