package service import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "fmt" "net/http" "net/url" "strconv" "strings" "time" "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/timeutil" "gorm.io/gorm" ) func (s *DeliveryService) authorizeDeliveryLink(order *model.FulfillmentOrder, auth DeliveryLinkAuth) error { if order == nil { return newDeliveryHTTPError(http.StatusNotFound, "订单不存在") } if strings.TrimSpace(auth.Sign) == "" { return newDeliveryHTTPError(http.StatusForbidden, "链接参数缺失") } if order.DeliveryLinkRevokedAt != nil { return newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废") } // 订单已取消后链接立即失效,防止已退款订单仍通过链接查看或继续发货。 if normalizeOrderStatus(order) == model.OrderStatusCancelled { return newDeliveryHTTPError(http.StatusForbidden, "订单已取消,发货链接不可用") } // 从未生成过链接(兼容历史数据:本次改动前生成的有时效链接没有 generated_at 记录)。 if order.DeliveryLinkGeneratedAt == nil && order.DeliveryLinkExpiresAt == nil { return newDeliveryHTTPError(http.StatusForbidden, "发货链接未生成") } // 永久有效链接(expires_at 为 NULL):仅校验签名(exp 固定为 0),不检查过期。 if order.DeliveryLinkExpiresAt == nil { 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 } 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 } // buildDeliveryLinkResult 组装链接结果;expiresAt 为 nil 表示永久有效(exp 固定为 0)。 func (s *DeliveryService) buildDeliveryLinkResult(orderNo string, expiresAt *time.Time, requestBaseURL string) *DeliveryLinkResult { exp := int64(0) if expiresAt != nil { exp = expiresAt.UTC().Unix() } sign := s.signDeliveryLink(orderNo, exp) return &DeliveryLinkResult{ OrderNo: orderNo, DeliveryURL: s.buildDeliveryURL(orderNo, exp, sign, requestBaseURL), ExpiresAt: expiresAt, Exp: exp, Sign: sign, } } func (s *DeliveryService) buildDeliveryURL(orderNo string, exp int64, sign, requestBaseURL string) string { path := fmt.Sprintf("/delivery/%s/%s?exp=%d&sign=%s", url.PathEscape(s.channel), url.PathEscape(orderNo), exp, url.QueryEscape(sign)) base := strings.TrimRight(s.linkBaseURL, "/") if base == "" { base = strings.TrimRight(requestBaseURL, "/") } if base == "" { return path } return base + 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 (s *DeliveryService) GetOrCreateDeliveryLink(merchantID uint, orderNo, requestBaseURL 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 } // 已取消订单即使存在未过期链接也不再发放(买家侧授权同样拦截已取消订单)。 if normalizeOrderStatus(order) == model.OrderStatusCancelled { return nil, newDeliveryHTTPError(http.StatusBadRequest, "订单已取消,无法获取发货链接") } now := time.Now().UTC() if order.DeliveryLinkRevokedAt != nil { return nil, newDeliveryHTTPError(http.StatusForbidden, "发货链接已作废") } // 复用未过期链接:已交付/发货中订单也允许查看兑换结果或进度,仅停止发放新链接。 if order.DeliveryLinkExpiresAt != nil && order.DeliveryLinkExpiresAt.After(now) { expiresAt := order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second) return s.buildDeliveryLinkResult(order.OrderNo, &expiresAt, requestBaseURL), nil } // 永久有效链接(已生成、未作废、无有效期)直接复用,不重复生成。 if order.DeliveryLinkGeneratedAt != nil && order.DeliveryLinkExpiresAt == nil { return s.buildDeliveryLinkResult(order.OrderNo, nil, requestBaseURL), nil } // 仅待发货(含可重试的发货失败)订单能生成或续期新链接。 canShip, reason := CanFulfill(order) if !canShip { if reason == "" { reason = "订单暂不可发货" } return nil, newDeliveryHTTPError(http.StatusBadRequest, reason) } var expiresAt *time.Time if s.linkTTL > 0 { value := now.Add(s.linkTTL).UTC().Truncate(time.Second) expiresAt = &value } 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, "delivery_link_generated_at": now, }).Error; err != nil { return nil, err } return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil } func (s *DeliveryService) RevokeDeliveryLink(merchantID, actorUserID 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.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&model.FulfillmentOrder{}). Where("id = ?", order.ID). Updates(map[string]interface{}{ "delivery_link_revoked_at": now, }).Error; err != nil { return err } return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.revoke", "fulfillment_order", order.OrderNo, map[string]interface{}{ "revoked_at": timeutil.FormatAPITime(now), }) }) } func (s *DeliveryService) RestoreDeliveryLink(merchantID, actorUserID uint, orderNo, requestBaseURL 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) } now := time.Now().UTC() var expiresAt *time.Time if s.linkTTL > 0 { value := now.Add(s.linkTTL).Truncate(time.Second) if order.DeliveryLinkExpiresAt != nil && value.Equal(order.DeliveryLinkExpiresAt.UTC().Truncate(time.Second)) { value = value.Add(time.Second) } expiresAt = &value } if err := s.fulfillment.db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&model.FulfillmentOrder{}). Where("id = ?", order.ID). Updates(map[string]interface{}{ "delivery_link_expires_at": expiresAt, "delivery_link_revoked_at": nil, "delivery_link_generated_at": now, }).Error; err != nil { return err } metadata := map[string]interface{}{ "generated_at": timeutil.FormatAPITime(now), } if expiresAt != nil { metadata["expires_at"] = timeutil.FormatAPITime(*expiresAt) } else { metadata["permanent"] = true } return writeAudit(tx, &merchantID, optionalUint(actorUserID), nil, "delivery_link.restore", "fulfillment_order", order.OrderNo, metadata) }); err != nil { return nil, err } return s.buildDeliveryLinkResult(order.OrderNo, expiresAt, requestBaseURL), nil }