Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
package activityclient
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"91porn-server/app/appg"
"91porn-server/common"
"91porn-server/common/httputil"
"91porn-server/common/log"
)
// 抵扣券状态回调(与 activity-public-server /api/app/deduct/coupon/notify 约定一致)。
// 券状态流转由本服双回调驱动:建单成功->used、支付成功->verified。
const (
DeductCouponStatusUsed = "used" // 建单成功: 券 active -> used(下单占用/已使用),需携 deductAmount
DeductCouponStatusVerified = "verified" // 支付成功: 券 used -> verified(已核销)usedCount++
DeductCouponStatusFailed = "failed" // 支付失败: 券 used -> active(释放)
DeductCouponStatusClosed = "closed" // 订单关闭/超时: 券 used -> active(释放)
)
const deductCouponNotifyPath = "/api/app/deduct/coupon/notify"
// deductCouponNotifyReq 支付结果回调请求体
type deductCouponNotifyReq struct {
CouponID string `json:"couponId"` // 下单时收到的券号,原样回传
OrderID string `json:"orderId,omitempty"` // 本服订单号,活动服存档对账用
Status string `json:"status"` // paid/failed/closed
DeductAmount int64 `json:"deductAmount,omitempty"` // 实际抵扣金额(分),对账用
}
// NotifyDeductCoupon 向活动服回调抵扣券支付结果。
// 鉴权走方向B(X-Svc-*),密钥用 secretKey 原始字节(BuildSvcHeaders 已按此实现)。
// 单次调用;活动服对 couponId 幂等,重复回调安全。
// 返回 nil 表示活动服已受理(HTTP 200 且 body.code==200)。
func NotifyDeductCoupon(ctx context.Context, couponID, orderID, status string, deductAmount int64) error {
conf := appg.Conf.ActivityServer
if conf.ApiUrl == "" || conf.AppId == "" {
return fmt.Errorf("活动服配置缺失(apiUrl/appId)")
}
if couponID == "" {
return fmt.Errorf("couponId 为空")
}
body, err := json.Marshal(deductCouponNotifyReq{
CouponID: couponID,
OrderID: orderID,
Status: status,
DeductAmount: deductAmount,
})
if err != nil {
return fmt.Errorf("marshal notify body failed: %w", err)
}
// 签名针对下面实际发送的 body 字节;httputil 对 []byte 原样透传,两者一致
headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "POST", deductCouponNotifyPath, "", body)
if headers == nil {
return fmt.Errorf("生成服务间签名头失败")
}
headers["Content-Type"] = "application/json"
resp, err := httputil.DefaultClientPostWithCtx(ctx, conf.ApiUrl+deductCouponNotifyPath, headers, body)
if err != nil {
return fmt.Errorf("请求活动服失败: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("活动服回调HTTP状态异常: %d, body: %s", resp.StatusCode, string(respBody))
}
var out struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
if err := json.Unmarshal(respBody, &out); err != nil {
return fmt.Errorf("解析活动服回调响应失败: %w, body: %s", err, string(respBody))
}
if out.Code != 200 {
return fmt.Errorf("活动服回调返回非成功: code=%d, msg=%s", out.Code, out.Msg)
}
return nil
}
// NotifyDeductCouponAsync 异步回调抵扣券状态,失败按 1s/3s/5s/10s 退避重试。
// 独立 goroutine 执行,不阻塞下单/支付主流程;最终仍失败仅记录错误——
// 活动服会在 used 券超 30 分钟未核销时由定时任务兜底释放回 active,不会丢一致性。
func NotifyDeductCouponAsync(couponID, orderID, status string, deductAmount int64) {
if couponID == "" {
return
}
common.Go(func() {
backoffs := []time.Duration{0, time.Second, 3 * time.Second, 5 * time.Second, 10 * time.Second}
var lastErr error
for i, d := range backoffs {
if d > 0 {
time.Sleep(d)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
lastErr = NotifyDeductCoupon(ctx, couponID, orderID, status, deductAmount)
cancel()
if lastErr == nil {
if i > 0 {
log.Info("活动服-抵扣券回调重试成功",
log.Any("couponId", couponID), log.Any("status", status), log.Any("attempt", i+1))
}
return
}
log.Warn("活动服-抵扣券回调失败,待重试",
log.Any("couponId", couponID), log.Any("orderId", orderID),
log.Any("status", status), log.Any("attempt", i+1), log.E(lastErr))
}
log.Error("活动服-抵扣券回调最终失败(依赖活动服到期兜底释放)",
log.Any("couponId", couponID), log.Any("orderId", orderID),
log.Any("status", status), log.Any("deductAmount", deductAmount), log.E(lastErr))
})
}