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

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) response.Page(c, list, total, page, size)
} }
type merchantProductReq struct { type merchantProductStatusReq struct {
ProductCode string `json:"product_code"` Status string `json:"status" binding:"required"`
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"`
} }
func (h *MerchantHandler) CreateProduct(c *gin.Context) { // UpdateProductStatus PATCH /api/merchant/products/:id/status
var req merchantProductReq // 商户仅能上架/下架商品,价格、成本等配置由平台管理员维护。
func (h *MerchantHandler) UpdateProductStatus(c *gin.Context) {
var req merchantProductStatusReq
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数错误:sku 必填") response.BadRequest(c, "参数错误:status 必填")
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, "参数错误")
return return
} }
id, _ := strconv.ParseUint(c.Param("id"), 10, 64) id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{ if err := h.merchantSvc.UpdateMerchantProduct(middleware.GetMerchantID(c), uint(id), service.UpdateMerchantProductInput{
DisplayName: req.DisplayName, Status: &req.Status,
PriceAmount: req.PriceAmount,
CostAmount: req.CostAmount,
Stock: req.Stock,
Status: req.Status,
FulfillmentConfig: req.FulfillmentConfig,
}, middleware.GetUserID(c)); err != nil { }, middleware.GetUserID(c)); err != nil {
response.BadRequest(c, err.Error()) response.BadRequest(c, err.Error())
return return
@@ -453,6 +401,42 @@ func (h *MerchantHandler) ListMerchantProductsByAdmin(c *gin.Context) {
response.OK(c, list) 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 { type assignProductsReq struct {
CatalogIDs []uint `json:"catalog_ids"` 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("", h.Merchant.Current)
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), h.Merchant.ListProducts) 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/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateProductStatus)
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.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.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) 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.POST("/platform/merchants/:id/members", h.Merchant.AddPlatformMerchantMember)
admin.GET("/platform/product-catalog", h.Merchant.ListProductCatalog) admin.GET("/platform/product-catalog", h.Merchant.ListProductCatalog)
admin.GET("/platform/merchants/:id/products", h.Merchant.ListMerchantProductsByAdmin) 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.POST("/platform/merchants/:id/products/assign", h.Merchant.AssignProducts)
admin.GET("/platform/merchants/:id/wallet", h.Merchant.AdminGetWallet) admin.GET("/platform/merchants/:id/wallet", h.Merchant.AdminGetWallet)
admin.GET("/platform/merchants/:id/wallet/ledger", h.Merchant.AdminListWalletLedger) 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) { func TestCreateTestOrderCreatesFulfillableOrderWithoutBilling(t *testing.T) {
db := newServiceTestDB(t) db := newServiceTestDB(t)
merchantID, product := seedFulfillmentMerchant(t, db, "merchant-test-order", 0, 0, 100) merchantID, product := seedFulfillmentMerchant(t, db, "merchant-test-order", 0, 0, 100)
+1 -1
View File
@@ -40,7 +40,7 @@ function AppRoutes() {
> >
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="merchant-center" element={<MerchantCenter />} /> <Route path="merchant-center" element={<MerchantCenter />} />
<Route path="merchant-products" element={<MerchantCenter fixedTab="products" title="商品" />} /> <Route path="merchant-products" element={<MerchantCenter fixedTab="products" title="商品列表" />} />
<Route path="merchant-orders" element={<MerchantCenter fixedTab="orders" title="发货订单" />} /> <Route path="merchant-orders" element={<MerchantCenter fixedTab="orders" title="发货订单" />} />
<Route path="merchant-wallet" element={<MerchantCenter fixedTab="wallet" title="积分明细" />} /> <Route path="merchant-wallet" element={<MerchantCenter fixedTab="wallet" title="积分明细" />} />
<Route path="merchant-recharge" element={<MerchantRecharge />} /> <Route path="merchant-recharge" element={<MerchantRecharge />} />
+4 -9
View File
@@ -56,15 +56,8 @@ export const merchantApi = {
request.get('/merchant').then((r) => r.data.data as { merchant: Merchant; role: MerchantMember['role'] }), request.get('/merchant').then((r) => r.data.data as { merchant: Merchant; role: MerchantMember['role'] }),
products: (params?: Record<string, unknown>) => products: (params?: Record<string, unknown>) =>
request.get('/merchant/products', { params }).then((r) => r.data.data as PageResult<MerchantProduct>), request.get('/merchant/products', { params }).then((r) => r.data.data as PageResult<MerchantProduct>),
createProduct: (data: Partial<MerchantProduct> & { updateProductStatus: (id: number, status: MerchantProduct['status']) =>
product_code?: string request.patch(`/merchant/products/${id}/status`, { status }).then((r) => r.data.data),
product_name?: string
category?: string
description?: string
attributes?: string
}) => request.post('/merchant/products', data).then((r) => r.data.data as MerchantProduct),
updateProduct: (id: number, data: Partial<MerchantProduct>) =>
request.patch(`/merchant/products/${id}`, data).then((r) => r.data.data),
orders: (params?: Record<string, unknown>) => orders: (params?: Record<string, unknown>) =>
request.get('/merchant/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>), request.get('/merchant/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>),
createTestOrder: (data: { createTestOrder: (data: {
@@ -184,6 +177,8 @@ export const platformApi = {
request.get('/platform/product-catalog').then((r) => r.data.data as ProductCatalogItem[]), request.get('/platform/product-catalog').then((r) => r.data.data as ProductCatalogItem[]),
merchantProducts: (merchantId: number) => merchantProducts: (merchantId: number) =>
request.get(`/platform/merchants/${merchantId}/products`).then((r) => r.data.data as ProductCatalogItem[]), request.get(`/platform/merchants/${merchantId}/products`).then((r) => r.data.data as ProductCatalogItem[]),
updateMerchantProduct: (merchantId: number, productId: number, data: Partial<MerchantProduct>) =>
request.patch(`/platform/merchants/${merchantId}/products/${productId}`, data).then((r) => r.data.data),
assignProducts: (merchantId: number, catalogIds: number[]) => assignProducts: (merchantId: number, catalogIds: number[]) =>
request.post(`/platform/merchants/${merchantId}/products/assign`, { catalog_ids: catalogIds }).then((r) => r.data.data as { assigned: number }), request.post(`/platform/merchants/${merchantId}/products/assign`, { catalog_ids: catalogIds }).then((r) => r.data.data as { assigned: number }),
wallet: (merchantId: number) => wallet: (merchantId: number) =>
+23 -23
View File
@@ -770,20 +770,18 @@ a:hover {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 4px 8px; padding: 4px 8px;
margin-bottom: 4px; margin-bottom: 6px;
font-size: 11.5px; font-size: 11.5px;
font-weight: 800; font-weight: 700;
color: #1e293b; color: #64748b;
background: #e2e8f0;
border-radius: 4px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.api-docs__nav-heading::before { .api-docs__nav-heading::before {
content: ''; content: '';
display: inline-block; display: inline-block;
width: 5px; width: 4px;
height: 5px; height: 4px;
border-radius: 50%; border-radius: 50%;
background: #2563eb; background: #2563eb;
} }
@@ -791,9 +789,10 @@ a:hover {
.api-docs__nav-item { .api-docs__nav-item {
display: block; display: block;
min-height: 32px; min-height: 32px;
padding: 6px 10px 6px 12px; padding: 6px 12px;
font-size: 13px; font-size: 13px;
color: #475569; font-weight: 500;
color: #334155;
cursor: pointer; cursor: pointer;
border-radius: 6px; border-radius: 6px;
transition: all 0.15s ease; transition: all 0.15s ease;
@@ -805,15 +804,13 @@ a:hover {
.api-docs__nav-item:hover { .api-docs__nav-item:hover {
color: #2563eb; color: #2563eb;
background: #e2e8f0; background: #eff6ff;
} }
.api-docs__nav-item--active { .api-docs__nav-item--active {
color: #2563eb !important; color: #2563eb !important;
background: #ffffff !important; background: #eff6ff !important;
font-weight: 700; font-weight: 600;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
border-left: 3.5px solid #2563eb;
} }
.api-docs__nav-sub { .api-docs__nav-sub {
@@ -821,34 +818,37 @@ a:hover {
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
margin: 4px 0 6px 10px; margin: 4px 0 6px 10px;
padding-left: 10px; padding-left: 12px;
border-left: 2px solid #cbd5e1; border-left: 1.5px solid #cbd5e1;
} }
.api-docs__nav-sub-item { .api-docs__nav-sub-item {
display: block; display: block;
min-height: 28px; min-height: 28px;
padding: 4px 8px; padding: 4px 10px;
font-size: 12px; font-size: 12px;
color: #64748b; color: #64748b;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 5px;
transition: all 0.15s ease; transition: all 0.15s ease;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
position: relative;
} }
.api-docs__nav-sub-item:hover { .api-docs__nav-sub-item:hover {
color: #2563eb; color: #2563eb;
background: #e2e8f0; background: #f1f5f9;
} }
.api-docs__nav-sub-item--active { .api-docs__nav-sub-item--active {
color: #1d4ed8 !important; color: #2563eb !important;
font-weight: 700; font-weight: 600;
background: #ffffff !important; background: #eff6ff !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); border-left: 2px solid #2563eb;
margin-left: -13.5px;
padding-left: 11.5px;
} }
.api-docs__content { .api-docs__content {
+19 -100
View File
@@ -113,8 +113,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
const [callback, setCallback] = useState<CallbackSubscription | null>(null) const [callback, setCallback] = useState<CallbackSubscription | null>(null)
const [members, setMembers] = useState<MerchantMember[]>([]) const [members, setMembers] = useState<MerchantMember[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [productOpen, setProductOpen] = useState(false)
const [editingProduct, setEditingProduct] = useState<MerchantProduct | null>(null)
const [apiClientOpen, setApiClientOpen] = useState(false) const [apiClientOpen, setApiClientOpen] = useState(false)
const [apiCredential, setApiCredential] = useState<ApiCredential | null>(null) const [apiCredential, setApiCredential] = useState<ApiCredential | null>(null)
const [callbackCredential, setCallbackCredential] = useState<CallbackCredential | null>(null) const [callbackCredential, setCallbackCredential] = useState<CallbackCredential | null>(null)
@@ -123,7 +121,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
const [testOrderProducts, setTestOrderProducts] = useState<MerchantProduct[]>([]) const [testOrderProducts, setTestOrderProducts] = useState<MerchantProduct[]>([])
const [testOrderProductsLoading, setTestOrderProductsLoading] = useState(false) const [testOrderProductsLoading, setTestOrderProductsLoading] = useState(false)
const [testOrderResult, setTestOrderResult] = useState<CreateTestOrderResult | null>(null) const [testOrderResult, setTestOrderResult] = useState<CreateTestOrderResult | null>(null)
const [productForm] = Form.useForm()
const [walletFilterForm] = Form.useForm() const [walletFilterForm] = Form.useForm()
const [apiClientForm] = Form.useForm() const [apiClientForm] = Form.useForm()
const [callbackForm] = Form.useForm() const [callbackForm] = Form.useForm()
@@ -295,45 +292,13 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
} }
}, [activeTab, routeTab]) }, [activeTab, routeTab])
const openProductCreate = () => { const openProductToggle = (record: MerchantProduct) => {
setEditingProduct(null) merchantApi.updateProductStatus(record.id, record.status === 'active' ? 'inactive' : 'active')
productForm.resetFields() .then(() => {
productForm.setFieldsValue({ message.success(record.status === 'active' ? '商品已下架,商户无法再下单' : '商品已上架')
currency: 'POINT',
stock: -1,
status: 'active',
price_amount: 0,
cost_amount: 0,
})
setProductOpen(true)
}
const openProductEdit = (record: MerchantProduct) => {
setEditingProduct(record)
productForm.setFieldsValue({
...record,
})
setProductOpen(true)
}
const submitProduct = async () => {
const values = await productForm.validateFields()
try {
const payload = {
...values,
}
if (editingProduct) {
await merchantApi.updateProduct(editingProduct.id, payload)
message.success('商品已更新')
} else {
await merchantApi.createProduct(payload)
message.success('商品已创建')
}
setProductOpen(false)
loadProducts() loadProducts()
} catch (e) { })
message.error(e instanceof Error ? e.message : '保存失败') .catch((e) => message.error(e instanceof Error ? e.message : '操作失败'))
}
} }
const handleWalletFilter = async () => { const handleWalletFilter = async () => {
@@ -542,7 +507,16 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
width: 90, width: 90,
fixed: 'right', fixed: 'right',
render: (_, record) => canManage ? ( render: (_, record) => canManage ? (
<Button type="link" size="small" onClick={() => openProductEdit(record)}></Button> <Popconfirm
title={record.status === 'active' ? '下架该商品?' : '上架该商品?'}
description={record.status === 'active' ? '下架后商户将无法再创建该商品订单' : '上架后商户可正常下单'}
okText="确认"
onConfirm={() => openProductToggle(record)}
>
<Button type="link" size="small">
{record.status === 'active' ? '下架' : '上架'}
</Button>
</Popconfirm>
) : '-', ) : '-',
}, },
] ]
@@ -646,16 +620,11 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
<Card size="small" style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 8 }}> <Card size="small" style={{ background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div> <div>
<span style={{ fontWeight: 600, color: '#0f172a', marginRight: 8 }}></span> <span style={{ fontWeight: 600, color: '#0f172a', marginRight: 8 }}></span>
<Typography.Text type="secondary" style={{ fontSize: 13 }}> <Typography.Text type="secondary" style={{ fontSize: 13 }}>
SKU /
</Typography.Text> </Typography.Text>
</div> </div>
{canManage && (
<Button type="primary" icon={<PlusOutlined />} onClick={openProductCreate}>
</Button>
)}
</div> </div>
</Card> </Card>
<Table <Table
@@ -1005,7 +974,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
) )
const tabItems = [ const tabItems = [
{ key: 'products', label: '商品', disabled: !hasFeature('products'), children: productContent }, { key: 'products', label: '商品列表', disabled: !hasFeature('products'), children: productContent },
{ key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: orderContent }, { key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: orderContent },
{ key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: walletContent }, { key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: walletContent },
{ key: 'api', label: 'API 密钥', disabled: !hasFeature('api'), children: apiKeyContent }, { key: 'api', label: 'API 密钥', disabled: !hasFeature('api'), children: apiKeyContent },
@@ -1035,56 +1004,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
/> />
)} )}
<Modal title={editingProduct ? '编辑商品' : '新增商品'} open={productOpen} onOk={submitProduct} onCancel={() => setProductOpen(false)} destroyOnClose width={620}>
<Form form={productForm} layout="vertical" style={{ marginTop: 16 }}>
{!editingProduct && (
<>
<Form.Item name="product_code" label="目录编码">
<Input placeholder="已有目录编码可选填" />
</Form.Item>
<Form.Item name="product_name" label="商品名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</>
)}
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="sku" label="商户 SKU" rules={[{ required: true }]} style={{ width: 280 }}>
<Input disabled={!!editingProduct} />
</Form.Item>
<Form.Item name="display_name" label="展示名" style={{ width: 280 }}>
<Input />
</Form.Item>
</Space>
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="price_amount" label="售价(积分)" rules={[{ required: true }]} style={{ width: 180 }}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="cost_amount" label="成本(积分)" style={{ width: 180 }}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="currency" label="币种" style={{ width: 180 }}>
<Select options={[{ value: 'POINT', label: 'POINT(积分)' }]} />
</Form.Item>
</Space>
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="stock" label="库存(-1 无限)" style={{ width: 180 }}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="status" label="状态" style={{ width: 180 }}>
<Select options={[{ value: 'active', label: '上架' }, { value: 'inactive', label: '下架' }]} />
</Form.Item>
{!editingProduct && (
<Form.Item name="category" label="品类" style={{ width: 180 }}>
<Input />
</Form.Item>
)}
</Space>
<Form.Item name="fulfillment_config" label="发货配置">
<Input.TextArea rows={3} placeholder='{"provider":"manual"}' />
</Form.Item>
</Form>
</Modal>
<Modal title="创建测试订单" open={testOrderOpen} onOk={submitTestOrder} onCancel={() => setTestOrderOpen(false)} destroyOnClose width={560}> <Modal title="创建测试订单" open={testOrderOpen} onOk={submitTestOrder} onCancel={() => setTestOrderOpen(false)} destroyOnClose width={560}>
<Form form={testOrderForm} layout="vertical" style={{ marginTop: 16 }}> <Form form={testOrderForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="sku" label="商品皮肤" rules={[{ required: true, message: '请选择商品' }]}> <Form.Item name="sku" label="商品皮肤" rules={[{ required: true, message: '请选择商品' }]}>
+82
View File
@@ -45,9 +45,13 @@ export default function PlatformMerchants() {
const [adjustLoading, setAdjustLoading] = useState(false) const [adjustLoading, setAdjustLoading] = useState(false)
const [adjustSubmitting, setAdjustSubmitting] = useState(false) const [adjustSubmitting, setAdjustSubmitting] = useState(false)
const [adjustWallet, setAdjustWallet] = useState<WalletAccount | null>(null) const [adjustWallet, setAdjustWallet] = useState<WalletAccount | null>(null)
const [editProduct, setEditProduct] = useState<ProductCatalogItem | null>(null)
const [editProductOpen, setEditProductOpen] = useState(false)
const [editProductSubmitting, setEditProductSubmitting] = useState(false)
const [createForm] = Form.useForm() const [createForm] = Form.useForm()
const [settingsForm] = Form.useForm() const [settingsForm] = Form.useForm()
const [adjustForm] = Form.useForm() const [adjustForm] = Form.useForm()
const [editProductForm] = Form.useForm()
const load = useCallback(async (page = data.page, size = data.size) => { const load = useCallback(async (page = data.page, size = data.size) => {
setLoading(true) setLoading(true)
@@ -184,6 +188,42 @@ export default function PlatformMerchants() {
} }
} }
const openProductEdit = (record: ProductCatalogItem) => {
setEditProduct(record)
editProductForm.resetFields()
editProductForm.setFieldsValue({
display_name: record.display_name,
price_amount: record.price_amount,
cost_amount: record.cost_amount,
stock: record.stock,
status: record.status,
})
setEditProductOpen(true)
}
const submitProductEdit = async () => {
if (!selectedMerchant || !editProduct) return
const values = await editProductForm.validateFields()
setEditProductSubmitting(true)
try {
await platformApi.updateMerchantProduct(selectedMerchant.id, editProduct.id, {
display_name: values.display_name,
price_amount: Number(values.price_amount || 0),
cost_amount: Number(values.cost_amount || 0),
stock: Number(values.stock ?? -1),
status: values.status,
})
message.success('商品已更新')
setEditProductOpen(false)
const productsData = await platformApi.merchantProducts(selectedMerchant.id)
setMerchantProducts(productsData || [])
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败')
} finally {
setEditProductSubmitting(false)
}
}
const columns: ColumnsType<Merchant> = [ const columns: ColumnsType<Merchant> = [
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' }, { title: 'ID', dataIndex: 'id', width: 70, align: 'center' },
{ title: '编码', dataIndex: 'code', width: 160, render: (v) => <Typography.Text code copyable={{ text: v }}>{v}</Typography.Text> }, { title: '编码', dataIndex: 'code', width: 160, render: (v) => <Typography.Text code copyable={{ text: v }}>{v}</Typography.Text> },
@@ -515,6 +555,14 @@ export default function PlatformMerchants() {
width: 80, width: 80,
render: (v) => (v === 'active' ? <Tag color="green"></Tag> : <Tag></Tag>), render: (v) => (v === 'active' ? <Tag color="green"></Tag> : <Tag></Tag>),
}, },
{
title: '操作',
key: 'action',
width: 60,
render: (_, record: ProductCatalogItem) => (
<Button type="link" size="small" onClick={() => openProductEdit(record)}></Button>
),
},
]} ]}
/> />
</Checkbox.Group> </Checkbox.Group>
@@ -524,6 +572,40 @@ export default function PlatformMerchants() {
</Space> </Space>
</Spin> </Spin>
</Modal> </Modal>
<Modal
title={editProduct ? `编辑商品:${editProduct.sku}` : '编辑商品'}
open={editProductOpen}
onOk={submitProductEdit}
onCancel={() => setEditProductOpen(false)}
destroyOnClose
width={560}
confirmLoading={editProductSubmitting}
okText="保存"
>
<Form form={editProductForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="display_name" label="展示名">
<Input maxLength={160} />
</Form.Item>
<Space size="middle" style={{ width: '100%' }}>
<Form.Item name="price_amount" label="售价(积分)" rules={[{ required: true }]} style={{ width: 160 }}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="cost_amount" label="成本(积分)" rules={[{ required: true }]} style={{ width: 160 }}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="stock" label="库存(-1 无限)" style={{ width: 160 }}>
<InputNumber min={-1} precision={0} style={{ width: '100%' }} />
</Form.Item>
</Space>
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
<Select options={[{ value: 'active', label: '上架' }, { value: 'inactive', label: '下架' }]} />
</Form.Item>
<Typography.Text type="secondary">
/
</Typography.Text>
</Form>
</Modal>
</div> </div>
) )
} }