商户管理优化, 商户只能上架下架, 管理员可以编辑

This commit is contained in:
yml2213
2026-08-04 14:35:53 +08:00
parent 6404b10626
commit 16b508eb9b
8 changed files with 202 additions and 196 deletions
+44 -60
View File
@@ -52,73 +52,21 @@ func (h *MerchantHandler) ListProducts(c *gin.Context) {
response.Page(c, list, total, page, size)
}
type merchantProductReq struct {
ProductCode string `json:"product_code"`
ProductName string `json:"product_name"`
Category string `json:"category"`
Description string `json:"description"`
Attributes string `json:"attributes"`
SKU string `json:"sku" binding:"required"`
DisplayName string `json:"display_name"`
PriceAmount int64 `json:"price_amount"`
CostAmount int64 `json:"cost_amount"`
Currency string `json:"currency"`
Stock int64 `json:"stock"`
Status string `json:"status"`
FulfillmentConfig string `json:"fulfillment_config"`
type merchantProductStatusReq struct {
Status string `json:"status" binding:"required"`
}
func (h *MerchantHandler) CreateProduct(c *gin.Context) {
var req merchantProductReq
// UpdateProductStatus PATCH /api/merchant/products/:id/status
// 商户仅能上架/下架商品,价格、成本等配置由平台管理员维护。
func (h *MerchantHandler) UpdateProductStatus(c *gin.Context) {
var req merchantProductStatusReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:sku 必填")
return
}
product, err := h.merchantSvc.CreateMerchantProduct(middleware.GetMerchantID(c), service.CreateMerchantProductInput{
ProductCode: req.ProductCode,
ProductName: req.ProductName,
Category: req.Category,
Description: req.Description,
Attributes: req.Attributes,
SKU: req.SKU,
DisplayName: req.DisplayName,
PriceAmount: req.PriceAmount,
CostAmount: req.CostAmount,
Currency: req.Currency,
Stock: req.Stock,
Status: req.Status,
FulfillmentConfig: req.FulfillmentConfig,
}, middleware.GetUserID(c))
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, product)
}
type merchantProductUpdateReq struct {
DisplayName *string `json:"display_name"`
PriceAmount *int64 `json:"price_amount"`
CostAmount *int64 `json:"cost_amount"`
Stock *int64 `json:"stock"`
Status *string `json:"status"`
FulfillmentConfig *string `json:"fulfillment_config"`
}
func (h *MerchantHandler) UpdateProduct(c *gin.Context) {
var req merchantProductUpdateReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
response.BadRequest(c, "参数错误:status 必填")
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{
DisplayName: req.DisplayName,
PriceAmount: req.PriceAmount,
CostAmount: req.CostAmount,
Stock: req.Stock,
Status: req.Status,
FulfillmentConfig: req.FulfillmentConfig,
Status: &req.Status,
}, middleware.GetUserID(c)); err != nil {
response.BadRequest(c, err.Error())
return
@@ -453,6 +401,42 @@ func (h *MerchantHandler) ListMerchantProductsByAdmin(c *gin.Context) {
response.OK(c, list)
}
type merchantProductUpdateReq struct {
DisplayName *string `json:"display_name"`
PriceAmount *int64 `json:"price_amount"`
CostAmount *int64 `json:"cost_amount"`
Stock *int64 `json:"stock"`
Status *string `json:"status"`
FulfillmentConfig *string `json:"fulfillment_config"`
}
// AdminUpdateMerchantProduct 供平台管理员编辑指定商户的商品(价格、成本、库存、状态等)。
func (h *MerchantHandler) AdminUpdateMerchantProduct(c *gin.Context) {
merchantID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
productID, _ := strconv.ParseUint(c.Param("pid"), 10, 64)
if merchantID == 0 || productID == 0 {
response.BadRequest(c, "参数错误")
return
}
var req merchantProductUpdateReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误")
return
}
if err := h.merchantSvc.UpdateMerchantProduct(uint(merchantID), uint(productID), service.UpdateMerchantProductInput{
DisplayName: req.DisplayName,
PriceAmount: req.PriceAmount,
CostAmount: req.CostAmount,
Stock: req.Stock,
Status: req.Status,
FulfillmentConfig: req.FulfillmentConfig,
}, middleware.GetUserID(c)); err != nil {
response.BadRequest(c, err.Error())
return
}
response.OK(c, nil)
}
type assignProductsReq struct {
CatalogIDs []uint `json:"catalog_ids"`
}
+2 -2
View File
@@ -124,8 +124,7 @@ func Setup(h *Handlers) *gin.Engine {
{
merchant.GET("", h.Merchant.Current)
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), h.Merchant.ListProducts)
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.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateProductStatus)
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("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.GetDeliveryLink)
@@ -165,6 +164,7 @@ func Setup(h *Handlers) *gin.Engine {
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.PATCH("/platform/merchants/:id/products/:pid", h.Merchant.AdminUpdateMerchantProduct)
admin.POST("/platform/merchants/:id/products/assign", h.Merchant.AssignProducts)
admin.GET("/platform/merchants/:id/wallet", h.Merchant.AdminGetWallet)
admin.GET("/platform/merchants/:id/wallet/ledger", h.Merchant.AdminListWalletLedger)
@@ -723,6 +723,32 @@ func TestCreateOrderRejectsInsufficientBalance(t *testing.T) {
}
}
func TestCreateOrderRejectsInactiveProduct(t *testing.T) {
db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-inactive-product", 5000, 1, 100)
svc := NewFulfillmentService(db, nil)
// 商户下架商品后:开放接口下单与商户测试订单都应被拒绝
if err := db.Model(&model.MerchantProduct{}).Where("id = ?", product.ID).Update("status", model.ProductStatusInactive).Error; err != nil {
t.Fatalf("deactivate product: %v", err)
}
if _, err := svc.CreateOrder(CreateFulfillmentOrderInput{
MerchantID: merchantID,
APIClientID: 16,
ClientOrderNo: "client-inactive-product",
SKU: product.SKU,
}); err == nil || !strings.Contains(err.Error(), "已下架") {
t.Fatalf("inactive product should reject order, got %v", err)
}
if _, err := svc.CreateTestOrder(CreateTestOrderInput{
MerchantID: merchantID,
ActorUserID: 99,
SKU: product.SKU,
}); err == nil || !strings.Contains(err.Error(), "已下架") {
t.Fatalf("inactive product should reject test order, got %v", err)
}
}
func TestCreateTestOrderCreatesFulfillableOrderWithoutBilling(t *testing.T) {
db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-test-order", 0, 0, 100)