Files
team-api/internal/logic/admin/promo_code.go
T
chengqian 8a68f6a501 fix: 修复代码分析报告中的中等优先级问题
修复 docs/claude代码分析.md 中等优先级的 8 项确认问题:

- tenant/project.go:ProjectUpdate/Archive/Unarchive 的 UPDATE 补上
  tenant_id 过滤,恢复写操作租户隔离约定
- taskchannel(ali/sora/suno/volcengine/midjourney):裸 http.Client 改为
  common.NewPooledClient(走连接池 + 尊重渠道 UseProxy),不再丢弃
  BuildRequestHeader 的错误
- handler/public/payment.go:支付回调失败补日志,伪造/篡改尝试可追溯
- billing/wallet.go + payment/fulfill.go:新增导出 InvalidateWallet
  同时清进程内 walletCache 与 Redis,消除充值后读到旧余额
- logic/task/task.go:ExecuteTask 领取任务改为原子 CAS,多副本不再重复执行
- middleware/rbac.go:matchPermission 后缀匹配改为按路径分段边界,
  修复 /test 规则误命中 .../latest
- admin/channel.go + channel_oauth.go:渠道→密钥跨表写入包进事务,
  杜绝孤儿渠道
- admin/promo_code.go:CreatePromoCode 增加字段白名单,对齐 UpdatePromoCode

另有 6 项经评估暂不修改(配额非原子/补扣负数/无 jti token/setup 竞态/
crypto AAD/turnstile 校验),原因见分析报告说明。
2026-07-22 18:43:38 +08:00

224 lines
6.1 KiB
Go

package admin
import (
"context"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
do "github.com/qianfree/team-api/internal/model/do"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/utility/export"
)
// ListPromoCodes 获取优惠码列表
func (s *sAdmin) ListPromoCodes(ctx context.Context, req *v1.PromoCodeListReq) (*v1.PromoCodeListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
var total int
items := make([]*v1.PromoCodeItem, 0)
err := dao.OrdPromoCodes.Ctx(ctx).
OrderDesc("created_at").
Page(page, pageSize).
ScanAndCount(&items, &total, false)
if err != nil {
return nil, err
}
return &v1.PromoCodeListRes{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// CreatePromoCode 创建优惠码
func (s *sAdmin) CreatePromoCode(ctx context.Context, req *v1.PromoCodeCreateReq) (*v1.PromoCodeCreateRes, error) {
// 字段白名单:req.Data 是原始 map,直接 Insert 会把 used_count / id / created_at 等
// 非预期字段一并写入。此处仅放行显式列出的列(与 UpdatePromoCode 的白名单口径一致),
// 杜绝请求体越权注入内部字段。
data := do.OrdPromoCodes{}
for k, v := range req.Data {
switch k {
case "code":
data.Code = v
case "name":
data.Name = v
case "type":
data.Type = v
case "discount_value":
data.DiscountValue = v
case "min_amount":
data.MinAmount = v
case "max_discount":
data.MaxDiscount = v
case "total_count":
data.TotalCount = v
case "per_user_limit":
data.PerUserLimit = v
case "status":
data.Status = v
case "valid_from":
data.ValidFrom = gconv.GTime(v)
case "valid_to":
data.ValidTo = gconv.GTime(v)
case "plan_ids":
data.PlanIds = gconv.Int64s(v)
}
}
if data.Code == nil || gconv.String(data.Code) == "" {
return nil, common.NewBadRequestError("优惠码 code 不能为空")
}
result, err := dao.OrdPromoCodes.Ctx(ctx).Insert(data)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &v1.PromoCodeCreateRes{ID: id}, nil
}
// UpdatePromoCode 更新优惠码
func (s *sAdmin) UpdatePromoCode(ctx context.Context, req *v1.PromoCodeUpdateReq) (*v1.PromoCodeUpdateRes, error) {
count, err := dao.OrdPromoCodes.Ctx(ctx).Where("id", req.Id).Count()
if err != nil {
return nil, err
}
if count == 0 {
return nil, common.NewNotFoundError("优惠码")
}
data := do.OrdPromoCodes{}
allowedFields := map[string]bool{
"name": true, "type": true, "discount_value": true,
"min_amount": true, "total_count": true,
"max_discount": true, "status": true,
"starts_at": true, "expires_at": true,
}
for k, v := range req.Update {
if !allowedFields[k] {
continue
}
switch k {
case "name":
data.Name = v.(string)
case "type":
data.Type = v.(string)
case "discount_value":
data.DiscountValue = v
case "min_amount":
data.MinAmount = v
case "total_count":
data.TotalCount = v
case "max_discount":
data.MaxDiscount = v
case "status":
data.Status = v.(string)
}
}
_, err = dao.OrdPromoCodes.Ctx(ctx).
Where("id", req.Id).
Data(data).
Update()
if err != nil {
return nil, err
}
return &v1.PromoCodeUpdateRes{}, nil
}
// GetPromoCodeUsages 获取优惠码使用记录
func (s *sAdmin) GetPromoCodeUsages(ctx context.Context, req *v1.PromoCodeUsagesReq) (*v1.PromoCodeUsagesRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
var total int
items := make([]*v1.PromoCodeUsageItem, 0)
err := dao.OrdPromoCodeUsages.Ctx(ctx).
Where("promo_code_id", req.Id).
OrderDesc("created_at").
Page(page, pageSize).
ScanAndCount(&items, &total, false)
if err != nil {
return nil, err
}
return &v1.PromoCodeUsagesRes{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// ExportPromoCodes exports promo code list to CSV or Excel.
func (s *sAdmin) ExportPromoCodes(ctx context.Context, req *v1.PromoCodeExportReq) (*v1.PromoCodeExportRes, error) {
columns := []export.Column{
{Field: "id", Header: "ID"},
{Field: "code", Header: "优惠码"},
{Field: "name", Header: "名称"},
{Field: "type", Header: "类型"},
{Field: "discount_value", Header: "折扣值"},
{Field: "min_amount", Header: "最低金额"},
{Field: "total_count", Header: "总数"},
{Field: "used_count", Header: "已用数"},
{Field: "status", Header: "状态"},
{Field: "created_at", Header: "创建时间"},
}
config := export.Config{
Format: req.Format,
Filename: "优惠码_" + gtime.Now().Format("Ymd_His"),
Columns: columns,
}
promoFields := "id, code, name, type, discount_value, min_amount, total_count, used_count, status, created_at"
return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) {
offset := 0
for {
var batch []struct {
Id int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Type string `json:"type"`
DiscountValue float64 `json:"discount_value"`
MinAmount float64 `json:"min_amount"`
TotalCount int `json:"total_count"`
UsedCount int `json:"used_count"`
Status string `json:"status"`
CreatedAt *gtime.Time `json:"created_at"`
}
if err := dao.OrdPromoCodes.Ctx(ctx).Fields(promoFields).OrderDesc("created_at").Limit(1000).Offset(offset).Scan(&batch); err != nil {
return
}
for _, item := range batch {
if !yield(map[string]any{
"id": item.Id,
"code": item.Code,
"name": item.Name,
"type": item.Type,
"discount_value": item.DiscountValue,
"min_amount": item.MinAmount,
"total_count": item.TotalCount,
"used_count": item.UsedCount,
"status": item.Status,
"created_at": item.CreatedAt.String(),
}) {
return
}
}
if len(batch) < 1000 {
break
}
offset += 1000
}
})
}