package service import ( "errors" "strings" "time" "unicode/utf8" "affiliate_dash/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ShipNotifyInput 上游发货结果推送。 type ShipNotifyInput struct { OrderNo string ShipStatus string // success / failed ProviderOrderNo string ShippedAt *time.Time FailReason string RawPayload string GameChannel *string GameUID *string RoleName *string PayScore *int } // ShipNotifyResult 上游推送处理结果。 type ShipNotifyResult struct { OrderNo string `json:"order_no"` Status string `json:"status"` Message string `json:"message"` } // HandleShipNotify 处理上游发货结果推送(幂等),基于 FulfillmentOrder。 func (s *FulfillmentService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyResult, error) { in.ShipStatus = strings.TrimSpace(in.ShipStatus) in.FailReason = strings.TrimSpace(in.FailReason) if in.OrderNo == "" { return nil, errors.New("订单号不能为空") } if in.ShipStatus != "success" && in.ShipStatus != "failed" { return nil, errors.New("无效的 ship_status,仅支持 success/failed") } if in.ShipStatus == "failed" && in.FailReason == "" { return nil, errors.New("发货失败时 fail_reason 必填") } if utf8.RuneCountInString(in.FailReason) > 512 { return nil, errors.New("fail_reason 最长 512 个字符") } var nextStatus string switch in.ShipStatus { case "success": nextStatus = model.OrderStatusDelivered case "failed": nextStatus = model.OrderStatusShipFailed } var result ShipNotifyResult var rejectionErr error if err := s.db.Transaction(func(tx *gorm.DB) error { var order model.FulfillmentOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Preload("MerchantProduct.Product"). Where("order_no = ?", in.OrderNo). First(&order).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("订单不存在") } return err } // 已交付:success 推送幂等成功。状态读取和后续更新必须在同一把行锁内完成。 if normalizeOrderStatus(&order) == model.OrderStatusDelivered && in.ShipStatus == "success" { result = ShipNotifyResult{ OrderNo: order.OrderNo, Status: normalizeOrderStatus(&order), Message: "订单已交付,幂等成功", } return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, shipNotifyAuditMetadata(in, normalizeOrderStatus(&order), "订单已交付,幂等忽略")) } if normalizeOrderStatus(&order) == model.OrderStatusCancelled { if err := writeShipNotifyRejectedAudit(tx, &order, in, "订单已取消,拒绝更新"); err != nil { return err } rejectionErr = errors.New("订单已取消,无法更新发货状态") return nil } now := time.Now() shippedAt := in.ShippedAt if shippedAt == nil && in.ShipStatus == "success" { shippedAt = &now } updates := map[string]interface{}{ "order_status": nextStatus, } var message string switch in.ShipStatus { case "success": if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil { message := "当前状态不允许标记发货成功" if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil { return err } rejectionErr = err return nil } updates["delivered_at"] = shippedAt updates["failure_reason"] = "" if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } message = "发货成功,订单已交付" case "failed": if err := validateOrderStatusTransition(&order, nextStatus, fulfillmentTransitionShipNotify); err != nil { message := "当前状态不允许标记发货失败" if normalizeOrderStatus(&order) == model.OrderStatusDelivered { message = "订单已交付,拒绝失败推送" } if err := writeShipNotifyRejectedAudit(tx, &order, in, message); err != nil { return err } rejectionErr = err return nil } updates["failure_reason"] = in.FailReason if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } message = "已记录发货失败" } resultData := buildShipNotifyResultData(order.ResultData, in, shippedAt) updates["result_data"] = resultData if err := tx.Model(&model.FulfillmentOrder{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil { return err } if err := writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, shipNotifyAuditMetadata(in, nextStatus, message)); err != nil { return err } var updated model.FulfillmentOrder if err := tx.Preload("MerchantProduct.Product").First(&updated, order.ID).Error; err != nil { return err } if s.callbacks != nil { if err := s.callbacks.Enqueue(tx, order.MerchantID, "order.shipping.updated", orderCallbackData(&updated)); err != nil { return err } } result = ShipNotifyResult{ OrderNo: order.OrderNo, Status: nextStatus, Message: message, } return nil }); err != nil { return nil, err } if rejectionErr != nil { return nil, rejectionErr } return &result, nil } func writeShipNotifyRejectedAudit(tx *gorm.DB, order *model.FulfillmentOrder, in ShipNotifyInput, message string) error { return writeAudit(tx, &order.MerchantID, nil, nil, "ship.notify", "fulfillment_order", order.OrderNo, shipNotifyAuditMetadata(in, normalizeOrderStatus(order), message)) } func shipNotifyAuditMetadata(in ShipNotifyInput, resultStatus, message string) map[string]interface{} { metadata := map[string]interface{}{ "ship_status": in.ShipStatus, "provider_order_no": in.ProviderOrderNo, "fail_reason": in.FailReason, "result_status": resultStatus, "message": message, "payload": in.RawPayload, } return metadata }