@@ -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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user