Files
huangguo_server/app/service/activityclient/deduct.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

121 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
})
}