完善发货链接签名和作废恢复

This commit is contained in:
yml2213
2026-07-31 11:43:13 +08:00
parent 6ca226ce4b
commit e4c1306216
17 changed files with 527 additions and 109 deletions
+203 -20
View File
@@ -2,14 +2,20 @@ package service
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"affiliate_dash/internal/model"
)
const defaultDeliveryBFFBaseURL = "https://www.jxya.top/bff-stg"
@@ -18,10 +24,13 @@ type DeliveryService struct {
fulfillment *FulfillmentService
bffBaseURL string
channel string
linkBaseURL string
linkSecret string
linkTTL time.Duration
httpClient *http.Client
}
func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel string) *DeliveryService {
func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel, linkBaseURL, linkSecret string, linkTTLMinutes int) *DeliveryService {
bffBaseURL = strings.TrimRight(strings.TrimSpace(bffBaseURL), "/")
if bffBaseURL == "" {
bffBaseURL = defaultDeliveryBFFBaseURL
@@ -30,10 +39,21 @@ func NewDeliveryService(fulfillment *FulfillmentService, bffBaseURL, channel str
if channel == "" {
channel = "dlc"
}
linkBaseURL = strings.TrimRight(strings.TrimSpace(linkBaseURL), "/")
linkSecret = strings.TrimSpace(linkSecret)
if linkSecret == "" {
linkSecret = "affiliate-dash-delivery-link-secret"
}
if linkTTLMinutes <= 0 {
linkTTLMinutes = 120
}
return &DeliveryService{
fulfillment: fulfillment,
bffBaseURL: bffBaseURL,
channel: channel,
linkBaseURL: linkBaseURL,
linkSecret: linkSecret,
linkTTL: time.Duration(linkTTLMinutes) * time.Minute,
httpClient: &http.Client{Timeout: 15 * time.Second},
}
}
@@ -75,6 +95,14 @@ type DeliveryBindResult struct {
QRURL string `json:"qr_url"`
}
type DeliveryLinkResult struct {
OrderNo string `json:"order_no"`
DeliveryURL string `json:"delivery_url"`
ExpiresAt time.Time `json:"expires_at"`
Exp int64 `json:"exp"`
Sign string `json:"sign"`
}
type DeliverySubmitResult struct {
OrderNo string `json:"order_no"`
Status string `json:"status"`
@@ -84,17 +112,39 @@ type DeliverySubmitResult struct {
UpstreamOrder interface{} `json:"upstream_order,omitempty"`
}
func (s *DeliveryService) GetOrder(orderNo string) (*DeliveryOrderInfo, error) {
info, _, _, err := s.prepareOrder(orderNo, false)
type DeliveryLinkAuth struct {
Exp int64
Sign string
}
type deliveryHTTPError struct {
status int
message string
}
func (e deliveryHTTPError) Error() string {
return e.message
}
func (e deliveryHTTPError) HTTPStatus() int {
return e.status
}
func newDeliveryHTTPError(status int, message string) error {
return deliveryHTTPError{status: status, message: message}
}
func (s *DeliveryService) GetOrder(orderNo string, auth DeliveryLinkAuth) (*DeliveryOrderInfo, error) {
info, _, _, err := s.prepareOrder(orderNo, false, &auth)
return info, err
}
func (s *DeliveryService) Bind(orderNo, gameAccount string) (*DeliveryBindResult, error) {
func (s *DeliveryService) Bind(orderNo, gameAccount string, auth DeliveryLinkAuth) (*DeliveryBindResult, error) {
gameAccount = strings.TrimSpace(gameAccount)
if gameAccount == "" {
return nil, errors.New("请输入玩家编号")
}
_, _, goodID, err := s.prepareOrder(orderNo, true)
_, _, goodID, err := s.prepareOrder(orderNo, true, &auth)
if err != nil {
return nil, err
}
@@ -120,14 +170,14 @@ func (s *DeliveryService) Bind(orderNo, gameAccount string) (*DeliveryBindResult
}, nil
}
func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string) (*DeliverySubmitResult, error) {
func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string, auth DeliveryLinkAuth) (*DeliverySubmitResult, error) {
gameAccount = strings.TrimSpace(gameAccount)
bindUUID = strings.TrimSpace(bindUUID)
orderNo = strings.TrimSpace(orderNo)
if gameAccount == "" || bindUUID == "" {
return nil, errors.New("玩家编号和绑定凭证不能为空")
}
_, order, goodID, err := s.prepareOrder(orderNo, true)
_, order, goodID, err := s.prepareOrder(orderNo, true, &auth)
if err != nil {
return nil, err
}
@@ -186,7 +236,90 @@ func (s *DeliveryService) Submit(orderNo, gameAccount, bindUUID string) (*Delive
}, nil
}
func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool) (*DeliveryOrderInfo, *FulfillmentOrderSnapshot, string, error) {
func (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo string) (*DeliveryLinkResult, error) {
orderNo = strings.TrimSpace(orderNo)
if orderNo == "" {
return nil, errors.New("订单号不能为空")
}
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
if err != nil {
return nil, err
}
now := time.Now().UTC()
if order.DeliveryLinkRevokedAt != nil {
return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
}
if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) {
return s.buildDeliveryLinkResult(order.OrderNo, order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)), nil
}
canShip, reason := CanFulfill(order)
if !canShip {
if reason == "" {
reason = "订单暂不可发货"
}
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
}
expiresAt := now.Add(s.linkTTL).UTC().Truncate(time.Second)
if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}).
Where("id = ?", order.ID).
Updates(map[string]interface{}{
"delivery_link_expires_at": expiresAt,
"delivery_link_revoked_at": nil,
}).Error; err != nil {
return nil, err
}
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt), nil
}
func (s *DeliveryService) RevokeDeliveryLink(merchantID uint, orderNo string) error {
orderNo = strings.TrimSpace(orderNo)
if orderNo == "" {
return errors.New("订单号不能为空")
}
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
if err != nil {
return err
}
now := time.Now().UTC().Truncate(time.Second)
return s.fulfillment.db.Model(&model.FulfillmentOrder{}).
Where("id = ?", order.ID).
Updates(map[string]interface{}{
"delivery_link_revoked_at": now,
}).Error
}
func (s *DeliveryService) RestoreDeliveryLink(merchantID uint, orderNo string) (*DeliveryLinkResult, error) {
orderNo = strings.TrimSpace(orderNo)
if orderNo == "" {
return nil, errors.New("订单号不能为空")
}
order, err := s.fulfillment.GetOrder(merchantID, orderNo)
if err != nil {
return nil, err
}
canShip, reason := CanFulfill(order)
if !canShip {
if reason == "" {
reason = "订单暂不可发货"
}
return nil, newDeliveryHTTPError(http.StatusBadRequest, reason)
}
expiresAt := time.Now().UTC().Add(s.linkTTL).Truncate(time.Second)
if order.DeliveryLinkExpiresAt != nil && expiresAt.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) {
expiresAt = expiresAt.Add(time.Second)
}
if err := s.fulfillment.db.Model(&model.FulfillmentOrder{}).
Where("id = ?", order.ID).
Updates(map[string]interface{}{
"delivery_link_expires_at": expiresAt,
"delivery_link_revoked_at": nil,
}).Error; err != nil {
return nil, err
}
return s.buildDeliveryLinkResult(order.OrderNo, expiresAt), nil
}
func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool, auth *DeliveryLinkAuth) (*DeliveryOrderInfo, *model.FulfillmentOrder, string, error) {
orderNo = strings.TrimSpace(orderNo)
if orderNo == "" {
return nil, nil, "", errors.New("订单号不能为空")
@@ -199,6 +332,11 @@ func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool) (*De
if err != nil {
return nil, nil, "", err
}
if auth != nil {
if err := s.authorizeDeliveryLink(order, *auth); err != nil {
return nil, nil, "", err
}
}
canShip, reason := CanFulfill(order)
if !openOrder.CanShip {
reason = openOrder.CannotShipReason
@@ -219,7 +357,7 @@ func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool) (*De
}
}
if requireCanShip && !canShip {
return nil, nil, "", errors.New(reason)
return nil, nil, "", newDeliveryHTTPError(http.StatusBadRequest, reason)
}
var good map[string]interface{}
if goodID != "" {
@@ -243,11 +381,62 @@ func (s *DeliveryService) prepareOrder(orderNo string, requireCanShip bool) (*De
PayScore: openOrder.PayScore,
Good: good,
}
return info, &FulfillmentOrderSnapshot{
ID: order.ID,
MerchantID: order.MerchantID,
OrderNo: order.OrderNo,
}, goodID, nil
return info, order, goodID, nil
}
func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, auth DeliveryLinkAuth) error {
if order == nil {
return newDeliveryHTTPError(http.StatusNotFound, "订单不存在")
}
if auth.Exp <= 0 || strings.TrimSpace(auth.Sign) == "" {
return newDeliveryHTTPError(http.StatusForbidden, "链接参数缺失")
}
if order.DeliveryLinkRevokedAt != nil {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废")
}
if order.DeliveryLinkExpiresAt == nil {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成")
}
expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)
linkExpires := time.Unix(auth.Exp, 0).UTC()
if !linkExpires.Equal(expiresAt) {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已失效")
}
if time.Now().UTC().After(linkExpires) {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接已过期")
}
expected := s.signDeliveryLink(order.OrderNo, auth.Exp)
if !hmac.Equal([]byte(strings.ToLower(strings.TrimSpace(auth.Sign))), []byte(expected)) {
return newDeliveryHTTPError(http.StatusForbidden, "发货链接签名无效")
}
return nil
}
func (s *DeliveryService) buildDeliveryLinkResult(orderNo string, expiresAt time.Time) *DeliveryLinkResult {
expiresAt = expiresAt.UTC()
exp := expiresAt.Unix()
sign := s.signDeliveryLink(orderNo, exp)
return &DeliveryLinkResult{
OrderNo: orderNo,
DeliveryURL: s.buildDeliveryURL(orderNo, exp, sign),
ExpiresAt: expiresAt,
Exp: exp,
Sign: sign,
}
}
func (s *DeliveryService) buildDeliveryURL(orderNo string, exp int64, sign string) string {
path := fmt.Sprintf("/delivery/%s/%s?exp=%d&sign=%s", url.PathEscape(s.channel), url.PathEscape(orderNo), exp, url.QueryEscape(sign))
if s.linkBaseURL == "" {
return path
}
return s.linkBaseURL + path
}
func (s *DeliveryService) signDeliveryLink(orderNo string, exp int64) string {
mac := hmac.New(sha256.New, []byte(s.linkSecret))
_, _ = mac.Write([]byte(orderNo + "|" + strconv.FormatInt(exp, 10)))
return hex.EncodeToString(mac.Sum(nil))
}
func buildDeliveryProduct(openProduct *OpenOrderProduct, good map[string]interface{}) *DeliveryProduct {
@@ -266,12 +455,6 @@ func buildDeliveryProduct(openProduct *OpenOrderProduct, good map[string]interfa
return product
}
type FulfillmentOrderSnapshot struct {
ID uint
MerchantID uint
OrderNo string
}
func (s *DeliveryService) goodsDetail(goodID string) (map[string]interface{}, error) {
var out struct {
Good map[string]interface{} `json:"good"`