Files
affiliate_dash/frontend/src/pages/PlatformMerchants.tsx
T
yml2213 0de0ad9e9d 货币体系: 全平台统一使用积分(POINT)替代人民币(CNY)
后端:
- 模型 currency 默认值 CNY→POINT (MerchantProduct/WalletAccount/FulfillmentOrder)
- DashboardStats TotalSales/TotalFees 从 float64 改为 int64,去掉 /100.0 转换
- 上游 OpenOrderQuery.Amount 从 float64 改为 int64,直接返回积分整数
- 钱包/商品创建时 currency 默认 POINT

前端:
- 去掉 centsToYuan/yuanToCents 转换函数,金额直接用整数积分
- money() 显示从 ¥X.XX 改为 X 积分
- 商品售价/成本表单字段名改回 price_amount/cost_amount,precision 改 0
- 钱包调整表单 amount_yuan→amount,precision 改 0
- 手续费固定金额表单 precision 改 0,label 改积分
- 币种选项 CNY→POINT
- Dashboard 成交金额/手续费 suffix 元→积分,去掉 precision

数据库:
- 新增迁移 004: currency 默认值 CNY→POINT,存量数据更新
2026-07-30 14:30:34 +08:00

384 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import {
Button,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Table,
Tag,
Typography,
message,
} from 'antd'
import { PlusOutlined, ReloadOutlined, TeamOutlined } from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import dayjs from 'dayjs'
import { useNavigate } from 'react-router-dom'
import { platformApi } from '../api'
import type { Merchant, MerchantMember, PageResult } from '../types'
const memberRoleOptions = [
{ value: 'owner', label: '负责人' },
{ value: 'operator', label: '运营' },
{ value: 'finance', label: '财务' },
{ 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) => {
setLoading(true)
try {
const result = await platformApi.merchants({ page, size })
setData(result)
} catch (e) {
message.error(e instanceof Error ? e.message : '加载失败')
} finally {
setLoading(false)
}
}, [data.page, data.size])
useEffect(() => {
load()
}, [load])
const submitCreate = async () => {
const values = await createForm.validateFields()
try {
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: Number(values.fee_fixed_amount || 0),
})
message.success('商户已创建')
setCreateOpen(false)
createForm.resetFields()
load()
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
}
}
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: Number(values.fee_fixed_amount || 0),
})
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()
try {
await platformApi.addMember(selectedMerchant.id, {
user_id: values.user_id,
role: values.role as MerchantMember['role'],
is_default: values.is_default,
})
message.success('成员已添加')
setMemberOpen(false)
} catch (e) {
message.error(e instanceof Error ? e.message : '添加失败')
}
}
const columns: ColumnsType<Merchant> = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '编码', dataIndex: 'code', width: 180, render: (v) => <Typography.Text code copyable>{v}</Typography.Text> },
{ 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'
? `固定 ${r.fee_fixed_amount} 积分/单`
: `${(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') },
{
title: '操作',
key: 'action',
width: 190,
render: (_, record) => (
<Space size={0}>
<Button
type="link"
size="small"
onClick={() => {
localStorage.setItem('merchant_id', String(record.id))
message.success('当前商户已切换')
navigate('/merchant-center')
}}
>
进入
</Button>
<Button
type="link"
size="small"
onClick={() => {
setSelectedMerchant(record)
settingsForm.setFieldsValue({
...record,
features: featuresToList(record.features),
fee_type: record.fee_type || 'rate',
fee_fixed_amount: record.fee_fixed_amount,
})
setSettingsOpen(true)
}}
>
设置
</Button>
<Button
type="link"
size="small"
icon={<TeamOutlined />}
onClick={() => {
setSelectedMerchant(record)
memberForm.resetFields()
memberForm.setFieldsValue({ role: 'operator', is_default: false })
setMemberOpen(true)
}}
>
添加成员
</Button>
</Space>
),
},
]
return (
<div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}>
商户管理
</Typography.Title>
<Typography.Text type="secondary">平台管理员维护租户与授权关系。</Typography.Text>
</div>
<Space>
<Button icon={<ReloadOutlined />} onClick={() => load()}>
刷新
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
createForm.resetFields()
createForm.setFieldsValue({
features: featureOptions.map((item) => item.value),
fee_type: 'rate',
fee_rate_bp: 0,
fee_fixed_amount: 0,
})
setCreateOpen(true)
}}
>
新增商户
</Button>
</Space>
</Space>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={data.list}
tableLayout="fixed"
pagination={{
current: data.page,
pageSize: data.size,
total: data.total,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
onChange: load,
}}
/>
<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" />
</Form.Item>
<Form.Item name="name" label="商户名称" rules={[{ required: true }]}>
<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_amount" label="每单固定手续费(积分)" rules={[{ required: true }]}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item>
) : (
<Form.Item name="fee_rate_bp" label="手续费比例 BP1BP=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_amount" label="每单固定手续费(积分)" rules={[{ required: true }]}>
<InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item>
) : (
<Form.Item name="fee_rate_bp" label="手续费比例 BP1BP=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={memberOpen}
onOk={submitMember}
onCancel={() => setMemberOpen(false)}
destroyOnClose
>
<Form form={memberForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="user_id" label="用户 ID" rules={[{ required: true }]}>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
<Select options={memberRoleOptions} />
</Form.Item>
<Form.Item name="is_default" label="默认商户">
<Select options={[{ value: true, label: '是' }, { value: false, label: '否' }]} />
</Form.Item>
</Form>
</Modal>
</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
}