diff --git a/backend/internal/handler/merchant.go b/backend/internal/handler/merchant.go index fd09359..e8947e0 100644 --- a/backend/internal/handler/merchant.go +++ b/backend/internal/handler/merchant.go @@ -214,20 +214,57 @@ func (h *MerchantHandler) ListWalletLedger(c *gin.Context) { response.Page(c, list, total, page, size) } +// AdminGetWallet 供平台管理员查看指定商户的钱包。 +func (h *MerchantHandler) AdminGetWallet(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + wallet, err := h.fulfillmentSvc.GetWallet(uint(id)) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.OK(c, wallet) +} + +// AdminListWalletLedger 供平台管理员查看指定商户的积分流水。 +func (h *MerchantHandler) AdminListWalletLedger(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } + page, size := pageParams(c) + list, total, err := h.fulfillmentSvc.ListWalletLedger(uint(id), page, size, c.Query("reference_no"), c.Query("type")) + if err != nil { + response.ServerError(c, err.Error()) + return + } + response.Page(c, list, total, page, size) +} + type walletAdjustReq struct { Amount int64 `json:"amount" binding:"required"` IdempotencyKey string `json:"idempotency_key" binding:"required"` Note string `json:"note"` } -func (h *MerchantHandler) AdjustWallet(c *gin.Context) { +// AdminAdjustWallet 供平台管理员手动调整指定商户的积分,商户侧无任何调账入口。 +func (h *MerchantHandler) AdminAdjustWallet(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if id == 0 { + response.BadRequest(c, "商户 ID 无效") + return + } var req walletAdjustReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "参数错误:amount 与 idempotency_key 必填") return } wallet, err := h.fulfillmentSvc.AdjustWallet(service.WalletAdjustInput{ - MerchantID: middleware.GetMerchantID(c), + MerchantID: uint(id), ActorUserID: middleware.GetUserID(c), Amount: req.Amount, IdempotencyKey: req.IdempotencyKey, diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 64fc585..19798c1 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -126,7 +126,6 @@ func Setup(h *Handlers) *gin.Engine { merchant.POST("/orders/:order_no/delivery-link/restore", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.RestoreDeliveryLink) 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) merchant.GET("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), h.Merchant.ListAPIClients) merchant.POST("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateAPIClient) merchant.PATCH("/api-clients/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateAPIClientStatus) @@ -151,6 +150,9 @@ func Setup(h *Handlers) *gin.Engine { 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("/platform/merchants/:id/wallet", h.Merchant.AdminGetWallet) + admin.GET("/platform/merchants/:id/wallet/ledger", h.Merchant.AdminListWalletLedger) + admin.POST("/platform/merchants/:id/wallet/adjust", h.Merchant.AdminAdjustWallet) } } } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f8c3bba..b778caa 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -79,8 +79,6 @@ export const merchantApi = { request.get('/merchant/wallet').then((r) => r.data.data as WalletAccount), ledger: (params?: Record) => request.get('/merchant/wallet/ledger', { params }).then((r) => r.data.data as PageResult), - adjustWallet: (data: { amount: number; idempotency_key: string; note?: string }) => - request.post('/merchant/wallet/adjust', data).then((r) => r.data.data as WalletAccount), apiClients: () => request.get('/merchant/api-clients').then((r) => r.data.data as ApiClient[]), createApiClient: (data: { @@ -154,4 +152,10 @@ export const platformApi = { request.get(`/platform/merchants/${merchantId}/products`).then((r) => r.data.data as ProductCatalogItem[]), assignProducts: (merchantId: number, catalogIds: number[]) => request.post(`/platform/merchants/${merchantId}/products/assign`, { catalog_ids: catalogIds }).then((r) => r.data.data as { assigned: number }), + wallet: (merchantId: number) => + request.get(`/platform/merchants/${merchantId}/wallet`).then((r) => r.data.data as WalletAccount), + walletLedger: (merchantId: number, params?: Record) => + request.get(`/platform/merchants/${merchantId}/wallet/ledger`, { params }).then((r) => r.data.data as PageResult), + adjustWallet: (merchantId: number, data: { amount: number; idempotency_key: string; note?: string }) => + request.post(`/platform/merchants/${merchantId}/wallet/adjust`, data).then((r) => r.data.data as WalletAccount), } diff --git a/frontend/src/pages/MerchantCenter.tsx b/frontend/src/pages/MerchantCenter.tsx index a85b06a..b421f64 100644 --- a/frontend/src/pages/MerchantCenter.tsx +++ b/frontend/src/pages/MerchantCenter.tsx @@ -108,7 +108,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer const [loading, setLoading] = useState(false) const [productOpen, setProductOpen] = useState(false) const [editingProduct, setEditingProduct] = useState(null) - const [walletOpen, setWalletOpen] = useState(false) const [apiClientOpen, setApiClientOpen] = useState(false) const [apiCredential, setApiCredential] = useState(null) const [callbackCredential, setCallbackCredential] = useState(null) @@ -118,7 +117,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer const [testOrderProductsLoading, setTestOrderProductsLoading] = useState(false) const [testOrderResult, setTestOrderResult] = useState(null) const [productForm] = Form.useForm() - const [walletForm] = Form.useForm() const [walletFilterForm] = Form.useForm() const [apiClientForm] = Form.useForm() const [callbackForm] = Form.useForm() @@ -331,23 +329,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer } } - const submitWalletAdjust = async () => { - const values = await walletForm.validateFields() - try { - const walletData = await merchantApi.adjustWallet({ - amount: values.amount, - idempotency_key: values.idempotency_key, - note: values.note, - }) - setWallet(walletData) - setWalletOpen(false) - message.success('钱包已调整') - loadWallet() - } catch (e) { - message.error(e instanceof Error ? e.message : '调整失败') - } - } - const handleWalletFilter = async () => { const values = await walletFilterForm.validateFields() loadWallet(1, ledger.size, merchantRole, { @@ -496,17 +477,63 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer } const productColumns: ColumnsType = [ - { title: 'SKU', dataIndex: 'sku', width: 200, ellipsis: true, render: (v) => {v} }, - { title: '名称', dataIndex: 'display_name', width: 200, ellipsis: true, render: (_, r) => r.display_name || r.product?.name || '-' }, - { title: '目录编码', dataIndex: ['product', 'code'], width: 140, ellipsis: true, render: (_, r) => r.product?.code || '-' }, - { title: '售价', dataIndex: 'price_amount', width: 100, render: money }, - { title: '成本', dataIndex: 'cost_amount', width: 100, render: money }, - { title: '库存', dataIndex: 'stock', width: 80, render: (v) => (v < 0 ? '无限' : v) }, - { title: '状态', dataIndex: 'status', width: 80, render: productStatusTag }, + { + title: '商户 SKU', + dataIndex: 'sku', + width: 180, + ellipsis: true, + render: (v) => {v}, + }, + { + title: '商品名称', + dataIndex: 'display_name', + width: 220, + ellipsis: true, + render: (_, r) => ( +
+
{r.display_name || r.product?.name || '-'}
+ {r.product?.category && ( + {r.product.category} + )} +
+ ), + }, + { + title: '目录编码', + dataIndex: ['product', 'code'], + width: 150, + ellipsis: true, + render: (_, r) => r.product?.code ? {r.product.code} : '-', + }, + { + title: '对外售价', + dataIndex: 'price_amount', + width: 120, + render: (v) => {money(v)}, + }, + { + title: '成本价', + dataIndex: 'cost_amount', + width: 120, + render: (v) => {money(v)}, + }, + { + title: '当前库存', + dataIndex: 'stock', + width: 110, + render: (v) => (v < 0 ? 无限库存 : v === 0 ? 已缺货 : `${v} 件`), + }, + { + title: '状态', + dataIndex: 'status', + width: 100, + render: productStatusTag, + }, { title: '操作', key: 'action', - width: 80, + width: 90, + fixed: 'right', render: (_, record) => canManage ? ( ) : '-', @@ -609,16 +636,27 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer const productContent = ( - - 通用商品以商户 SKU 对外销售。 - {canManage && } - + +
+
+ 商品上架与定额售价管理 + + 通用皮肤与游戏道具通过唯一的商户 SKU 进行对外销售与统一发货。 + +
+ {canManage && ( + + )} +
+
@@ -661,24 +699,10 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer {(wallet?.available_balance ?? 0).toLocaleString('zh-CN')} 积分 - {canFinance && ( -
- 手动划拨/调账 - -
- )} +
+ 充值/调账 + 仅平台管理员可操作 +
@@ -996,20 +1020,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer )} - setWalletOpen(false)} destroyOnClose> -
- - - - - - - - - - -
- setApiClientOpen(false)} destroyOnClose>
0 ? '+' : '' - return `${prefix}${money(amount)}` + if (amount > 0) { + return +{money(amount)} + } + if (amount < 0) { + return {money(amount)} + } + return {money(amount)} } function productStatusTag(value: string) { diff --git a/frontend/src/pages/PlatformMerchants.tsx b/frontend/src/pages/PlatformMerchants.tsx index 9eddeab..4776a9e 100644 --- a/frontend/src/pages/PlatformMerchants.tsx +++ b/frontend/src/pages/PlatformMerchants.tsx @@ -14,11 +14,11 @@ import { Typography, message, } from 'antd' -import { GiftOutlined, PlusOutlined, ReloadOutlined, TeamOutlined } from '@ant-design/icons' +import { GiftOutlined, PlusOutlined, ReloadOutlined, TeamOutlined, WalletOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import { useNavigate } from 'react-router-dom' import { platformApi } from '../api' -import type { Merchant, MerchantMember, PageResult, ProductCatalogItem } from '../types' +import type { Merchant, MerchantMember, PageResult, ProductCatalogItem, WalletAccount } from '../types' import { formatDateTime } from '../utils/time' import { PageHeader } from '../components/PageHeader' @@ -51,9 +51,14 @@ export default function PlatformMerchants() { const [merchantProducts, setMerchantProducts] = useState([]) const [selectedCatalogIds, setSelectedCatalogIds] = useState([]) const [selectedMerchant, setSelectedMerchant] = useState(null) + const [adjustOpen, setAdjustOpen] = useState(false) + const [adjustLoading, setAdjustLoading] = useState(false) + const [adjustSubmitting, setAdjustSubmitting] = useState(false) + const [adjustWallet, setAdjustWallet] = useState(null) const [createForm] = Form.useForm() const [settingsForm] = Form.useForm() const [memberForm] = Form.useForm() + const [adjustForm] = Form.useForm() const load = useCallback(async (page = data.page, size = data.size) => { setLoading(true) @@ -128,6 +133,44 @@ export default function PlatformMerchants() { } } + const openAdjust = async (record: Merchant) => { + setSelectedMerchant(record) + setAdjustOpen(true) + setAdjustLoading(true) + setAdjustWallet(null) + adjustForm.resetFields() + try { + const walletData = await platformApi.wallet(record.id) + setAdjustWallet(walletData) + adjustForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` }) + } catch (e) { + message.error(e instanceof Error ? e.message : '加载钱包失败') + } finally { + setAdjustLoading(false) + } + } + + const submitAdjust = async () => { + if (!selectedMerchant) return + const values = await adjustForm.validateFields() + setAdjustSubmitting(true) + try { + const walletData = await platformApi.adjustWallet(selectedMerchant.id, { + amount: Number(values.amount), + idempotency_key: values.idempotency_key, + note: values.note, + }) + setAdjustWallet(walletData) + message.success('积分已调整') + setAdjustOpen(false) + load() + } catch (e) { + message.error(e instanceof Error ? e.message : '调整失败') + } finally { + setAdjustSubmitting(false) + } + } + const openAssign = async (record: Merchant) => { setSelectedMerchant(record) setAssignOpen(true) @@ -255,6 +298,14 @@ export default function PlatformMerchants() { > 设置 +