拆分 service 与前端大文件,修复 CORS 配置与格式问题
- 后端 internal/service 按职责拆分: fulfillment.go(1397→527)拆出 wallet/timeout/data/order/query/dashboard/shipnotify delivery.go(1124→801)拆出 upstream/link/state/helpers merchant.go(855→251)拆出 member/product/api_client/catalog/helpers - 前端 MerchantCenter.tsx(1327→606)拆出 merchantCenterTabs/merchantCenterUtils - docker-compose backend 透传 CORS_ALLOWED_ORIGINS - CORS 白名单实现(config/router/README/.env.example 配套) - 修复 gofmt 与文件尾部多余空行
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user