feat(distribution): 独立分销后台账号体系、多租户隔离与主站后台分销管理GUI
- 独立账号鉴权:分销后台支持独立登录页面、Bearer+Cookie 双模鉴权与租户资料接口 - 多租户隔离:分销商成员强制锁定自身站点数据范围,防止横向越权;平台超管享有全局视界 - 一键开通分销商:平台端支持一键录入站点、管理员账号、分配角色并原子化初始化佣金规则 - 主后台分销分组:在主站后台增加「分销管理」导航分组,提供站点管理、规则配置、订单监控与结算管理 GUI - 门禁与联调:补充完整单测,全量代码门禁 check.sh 100% 通过,dev.sh 支持前后端与分销后台一键同启
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, Form, Input, Button, Alert, Typography, Space } from 'antd'
|
||||
import { UserOutlined, LockOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
|
||||
import { login, type CurrentMember } from '../api'
|
||||
|
||||
interface LoginProps {
|
||||
onSuccess: (member: CurrentMember) => void
|
||||
}
|
||||
|
||||
export default function Login({ onSuccess }: LoginProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
const handleSubmit = async (values: { username: string; password: string }) => {
|
||||
setLoading(true)
|
||||
setErrorMsg('')
|
||||
try {
|
||||
const res = await login(values.username.trim(), values.password)
|
||||
onSuccess(res.member)
|
||||
} catch (err: unknown) {
|
||||
const errObj = err as { response?: { data?: { message?: string } } }
|
||||
const msg = errObj?.response?.data?.message || '登录失败,请检查用户名和密码'
|
||||
setErrorMsg(msg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<Card className="login-card" bordered={false}>
|
||||
<div className="login-header">
|
||||
<div className="login-logo">
|
||||
<SafetyCertificateOutlined style={{ fontSize: 32, color: '#1677ff' }} />
|
||||
</div>
|
||||
<Typography.Title level={3} style={{ margin: '12px 0 4px' }}>
|
||||
HFB 分销系统
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
独立分销商 · 多站点综合管理后台
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{errorMsg && (
|
||||
<Alert
|
||||
type="error"
|
||||
message={errorMsg}
|
||||
showIcon
|
||||
closable
|
||||
onClose={() => setErrorMsg('')}
|
||||
style={{ marginBottom: 20 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form layout="vertical" onFinish={handleSubmit} size="large">
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入分销管理员账号' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="分销账号 / 管理员用户名"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="登录密码"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginTop: 24 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
style={{ height: 44, fontWeight: 500 }}
|
||||
>
|
||||
登录分销后台
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="login-footer">
|
||||
<Space split={<span style={{ color: '#d9d9d9' }}>·</span>}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
租户多维隔离
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
独立结算分成
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
安全双模鉴权
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -26,8 +26,17 @@ export default function Rules() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listSites().then(setSites).catch(() => message.error('站点加载失败'))
|
||||
void load(undefined)
|
||||
listSites()
|
||||
.then((rows) => {
|
||||
setSites(rows)
|
||||
if (rows.length === 1) {
|
||||
setSiteID(rows[0].id)
|
||||
void load(rows[0].id)
|
||||
} else {
|
||||
void load(undefined)
|
||||
}
|
||||
})
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
}, [])
|
||||
|
||||
async function toggleStatus(rule: Rule) {
|
||||
@@ -49,6 +58,7 @@ export default function Rules() {
|
||||
allowClear
|
||||
placeholder="分销网站"
|
||||
style={{ width: 200 }}
|
||||
value={siteID}
|
||||
options={sites.map((site) => ({ label: site.site_name, value: site.id }))}
|
||||
onChange={(value) => {
|
||||
setSiteID(value)
|
||||
|
||||
@@ -1,39 +1,336 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Typography, Space, Tag, message } from 'antd'
|
||||
import { listSites, type Site } from '../api'
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Typography,
|
||||
Space,
|
||||
Tag,
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Switch,
|
||||
message,
|
||||
Divider,
|
||||
Descriptions,
|
||||
} from 'antd'
|
||||
import { PlusOutlined, ShopOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { listSites, onboardSite, type Site, type OnboardSitePayload, type OnboardSiteResult } from '../api'
|
||||
|
||||
export default function Sites() {
|
||||
export default function Sites({ isSuperadmin = true }: { isSuperadmin?: boolean }) {
|
||||
const [items, setItems] = useState<Site[]>([])
|
||||
useEffect(() => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [successResult, setSuccessResult] = useState<OnboardSiteResult | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadData = () => {
|
||||
setLoading(true)
|
||||
listSites()
|
||||
.then(setItems)
|
||||
.catch(() => message.error('站点加载失败'))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
const handleOpenModal = () => {
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
member_type: 'operator',
|
||||
distributor_receive_ratio: 3.5,
|
||||
platform_sell_ratio: 4.0,
|
||||
service_fee_value: 2.5,
|
||||
display_account_enabled: true,
|
||||
tax_enabled: false,
|
||||
tax_rate_bps: 100,
|
||||
})
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const handleOnboardSubmit = async (values: any) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const payload: OnboardSitePayload = {
|
||||
site_code: values.site_code.trim(),
|
||||
site_name: values.site_name.trim(),
|
||||
domain: values.domain.trim(),
|
||||
admin_username: values.admin_username.trim(),
|
||||
admin_password: values.admin_password,
|
||||
admin_nickname: values.admin_nickname?.trim() || undefined,
|
||||
member_type: values.member_type,
|
||||
display_account_enabled: values.display_account_enabled,
|
||||
tax_enabled: values.tax_enabled,
|
||||
tax_rate_bps: values.tax_enabled ? values.tax_rate_bps : 0,
|
||||
distributor_receive_ratio: Number(values.distributor_receive_ratio),
|
||||
platform_sell_ratio: Number(values.platform_sell_ratio),
|
||||
service_fee_value: Number(values.service_fee_value),
|
||||
}
|
||||
const res = await onboardSite(payload)
|
||||
message.success('分销站点及分销商管理员开通成功!')
|
||||
setModalOpen(false)
|
||||
setSuccessResult(res)
|
||||
loadData()
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || '开通分销商失败,请检查站点标识或管理员用户名是否重复'
|
||||
message.error(msg)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" className="page">
|
||||
<Typography.Title level={3}>分销网站</Typography.Title>
|
||||
<Space direction="vertical" className="page" size="large">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography.Title level={3} style={{ margin: 0 }}>
|
||||
分销网站管理
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
管理全平台合作分销站点、域名配置与分销商管理员账号
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{isSuperadmin && (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenModal}>
|
||||
开通分销商
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: '名称', dataIndex: 'site_name' },
|
||||
{ title: '编码', dataIndex: 'site_code' },
|
||||
{ title: '域名', dataIndex: 'domain' },
|
||||
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={v === 'active' ? 'green' : 'default'}>{v}</Tag> },
|
||||
{
|
||||
title: '站点名称',
|
||||
dataIndex: 'site_name',
|
||||
render: (v: string) => (
|
||||
<Space>
|
||||
<ShopOutlined style={{ color: '#1677ff' }} />
|
||||
<strong>{v}</strong>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: '站点标识', dataIndex: 'site_code', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '绑定域名', dataIndex: 'domain' },
|
||||
{
|
||||
title: '运行状态',
|
||||
dataIndex: 'status',
|
||||
render: (v: string) => (
|
||||
<Tag color={v === 'active' ? 'green' : 'default'}>
|
||||
{v === 'active' ? '正常运营' : v}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '账号展示',
|
||||
dataIndex: 'display_account_enabled',
|
||||
render: (v: boolean) => (v ? <Tag color="green">开启</Tag> : <Tag>关闭</Tag>),
|
||||
},
|
||||
{
|
||||
title: '税费',
|
||||
render: (_: unknown, row: Site) => (row.tax_enabled ? `${row.tax_rate_bps / 100}%` : '关闭'),
|
||||
title: '税费设定',
|
||||
render: (_: unknown, row: Site) =>
|
||||
row.tax_enabled ? <Tag color="orange">{row.tax_rate_bps / 100}%</Tag> : <Tag>不征税</Tag>,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
render: (v: string) => (v ? new Date(v).toLocaleDateString() : '-'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 开通分销商 Modal */}
|
||||
<Modal
|
||||
title="开通分销商 (一键初始化站点与账号)"
|
||||
open={modalOpen}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
footer={null}
|
||||
width={680}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleOnboardSubmit} style={{ marginTop: 16 }}>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
1. 站点基础信息
|
||||
</Typography.Title>
|
||||
<Space direction="horizontal" style={{ width: '100%' }} size="middle">
|
||||
<Form.Item
|
||||
label="站点名称"
|
||||
name="site_name"
|
||||
rules={[{ required: true, message: '请输入站点名称' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input placeholder="例如:广州游戏分销站" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="站点唯一标识"
|
||||
name="site_code"
|
||||
rules={[
|
||||
{ required: true, message: '请输入唯一代码' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '仅支持字母、数字与下划线' },
|
||||
]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input placeholder="例如:gz_dist" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Form.Item
|
||||
label="分销专属域名"
|
||||
name="domain"
|
||||
rules={[{ required: true, message: '请输入分销绑定域名' }]}
|
||||
>
|
||||
<Input placeholder="例如:gz.distribution.hao858.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
<Typography.Title level={5}>2. 分销商管理员账号</Typography.Title>
|
||||
<Space direction="horizontal" style={{ width: '100%' }} size="middle">
|
||||
<Form.Item
|
||||
label="登录用户名"
|
||||
name="admin_username"
|
||||
rules={[{ required: true, message: '请输入管理员账号' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input placeholder="例如:gz_admin" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="初始登录密码"
|
||||
name="admin_password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入登录密码' },
|
||||
{ min: 6, message: '密码至少6位' },
|
||||
]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input.Password placeholder="至少6位" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Space direction="horizontal" style={{ width: '100%' }} size="middle">
|
||||
<Form.Item label="管理员姓名/昵称" name="admin_nickname" style={{ flex: 1 }}>
|
||||
<Input placeholder="例如:张站长" />
|
||||
</Form.Item>
|
||||
<Form.Item label="初始角色权限" name="member_type" style={{ flex: 1 }}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '分销运营 (operator)', value: 'operator' },
|
||||
{ label: '分销财务 (finance)', value: 'finance' },
|
||||
{ label: '客服人员 (customer_service)', value: 'customer_service' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
<Typography.Title level={5}>3. 分销比例与佣金规则初始化</Typography.Title>
|
||||
<Space direction="horizontal" style={{ width: '100%' }} size="middle">
|
||||
<Form.Item
|
||||
label="分销商收号分成比例 (%)"
|
||||
name="distributor_receive_ratio"
|
||||
tooltip="分销模式下分销商收号收益率"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<InputNumber min={0} max={100} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="平台卖号分成比例 (%)"
|
||||
name="platform_sell_ratio"
|
||||
tooltip="平台卖号提取分成率"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<InputNumber min={0} max={100} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="单笔服务费 (元)"
|
||||
name="service_fee_value"
|
||||
tooltip="按单扣除的固定服务费"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<InputNumber min={0} step={0.5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Space direction="horizontal" size="large" style={{ marginTop: 8 }}>
|
||||
<Form.Item
|
||||
label="允许展示主站账号"
|
||||
name="display_account_enabled"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||||
</Form.Item>
|
||||
<Form.Item label="是否征税" name="tax_enabled" valuePropName="checked">
|
||||
<Switch checkedChildren="征税" unCheckedChildren="免税" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 24 }}>
|
||||
<Button onClick={() => setModalOpen(false)}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
立即开通并生成分销商
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 开通成功信息 Modal */}
|
||||
<Modal
|
||||
title="分销商开通成功"
|
||||
open={Boolean(successResult)}
|
||||
onOk={() => setSuccessResult(null)}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
width={560}
|
||||
>
|
||||
{successResult && (
|
||||
<div>
|
||||
<Typography.Paragraph type="secondary">
|
||||
分销商站点与管理账号已成功创建,请妥善保管并交付给分销合作伙伴:
|
||||
</Typography.Paragraph>
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label="分销站点">
|
||||
{successResult.site.site_name} ({successResult.site.site_code})
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分销域名">
|
||||
{successResult.site.domain}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分销后台登录账号">
|
||||
<strong>{successResult.member.username}</strong>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="所属角色">
|
||||
{successResult.member.member_type}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="初始规则版本">
|
||||
收号分成: {successResult.rule.distributor_receive_ratio}% · 卖号分成:{' '}
|
||||
{successResult.rule.platform_sell_ratio}% · 服务费:{' '}
|
||||
¥{successResult.rule.service_fee_value}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div style={{ marginTop: 16, textAlign: 'right' }}>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
const text = `分销站点: ${successResult.site.site_name}\n后台地址: http://${window.location.host}/login\n用户名: ${successResult.member.username}`
|
||||
navigator.clipboard?.writeText(text)
|
||||
message.success('已复制分销商开通信息到剪贴板')
|
||||
}}
|
||||
>
|
||||
复制开通信息
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user