恢复测试订单创建

This commit is contained in:
yml2213
2026-07-30 19:04:38 +08:00
parent 2f0104be07
commit 254ba9b721
7 changed files with 417 additions and 24 deletions
+33
View File
@@ -132,6 +132,39 @@ func (h *MerchantHandler) ListOrders(c *gin.Context) {
response.Page(c, list, total, page, size)
}
type merchantTestOrderReq struct {
SKU string `json:"sku" binding:"required"`
BuyerReference string `json:"buyer_reference"`
Note string `json:"note"`
FulfillmentStatus string `json:"fulfillment_status"`
}
func (h *MerchantHandler) CreateTestOrder(c *gin.Context) {
var req merchantTestOrderReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:sku 必填")
return
}
order, err := h.fulfillmentSvc.CreateTestOrder(service.CreateTestOrderInput{
MerchantID: middleware.GetMerchantID(c),
ActorUserID: middleware.GetUserID(c),
SKU: req.SKU,
BuyerReference: req.BuyerReference,
Note: req.Note,
FulfillmentStatus: req.FulfillmentStatus,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
canShip, cannotShipReason := service.CanFulfill(order)
response.OK(c, gin.H{
"order": order,
"can_ship": canShip,
"cannot_ship_reason": cannotShipReason,
})
}
func (h *MerchantHandler) GetWallet(c *gin.Context) {
wallet, err := h.fulfillmentSvc.GetWallet(middleware.GetMerchantID(c))
if err != nil {
+14 -13
View File
@@ -83,10 +83,10 @@ func Setup(h *Handlers) *gin.Engine {
auth.Use(middleware.Auth(h.JWT))
auth.Use(middleware.Tenant(h.Tenant))
{
auth.GET("/auth/profile", h.Auth.Profile)
auth.GET("/dashboard", h.Dashboard.Dashboard)
auth.GET("/auth/profile", h.Auth.Profile)
auth.GET("/dashboard", h.Dashboard.Dashboard)
// 商户后台:商户就是平台下游客户,成员只代表该商户内部员工。
// 商户后台:商户就是平台下游客户,成员只代表该商户内部员工。
merchant := auth.Group("/merchant")
{
merchant.GET("", h.Merchant.Current)
@@ -94,6 +94,7 @@ func Setup(h *Handlers) *gin.Engine {
merchant.POST("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateProduct)
merchant.PATCH("/products/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateProduct)
merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), h.Merchant.ListOrders)
merchant.POST("/orders/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateTestOrder)
merchant.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator, model.MemberRoleFinance), h.Merchant.GetWallet)
merchant.GET("/wallet/ledger", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Merchant.ListWalletLedger)
merchant.POST("/wallet/adjust", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Merchant.AdjustWallet)
@@ -111,16 +112,16 @@ func Setup(h *Handlers) *gin.Engine {
admin := auth.Group("")
admin.Use(middleware.RequireRole(model.RoleAdmin))
{
admin.GET("/users", h.User.List)
admin.POST("/users", h.User.Create)
admin.PATCH("/users/:id/status", h.User.UpdateStatus)
admin.GET("/platform/merchants", h.Merchant.ListPlatformMerchants)
admin.POST("/platform/merchants", h.Merchant.CreateMerchant)
admin.PATCH("/platform/merchants/:id", h.Merchant.UpdateMerchantSettings)
admin.POST("/platform/merchants/:id/members", h.Merchant.AddPlatformMerchantMember)
admin.GET("/platform/product-catalog", h.Merchant.ListProductCatalog)
admin.GET("/platform/merchants/:id/products", h.Merchant.ListMerchantProductsByAdmin)
admin.POST("/platform/merchants/:id/products/assign", h.Merchant.AssignProducts)
admin.GET("/users", h.User.List)
admin.POST("/users", h.User.Create)
admin.PATCH("/users/:id/status", h.User.UpdateStatus)
admin.GET("/platform/merchants", h.Merchant.ListPlatformMerchants)
admin.POST("/platform/merchants", h.Merchant.CreateMerchant)
admin.PATCH("/platform/merchants/:id", h.Merchant.UpdateMerchantSettings)
admin.POST("/platform/merchants/:id/members", h.Merchant.AddPlatformMerchantMember)
admin.GET("/platform/product-catalog", h.Merchant.ListProductCatalog)
admin.GET("/platform/merchants/:id/products", h.Merchant.ListMerchantProductsByAdmin)
admin.POST("/platform/merchants/:id/products/assign", h.Merchant.AssignProducts)
}
}
}
+121 -1
View File
@@ -39,6 +39,15 @@ type CreateFulfillmentOrderResult struct {
Idempotent bool `json:"idempotent"`
}
type CreateTestOrderInput struct {
MerchantID uint
ActorUserID uint
SKU string
BuyerReference string
Note string
FulfillmentStatus string
}
func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) {
in.ClientOrderNo = strings.TrimSpace(in.ClientOrderNo)
in.SKU = strings.TrimSpace(in.SKU)
@@ -203,6 +212,113 @@ func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*Creat
return result, nil
}
// CreateTestOrder 创建后台联调订单:不扣钱包、不占库存,只用于上游按订单号查询与发货回传。
func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.FulfillmentOrder, error) {
in.SKU = strings.TrimSpace(in.SKU)
in.BuyerReference = strings.TrimSpace(in.BuyerReference)
in.Note = strings.TrimSpace(in.Note)
if in.MerchantID == 0 {
return nil, errors.New("无效的商户")
}
if in.SKU == "" {
return nil, errors.New("sku 不能为空")
}
if in.BuyerReference == "" {
in.BuyerReference = "测试买家"
}
if len(in.BuyerReference) > 128 {
return nil, errors.New("买家标识最长 128 位")
}
if len(in.Note) > 512 {
return nil, errors.New("备注最长 512 位")
}
if in.FulfillmentStatus == "" {
in.FulfillmentStatus = model.FulfillmentStatusPending
}
switch in.FulfillmentStatus {
case model.FulfillmentStatusPending, model.FulfillmentStatusFailed:
default:
return nil, errors.New("测试订单仅支持待履约或发货失败状态")
}
requestData := map[string]interface{}{
"source": "merchant_test_order",
}
if in.Note != "" {
requestData["note"] = in.Note
}
rawRequestData, err := json.Marshal(requestData)
if err != nil {
return nil, errors.New("订单请求数据无法序列化")
}
var out model.FulfillmentOrder
err = s.db.Transaction(func(tx *gorm.DB) error {
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").
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("商品目录已下架")
}
orderNo := newTestFulfillmentOrderNo()
order := &model.FulfillmentOrder{
MerchantID: in.MerchantID,
OrderNo: orderNo,
ClientOrderNo: "TEST-" + orderNo,
MerchantProductID: product.ID,
ProductSKU: product.SKU,
ProductName: fallbackName(product.DisplayName, product.Product.Name),
Quantity: 1,
BaseAmount: 0,
FeeType: merchant.FeeType,
FeeRateBP: merchant.FeeRateBP,
FeeFixedAmount: merchant.FeeFixedAmount,
ServiceFeeAmount: 0,
Amount: 0,
Currency: product.Currency,
PaymentStatus: model.PaymentStatusPaid,
FulfillmentStatus: in.FulfillmentStatus,
BuyerReference: in.BuyerReference,
RequestData: string(rawRequestData),
}
if in.FulfillmentStatus == model.FulfillmentStatusFailed {
order.FailureReason = fallbackName(in.Note, "联调测试订单初始化为发货失败,可重新发货")
}
if err := tx.Create(order).Error; err != nil {
return err
}
if err := writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "merchant_test_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{
"sku": in.SKU,
"fulfillment_status": in.FulfillmentStatus,
"can_ship": true,
}); err != nil {
return err
}
out = *order
out.MerchantProduct = &product
return nil
})
if err != nil {
return nil, err
}
return &out, nil
}
func (s *FulfillmentService) GetOrder(merchantID uint, orderNo string) (*model.FulfillmentOrder, error) {
var order model.FulfillmentOrder
err := s.db.Preload("MerchantProduct.Product").
@@ -513,6 +629,10 @@ func newFulfillmentOrderNo() string {
return "FO" + time.Now().UTC().Format("20060102150405") + strings.ReplaceAll(uuid.NewString()[:12], "-", "")
}
func newTestFulfillmentOrderNo() string {
return "O" + time.Now().UTC().Format("20060102150405") + strings.ReplaceAll(uuid.NewString()[:12], "-", "")
}
// calculateServiceFee 按"百分比或固定"二选一计算手续费:
// - feeType=rate:按 baseAmount * feeRateBP / 10000 计算
// - feeType=fixed:直接取 feeFixedAmount
@@ -567,7 +687,7 @@ func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} {
"fee_type": order.FeeType,
"service_fee_amount": order.ServiceFeeAmount,
"amount": order.Amount,
"currency": order.Currency,
"currency": order.Currency,
"payment_status": order.PaymentStatus,
"fulfillment_status": order.FulfillmentStatus,
"can_fulfill": canFulfill,
+83 -1
View File
@@ -121,7 +121,7 @@ func TestFulfillmentCreateOrderAppliesMerchantFeeRate(t *testing.T) {
db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-fee-rate", 1000, 5, 200)
if err := db.Model(&model.Merchant{}).Where("id = ?", merchantID).Updates(map[string]interface{}{
"fee_type": model.FeeTypeRate,
"fee_type": model.FeeTypeRate,
"fee_rate_bp": int64(250),
}).Error; err != nil {
t.Fatalf("update merchant fee: %v", err)
@@ -373,3 +373,85 @@ func TestCreateOrderRejectsInsufficientBalance(t *testing.T) {
t.Fatalf("balance should remain unchanged, got %d", wallet.AvailableBalance)
}
}
func TestCreateTestOrderCreatesFulfillableOrderWithoutBilling(t *testing.T) {
db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-test-order", 0, 0, 100)
originalStock := product.Stock
svc := NewFulfillmentService(db, nil)
order, err := svc.CreateTestOrder(CreateTestOrderInput{
MerchantID: merchantID,
ActorUserID: 99,
SKU: product.SKU,
BuyerReference: "测试买家 A",
Note: "联调测试",
})
if err != nil {
t.Fatalf("create test order: %v", err)
}
if !strings.HasPrefix(order.OrderNo, "O") || order.ClientOrderNo != "TEST-"+order.OrderNo {
t.Fatalf("unexpected test order numbers: %+v", order)
}
if order.PaymentStatus != model.PaymentStatusPaid || order.FulfillmentStatus != model.FulfillmentStatusPending {
t.Fatalf("unexpected test order status: %+v", order)
}
canShip, reason := CanFulfill(order)
if !canShip || reason != "" {
t.Fatalf("test order should be fulfillable, canShip=%v reason=%q", canShip, reason)
}
openOrder, err := svc.QueryOpenOrder(order.OrderNo)
if err != nil {
t.Fatalf("query open order: %v", err)
}
if !openOrder.CanShip || openOrder.Product == nil || openOrder.Product.SKU != product.SKU {
t.Fatalf("unexpected open order: %+v", openOrder)
}
var wallet model.WalletAccount
if err := db.Where("merchant_id = ?", merchantID).First(&wallet).Error; err != nil {
t.Fatalf("query wallet: %v", err)
}
if wallet.AvailableBalance != 0 {
t.Fatalf("test order should not debit wallet, got %d", wallet.AvailableBalance)
}
var refreshed model.MerchantProduct
if err := db.First(&refreshed, product.ID).Error; err != nil {
t.Fatalf("query product: %v", err)
}
if refreshed.Stock != originalStock {
t.Fatalf("test order should not decrease stock, got %d", refreshed.Stock)
}
var ledgerCount int64
db.Model(&model.WalletLedgerEntry{}).Where("merchant_id = ?", merchantID).Count(&ledgerCount)
if ledgerCount != 0 {
t.Fatalf("test order should not create wallet ledger, got %d", ledgerCount)
}
}
func TestCreateTestOrderSupportsFailedFulfillableStatus(t *testing.T) {
db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-test-failed", 0, -1, 100)
svc := NewFulfillmentService(db, nil)
order, err := svc.CreateTestOrder(CreateTestOrderInput{
MerchantID: merchantID,
SKU: product.SKU,
FulfillmentStatus: model.FulfillmentStatusFailed,
Note: "等待重新发货",
})
if err != nil {
t.Fatalf("create failed test order: %v", err)
}
if order.FulfillmentStatus != model.FulfillmentStatusFailed || order.FailureReason != "等待重新发货" {
t.Fatalf("unexpected failed test order: %+v", order)
}
openOrder, err := svc.QueryOpenOrder(order.OrderNo)
if err != nil {
t.Fatalf("query open failed order: %v", err)
}
if !openOrder.CanShip || openOrder.Status != "ship_failed" {
t.Fatalf("failed test order should be re-fulfillable, got %+v", openOrder)
}
}