Files
affiliate_dash/backend/internal/service/fulfillment_create.go
T
yml2213 5ed36c724f refactor(fulfillment): 按职责拆分 fulfillment.go(640 → 最大 235 行)
纯文件级移动,无行为变更,沿用既有的 fulfillment_<aspect>.go 命名约定:
- fulfillment.go            结构体、构造函数、上游钩子、CanFulfill、orderCallbackData
- fulfillment_create.go     下单类型定义、CreateManualOrder、CreateOrder
- fulfillment_testorder.go  联调测试订单
- fulfillment_update.go     履约结果更新
- fulfillment_cancel.go     取消订单(含上游联动取消)

校验:拆分前后 19 个 func/type 声明完全一致(无缺失、无多出、无重复)。
2026-10-06 19:50:13 +08:00

236 lines
7.8 KiB
Go

package service
import (
"encoding/json"
"errors"
"math"
"strings"
"affiliate_dash/internal/model"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type CreateFulfillmentOrderInput struct {
MerchantID uint
APIClientID uint
ActorUserID uint
ClientOrderNo string
SKU string
Quantity int64
BuyerReference string
RequestData interface{}
OrderSource string
}
type CreateFulfillmentOrderResult struct {
Order *model.FulfillmentOrder `json:"order"`
Idempotent bool `json:"idempotent"`
}
// CreateManualOrderInput 是商户后台创建真实订单的输入。其账务、库存与回调规则与开放 API 下单一致。
type CreateManualOrderInput struct {
MerchantID uint
ActorUserID uint
ClientOrderNo string
SKU string
Quantity int64
BuyerReference string
RequestData interface{}
}
func (s *FulfillmentService) CreateManualOrder(in CreateManualOrderInput) (*CreateFulfillmentOrderResult, error) {
return s.CreateOrder(CreateFulfillmentOrderInput{
MerchantID: in.MerchantID,
ActorUserID: in.ActorUserID,
ClientOrderNo: in.ClientOrderNo,
SKU: in.SKU,
Quantity: in.Quantity,
BuyerReference: in.BuyerReference,
RequestData: in.RequestData,
OrderSource: model.OrderSourceManual,
})
}
func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) {
in.ClientOrderNo = strings.TrimSpace(in.ClientOrderNo)
in.SKU = strings.TrimSpace(in.SKU)
if in.MerchantID == 0 || (in.APIClientID == 0 && in.ActorUserID == 0) {
return nil, errors.New("无效的商户或下单身份")
}
if in.ClientOrderNo == "" || len(in.ClientOrderNo) > 96 {
return nil, errors.New("client_order_no 不能为空且最长 96 位")
}
if in.SKU == "" {
return nil, errors.New("sku 不能为空")
}
if in.Quantity == 0 {
in.Quantity = 1
}
if in.Quantity < 1 {
return nil, errors.New("quantity 必须大于零")
}
orderSource := strings.TrimSpace(in.OrderSource)
if orderSource == "" {
orderSource = model.OrderSourceAPI
}
switch orderSource {
case model.OrderSourceAPI, model.OrderSourceManual:
default:
return nil, errors.New("无效的订单来源")
}
requestData := ""
if in.RequestData != nil {
raw, err := json.Marshal(in.RequestData)
if err != nil {
return nil, errors.New("订单请求数据无法序列化")
}
requestData = string(raw)
}
fingerprint := orderRequestFingerprint(in, requestData)
result := &CreateFulfillmentOrderResult{}
err := s.db.Transaction(func(tx *gorm.DB) error {
var existing model.FulfillmentOrder
err := tx.Where("merchant_id = ? AND client_order_no = ?", in.MerchantID, in.ClientOrderNo).First(&existing).Error
if err == nil {
if err := ensureSameIdempotentOrder(&existing, fingerprint); err != nil {
return err
}
result.Order = &existing
result.Idempotent = true
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
var merchant model.Merchant
if err := tx.Where("id = ? AND status = ?", in.MerchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("商户不存在或已禁用")
}
return err
}
var product model.MerchantProduct
if err := tx.Preload("Product").Clauses(clause.Locking{Strength: "UPDATE"}).
Where("merchant_id = ? AND sku = ? AND status = ?", in.MerchantID, in.SKU, model.ProductStatusActive).
First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("商品不存在或已下架")
}
return err
}
if product.Product == nil || product.Product.Status != model.ProductStatusActive {
return errors.New("商品目录已下架")
}
if product.Stock >= 0 && product.Stock < in.Quantity {
return errors.New("商品库存不足")
}
if product.PriceAmount > 0 && in.Quantity > math.MaxInt64/product.PriceAmount {
return errors.New("订单金额超出范围")
}
baseAmount := product.PriceAmount * in.Quantity
serviceFee, err := calculateServiceFee(baseAmount, merchant.FeeType, merchant.FeeRateBP, merchant.FeeFixedAmount)
if err != nil {
return err
}
if serviceFee > math.MaxInt64-baseAmount {
return errors.New("订单金额超出范围")
}
totalAmount := baseAmount + serviceFee
var wallet model.WalletAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("merchant_id = ?", in.MerchantID).First(&wallet).Error; err != nil {
return err
}
if wallet.AvailableBalance < totalAmount {
return errors.New("商户钱包余额不足")
}
newBalance := wallet.AvailableBalance - totalAmount
if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil {
return err
}
order := &model.FulfillmentOrder{
MerchantID: in.MerchantID,
OrderNo: newFulfillmentOrderNo(),
ClientOrderNo: in.ClientOrderNo,
MerchantProductID: product.ID,
ProductSKU: product.SKU,
ProductName: fallbackName(product.DisplayName, product.Product.Name),
Quantity: in.Quantity,
BaseAmount: baseAmount,
FeeType: merchant.FeeType,
FeeRateBP: merchant.FeeRateBP,
FeeFixedAmount: merchant.FeeFixedAmount,
ServiceFeeAmount: serviceFee,
Amount: totalAmount,
Currency: product.Currency,
OrderStatus: model.OrderStatusPaid,
OrderSource: orderSource,
BuyerReference: in.BuyerReference,
RequestFingerprint: fingerprint,
RequestData: requestData,
}
if err := tx.Create(order).Error; err != nil {
return err
}
idempotencyKey := in.ClientOrderNo
ledgerNote := "开放接口下单扣款(含平台手续费)"
if orderSource == model.OrderSourceManual {
ledgerNote = "商户后台手动下单扣款(含平台手续费)"
}
if err := tx.Create(&model.WalletLedgerEntry{
MerchantID: in.MerchantID,
WalletAccountID: wallet.ID,
EntryNo: "WL" + uuid.NewString(),
Type: model.WalletLedgerDebit,
Amount: -totalAmount,
BalanceAfter: newBalance,
ReferenceType: "fulfillment_order",
ReferenceNo: order.OrderNo,
IdempotencyKey: &idempotencyKey,
Note: ledgerNote,
}).Error; err != nil {
return err
}
if product.Stock >= 0 {
if err := tx.Model(&model.MerchantProduct{}).Where("id = ? AND stock >= ?", product.ID, in.Quantity).
Update("stock", gorm.Expr("stock - ?", in.Quantity)).Error; err != nil {
return err
}
}
action := "open_order.create"
if orderSource == model.OrderSourceManual {
action = "merchant_order.manual_create"
}
if err := writeAudit(tx, &in.MerchantID, optionalUint(in.ActorUserID), optionalUint(in.APIClientID), action, "fulfillment_order", order.OrderNo, map[string]interface{}{"client_order_no": in.ClientOrderNo, "sku": in.SKU, "source": orderSource}); err != nil {
return err
}
if s.callbacks != nil && orderSource != model.OrderSourceManual {
if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.created", orderCallbackData(order)); err != nil {
return err
}
}
result.Order = order
return nil
})
if err != nil {
// 并发请求恰好同时通过首次查询时,唯一约束冲突后返回既有订单。
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "UNIQUE") {
var existing model.FulfillmentOrder
if queryErr := s.db.Where("merchant_id = ? AND client_order_no = ?", in.MerchantID, in.ClientOrderNo).First(&existing).Error; queryErr == nil {
if sameErr := ensureSameIdempotentOrder(&existing, fingerprint); sameErr != nil {
return nil, sameErr
}
return &CreateFulfillmentOrderResult{Order: &existing, Idempotent: true}, nil
}
}
return nil, err
}
return result, nil
}