Files
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

62 lines
1.9 KiB
Go

package public
import (
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/qianfree/team-api/internal/logic/payment"
)
// HandlePaymentCallback 处理支付渠道的异步回调通知(支持 GET/POST)。
// channel 从 URL 路径获取(如 "epay")。
func HandlePaymentCallback(r *ghttp.Request) {
channel := r.Get("channel").String()
if channel == "" {
r.Response.WriteStatus(400)
r.Response.Write("fail")
return
}
err := payment.ProcessCallback(r.Context(), r.Request, channel)
if err != nil {
// 回调处理失败(验签失败 / 金额不符 / 订单不存在等)必须落日志:
// 否则签名伪造/篡改尝试在服务端完全不可追溯。返回 200 fail 供渠道停止重推。
g.Log().Warningf(r.Context(), "[Payment] callback failed: channel=%s clientIP=%s err=%v",
channel, r.GetClientIp(), err)
r.Response.WriteStatus(200)
r.Response.Write("fail")
return
}
r.Response.WriteStatus(200)
r.Response.Write("success")
}
// HandlePaymentEpayReturn 处理 Epay 支付完成后的浏览器同步跳转。
// 仅查询订单状态决定跳转目标,履约由异步回调完成。
func HandlePaymentEpayReturn(r *ghttp.Request) {
orderNo := r.GetQuery("out_trade_no").String()
if orderNo == "" {
r.Response.RedirectTo("/console/wallet?pay=fail")
return
}
ctx := r.Context()
// 异步 notify 可能晚于浏览器回跳到达:最多轮询约 4 秒等待回调入账。
// 仍未到账不一定是失败(回调可能仍在途),引导到"处理中"页由前端继续刷新,
// 避免给用户展示误导性的"失败"。
paid := payment.QueryOrderPaid(ctx, orderNo)
for i := 0; !paid && i < 8; i++ {
time.Sleep(500 * time.Millisecond)
paid = payment.QueryOrderPaid(ctx, orderNo)
}
if paid {
r.Response.RedirectTo("/console/wallet?pay=success")
return
}
r.Response.RedirectTo("/console/wallet?pay=processing")
}