121 lines
4.6 KiB
Go
121 lines
4.6 KiB
Go
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))
|
||
})
|
||
}
|