重构: 手续费改为百分比/固定二选一,清理旧 Skin/Order/ShipLog 演示链路
手续费: - 新增 fee_type 字段(rate/fixed),百分比与固定金额二选一,不再叠加 - calculateServiceFee 按 fee_type 分支计算 - 商户创建/更新校验 fee_type,订单落库快照 fee_type - 前端表单改为下拉选择手续费类型,动态显示对应输入框 - 测试拆为 TestFeeRate + TestFeeFixed 清理旧链路: - 删除旧 Skin/Order/ShipLog 模型及 OrderService/SkinService - Dashboard 迁移到 FulfillmentService - 上游 /api/open/v1 改为基于 FulfillmentOrder 实现,接口契约不变 - 推送留痕改用 AuditLog,不再建 ShipLog 表 - 删除前端 Skins/Orders/ShipLogs/Distributors 页面及路由、菜单、API、类型 - 新增迁移 003: 添加 fee_type 列并 DROP 旧表
This commit is contained in:
@@ -26,14 +26,24 @@ const memberRoleOptions = [
|
||||
{ value: 'viewer', label: '只读' },
|
||||
]
|
||||
|
||||
const featureOptions = [
|
||||
{ value: 'products', label: '商品' },
|
||||
{ value: 'orders', label: '订单' },
|
||||
{ value: 'wallet', label: '钱包' },
|
||||
{ value: 'api', label: 'API' },
|
||||
{ value: 'callbacks', label: '回调' },
|
||||
]
|
||||
|
||||
export default function PlatformMerchants() {
|
||||
const navigate = useNavigate()
|
||||
const [data, setData] = useState<PageResult<Merchant>>({ list: [], total: 0, page: 1, size: 10 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [memberOpen, setMemberOpen] = useState(false)
|
||||
const [selectedMerchant, setSelectedMerchant] = useState<Merchant | null>(null)
|
||||
const [createForm] = Form.useForm()
|
||||
const [settingsForm] = Form.useForm()
|
||||
const [memberForm] = Form.useForm()
|
||||
|
||||
const load = useCallback(async (page = data.page, size = data.size) => {
|
||||
@@ -55,7 +65,13 @@ export default function PlatformMerchants() {
|
||||
const submitCreate = async () => {
|
||||
const values = await createForm.validateFields()
|
||||
try {
|
||||
await platformApi.createMerchant(values)
|
||||
await platformApi.createMerchant({
|
||||
...values,
|
||||
features: featureListToText(values.features),
|
||||
fee_type: values.fee_type,
|
||||
fee_rate_bp: Number(values.fee_rate_bp || 0),
|
||||
fee_fixed_amount: yuanToCents(values.fee_fixed_yuan),
|
||||
})
|
||||
message.success('商户已创建')
|
||||
setCreateOpen(false)
|
||||
createForm.resetFields()
|
||||
@@ -65,6 +81,28 @@ export default function PlatformMerchants() {
|
||||
}
|
||||
}
|
||||
|
||||
const submitSettings = async () => {
|
||||
if (!selectedMerchant) return
|
||||
const values = await settingsForm.validateFields()
|
||||
try {
|
||||
await platformApi.updateMerchant(selectedMerchant.id, {
|
||||
name: values.name,
|
||||
status: values.status,
|
||||
contact_name: values.contact_name,
|
||||
contact_info: values.contact_info,
|
||||
features: featureListToText(values.features),
|
||||
fee_type: values.fee_type,
|
||||
fee_rate_bp: Number(values.fee_rate_bp || 0),
|
||||
fee_fixed_amount: yuanToCents(values.fee_fixed_yuan),
|
||||
})
|
||||
message.success('商户设置已更新')
|
||||
setSettingsOpen(false)
|
||||
load()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
const submitMember = async () => {
|
||||
if (!selectedMerchant) return
|
||||
const values = await memberForm.validateFields()
|
||||
@@ -87,6 +125,27 @@ export default function PlatformMerchants() {
|
||||
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contact_name', width: 120, render: (v) => v || '-' },
|
||||
{ title: '联系方式', dataIndex: 'contact_info', width: 160, render: (v) => v || '-' },
|
||||
{
|
||||
title: '手续费',
|
||||
key: 'fee',
|
||||
width: 160,
|
||||
render: (_, r) =>
|
||||
r.fee_type === 'fixed'
|
||||
? `固定 ¥${centsToYuan(r.fee_fixed_amount).toFixed(2)}/单`
|
||||
: `${(r.fee_rate_bp / 100).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
title: '功能',
|
||||
dataIndex: 'features',
|
||||
width: 220,
|
||||
render: (v) => (
|
||||
<Space size={4} wrap>
|
||||
{featuresToList(v).map((feature) => (
|
||||
<Tag key={feature}>{featureText(feature)}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (v) => v === 'active' ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag> },
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 160, render: (v) => dayjs(v).format('YYYY-MM-DD HH:mm') },
|
||||
{
|
||||
@@ -106,6 +165,22 @@ export default function PlatformMerchants() {
|
||||
>
|
||||
进入
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSelectedMerchant(record)
|
||||
settingsForm.setFieldsValue({
|
||||
...record,
|
||||
features: featuresToList(record.features),
|
||||
fee_type: record.fee_type || 'rate',
|
||||
fee_fixed_yuan: centsToYuan(record.fee_fixed_amount),
|
||||
})
|
||||
setSettingsOpen(true)
|
||||
}}
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@@ -142,6 +217,12 @@ export default function PlatformMerchants() {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
createForm.resetFields()
|
||||
createForm.setFieldsValue({
|
||||
features: featureOptions.map((item) => item.value),
|
||||
fee_type: 'rate',
|
||||
fee_rate_bp: 0,
|
||||
fee_fixed_yuan: 0,
|
||||
})
|
||||
setCreateOpen(true)
|
||||
}}
|
||||
>
|
||||
@@ -166,7 +247,7 @@ export default function PlatformMerchants() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal title="新增商户" open={createOpen} onOk={submitCreate} onCancel={() => setCreateOpen(false)} destroyOnClose>
|
||||
<Modal title="新增商户" open={createOpen} onOk={submitCreate} onCancel={() => setCreateOpen(false)} destroyOnClose width={680}>
|
||||
<Form form={createForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="code" label="商户编码" rules={[{ required: true }]}>
|
||||
<Input placeholder="lower-case-code" />
|
||||
@@ -174,15 +255,92 @@ export default function PlatformMerchants() {
|
||||
<Form.Item name="name" label="商户名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="owner_user_id" label="负责人用户 ID" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="contact_name" label="联系人">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Form.Item name="owner_username" label="负责人用户名" rules={[{ required: true }]} style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="owner_password" label="负责人密码" rules={[{ required: true, min: 6 }]} style={{ width: 300 }}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Form.Item name="owner_nickname" label="负责人昵称" style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="contact_name" label="联系人" style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="contact_info" label="联系方式">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="features" label="开通功能" rules={[{ required: true }]}>
|
||||
<Select mode="multiple" options={featureOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="fee_type" label="手续费类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'rate', label: '按百分比(每单按订单金额比例收取)' },
|
||||
{ value: 'fixed', label: '按固定金额(每单固定手续费)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('fee_type') === 'fixed' ? (
|
||||
<Form.Item name="fee_fixed_yuan" label="每单固定手续费(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item name="fee_rate_bp" label="手续费比例 BP(1BP=0.01%,如 250=2.5%)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={10000} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title={selectedMerchant ? `商户设置:${selectedMerchant.name}` : '商户设置'} open={settingsOpen} onOk={submitSettings} onCancel={() => setSettingsOpen(false)} destroyOnClose width={680}>
|
||||
<Form form={settingsForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="name" label="商户名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Form.Item name="status" label="状态" style={{ width: 300 }}>
|
||||
<Select options={[{ value: 'active', label: '启用' }, { value: 'disabled', label: '禁用' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="contact_name" label="联系人" style={{ width: 300 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="contact_info" label="联系方式">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="features" label="开通功能" rules={[{ required: true }]}>
|
||||
<Select mode="multiple" options={featureOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="fee_type" label="手续费类型" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'rate', label: '按百分比(每单按订单金额比例收取)' },
|
||||
{ value: 'fixed', label: '按固定金额(每单固定手续费)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('fee_type') === 'fixed' ? (
|
||||
<Form.Item name="fee_fixed_yuan" label="每单固定手续费(元)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item name="fee_rate_bp" label="手续费比例 BP(1BP=0.01%,如 250=2.5%)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={10000} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -208,3 +366,26 @@ export default function PlatformMerchants() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function featuresToList(features?: string) {
|
||||
return (features || '')
|
||||
.split(/[,\s]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function featureListToText(features?: string[]) {
|
||||
return (features || []).join(',')
|
||||
}
|
||||
|
||||
function featureText(feature: string) {
|
||||
return featureOptions.find((item) => item.value === feature)?.label || feature
|
||||
}
|
||||
|
||||
function yuanToCents(value?: number | null) {
|
||||
return Math.round(Number(value || 0) * 100)
|
||||
}
|
||||
|
||||
function centsToYuan(value?: number | null) {
|
||||
return Number(((value || 0) / 100).toFixed(2))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user