dev
This commit is contained in:
@@ -138,7 +138,7 @@ const Wallet: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
手续费率 0.6%,实际到账 = 提现金额 - 手续费
|
||||
无手续费,实际到账 = 提现金额
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ const Withdrawals: React.FC = () => {
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: '提现金额', dataIndex: 'amount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '手续费', dataIndex: 'fee', width: 100, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '实际到账', dataIndex: 'actualAmount', width: 120, render: (v) => <span style={{ color: '#FF6B35', fontWeight: 600 }}>¥{Number(v).toFixed(2)}</span> },
|
||||
{ title: '开户银行', dataIndex: 'bankName', width: 120 },
|
||||
{ title: '银行账号', dataIndex: 'bankAccount', width: 160 },
|
||||
|
||||
@@ -70,6 +70,14 @@
|
||||
<text class="info-label">优惠券</text>
|
||||
<text class="info-value discount">-¥{{ order.couponDiscount }}</text>
|
||||
</view>
|
||||
<view class="info-row flex-between">
|
||||
<text class="info-label">软件服务费</text>
|
||||
<text class="info-value service-fee">-¥{{ order.serviceFee || 0 }}</text>
|
||||
</view>
|
||||
<view class="info-row flex-between">
|
||||
<text class="info-label">预计收入</text>
|
||||
<text class="info-value merchant-income">¥{{ order.merchantIncome || order.payAmount }}</text>
|
||||
</view>
|
||||
<view class="info-row flex-between total-row">
|
||||
<text class="info-label">实付金额</text>
|
||||
<text class="total-price">¥{{ order.payAmount }}</text>
|
||||
@@ -340,6 +348,10 @@ async function submitReject() {
|
||||
|
||||
.discount { color: #52c41a; }
|
||||
|
||||
.service-fee { color: #faad14; }
|
||||
|
||||
.merchant-income { color: #1890ff; font-weight: 600; }
|
||||
|
||||
.total-row {
|
||||
padding-top: 20rpx;
|
||||
border-top: 2rpx solid #eee;
|
||||
|
||||
@@ -15,8 +15,7 @@ import SystemSettings from '@/pages/SystemSettings';
|
||||
import FinanceSettlements from '@/pages/finance/Settlements';
|
||||
import FinanceWithdrawals from '@/pages/finance/Withdrawals';
|
||||
import FinanceEarnings from '@/pages/finance/Earnings';
|
||||
import ActivityList from '@/pages/activity/ActivityList';
|
||||
import ActivityDetail from '@/pages/activity/ActivityDetail';
|
||||
import InviteManage from '@/pages/InviteManage';
|
||||
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
@@ -43,11 +42,7 @@ const App: React.FC = () => (
|
||||
<Route path="withdrawals" element={<FinanceWithdrawals />} />
|
||||
<Route path="earnings" element={<FinanceEarnings />} />
|
||||
</Route>
|
||||
<Route path="activity">
|
||||
<Route index element={<Navigate to="/activity/list" replace />} />
|
||||
<Route path="list" element={<ActivityList />} />
|
||||
<Route path="detail/:id" element={<ActivityDetail />} />
|
||||
</Route>
|
||||
<Route path="invite" element={<InviteManage />} />
|
||||
<Route path="promotions" element={<Promotion />} />
|
||||
<Route path="settings" element={<SystemSettings />} />
|
||||
</Route>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export const getServiceFeeConfig = () => request.get('/admin/config/service-fee');
|
||||
|
||||
export const updateServiceFeeConfig = (rate: number) => request.put('/admin/config/service-fee', { rate });
|
||||
@@ -1,25 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 活动列表
|
||||
export const getActivities = (params?: { page?: number; pageSize?: number; type?: string; enabled?: boolean }) =>
|
||||
request.get('/admin/activity/list', { params });
|
||||
|
||||
// 活动详情
|
||||
export const getActivityById = (id: number) =>
|
||||
request.get(`/admin/activity/${id}`);
|
||||
|
||||
// 创建活动
|
||||
export const createActivity = (data: any) =>
|
||||
request.post('/admin/activity/create', data);
|
||||
|
||||
// 编辑活动
|
||||
export const updateActivity = (id: number, data: any) =>
|
||||
request.put(`/admin/activity/${id}/update`, data);
|
||||
|
||||
// 启用/停用活动
|
||||
export const toggleActivity = (id: number) =>
|
||||
request.put(`/admin/activity/${id}/toggle`);
|
||||
|
||||
// 邀请数据总览
|
||||
export const getInviteStats = () =>
|
||||
request.get('/admin/activity/invite/stats');
|
||||
@@ -47,3 +27,18 @@ export const rejectInviteWithdrawal = (id: number, reason: string) =>
|
||||
// 确认打款
|
||||
export const payInviteWithdrawal = (id: number) =>
|
||||
request.put(`/admin/activity/invite/withdrawals/${id}/pay`);
|
||||
|
||||
// 获取邀请活动配置
|
||||
export const getInviteConfig = () =>
|
||||
request.get('/admin/activity/invite/config');
|
||||
|
||||
// 更新邀请活动配置
|
||||
export const updateInviteConfig = (data: {
|
||||
firstOrderRate?: number;
|
||||
secondOrderRate?: number;
|
||||
minCashback?: number;
|
||||
maxCashback?: number;
|
||||
withdrawThreshold?: number;
|
||||
enabled?: boolean;
|
||||
}) =>
|
||||
request.put('/admin/activity/invite/config', data);
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
AuditOutlined,
|
||||
PayCircleOutlined,
|
||||
FundOutlined,
|
||||
RocketOutlined,
|
||||
OrderedListOutlined,
|
||||
TrophyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
@@ -39,14 +37,7 @@ const menuItems = [
|
||||
{ key: '/finance/earnings', icon: <FundOutlined />, label: '平台收益' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '/activity',
|
||||
icon: <RocketOutlined />,
|
||||
label: '活动管理',
|
||||
children: [
|
||||
{ key: '/activity/list', icon: <GiftOutlined />, label: '活动列表' },
|
||||
],
|
||||
},
|
||||
{ key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' },
|
||||
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
|
||||
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
|
||||
];
|
||||
@@ -59,7 +50,6 @@ const MainLayout: React.FC = () => {
|
||||
const openKeys = useMemo(() => {
|
||||
const path = location.pathname;
|
||||
if (path.startsWith('/finance')) return ['/finance'];
|
||||
if (path.startsWith('/activity')) return ['/activity'];
|
||||
return [];
|
||||
}, [location.pathname]);
|
||||
|
||||
|
||||
+111
-104
@@ -1,22 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Descriptions, Tag, Button, Tabs, Table, Modal, Input, Space, message, Statistic, Row, Col } from 'antd';
|
||||
import { Card, Tabs, Table, Tag, Button, Modal, Input, Space, message, Statistic, Row, Col, Switch, Form, InputNumber } from 'antd';
|
||||
import {
|
||||
getActivityById, createActivity, updateActivity,
|
||||
getInviteStats, getInviteRecords, getCashbackRecords,
|
||||
getInviteWithdrawals, approveInviteWithdrawal, rejectInviteWithdrawal, payInviteWithdrawal,
|
||||
} from '@/api/activity';
|
||||
getInviteConfig, updateInviteConfig,
|
||||
} from '@/api/invite';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const ActivityDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isCreate = id === 'create';
|
||||
|
||||
const [activity, setActivity] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(!isCreate);
|
||||
|
||||
// 邀请返现数据
|
||||
const InviteManage: React.FC = () => {
|
||||
const [stats, setStats] = useState<any>({});
|
||||
const [invitations, setInvitations] = useState<any[]>([]);
|
||||
const [invTotal, setInvTotal] = useState(0);
|
||||
@@ -37,59 +28,70 @@ const ActivityDetail: React.FC = () => {
|
||||
const [currentWdId, setCurrentWdId] = useState<number | null>(null);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
|
||||
const fetchActivity = async () => {
|
||||
if (isCreate) return;
|
||||
setLoading(true);
|
||||
const [config, setConfig] = useState<any>(null);
|
||||
const [configLoading, setConfigLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await getActivityById(Number(id));
|
||||
setActivity(res.data);
|
||||
const res = await getInviteStats();
|
||||
setStats(res.data || {});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const fetchInvitations = async () => {
|
||||
setInvLoading(true);
|
||||
try {
|
||||
const res = await getInviteRecords({ page: invPage, pageSize: 10 });
|
||||
setInvitations(res.data?.list || []);
|
||||
setInvTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setInvLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchInviteData = async () => {
|
||||
const fetchCashbacks = async () => {
|
||||
setCbLoading(true);
|
||||
try {
|
||||
const [statsRes, invRes, cbRes, wdRes] = await Promise.all([
|
||||
getInviteStats(),
|
||||
getInviteRecords({ page: invPage, pageSize: 10 }),
|
||||
getCashbackRecords({ page: cbPage, pageSize: 10 }),
|
||||
getInviteWithdrawals({ page: wdPage, pageSize: 10 }),
|
||||
]);
|
||||
setStats(statsRes.data || {});
|
||||
setInvitations(invRes.data?.list || []);
|
||||
setInvTotal(invRes.data?.total || 0);
|
||||
setCashbacks(cbRes.data?.list || []);
|
||||
setCbTotal(cbRes.data?.total || 0);
|
||||
setWithdrawals(wdRes.data?.list || []);
|
||||
setWdTotal(wdRes.data?.total || 0);
|
||||
} catch {}
|
||||
const res = await getCashbackRecords({ page: cbPage, pageSize: 10 });
|
||||
setCashbacks(res.data?.list || []);
|
||||
setCbTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setCbLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchActivity(); }, [id]);
|
||||
useEffect(() => {
|
||||
if (!isCreate && activity?.type === 'invite_cashback') fetchInviteData();
|
||||
}, [activity, invPage, cbPage, wdPage]);
|
||||
|
||||
// 创建固定活动
|
||||
const handleCreate = async (type: string) => {
|
||||
const typeMap: Record<string, { name: string; config: any }> = {
|
||||
invite_cashback: {
|
||||
name: '邀请返现',
|
||||
config: { firstOrderRate: 0.05, secondOrderRate: 0.005, minCashback: 0.01, maxCashback: 50, withdrawThreshold: 10 },
|
||||
},
|
||||
};
|
||||
const preset = typeMap[type];
|
||||
if (!preset) return;
|
||||
const fetchWithdrawals = async () => {
|
||||
setWdLoading(true);
|
||||
try {
|
||||
await createActivity({ name: preset.name, type, enabled: true, config: preset.config });
|
||||
message.success('创建成功');
|
||||
navigate('/activity/list');
|
||||
} catch {}
|
||||
const res = await getInviteWithdrawals({ page: wdPage, pageSize: 10 });
|
||||
setWithdrawals(res.data?.list || []);
|
||||
setWdTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setWdLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setConfigLoading(true);
|
||||
try {
|
||||
const res = await getInviteConfig();
|
||||
setConfig(res.data);
|
||||
if (res.data) {
|
||||
form.setFieldsValue(res.data);
|
||||
}
|
||||
} finally {
|
||||
setConfigLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchStats(); fetchConfig(); }, []);
|
||||
useEffect(() => { fetchInvitations(); }, [invPage]);
|
||||
useEffect(() => { fetchCashbacks(); }, [cbPage]);
|
||||
useEffect(() => { fetchWithdrawals(); }, [wdPage]);
|
||||
|
||||
const handleApprove = async (wid: number) => {
|
||||
try { await approveInviteWithdrawal(wid); message.success('审核通过'); fetchInviteData(); } catch {}
|
||||
try { await approveInviteWithdrawal(wid); message.success('审核通过'); fetchWithdrawals(); fetchStats(); } catch {}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
@@ -99,31 +101,31 @@ const ActivityDetail: React.FC = () => {
|
||||
message.success('已拒绝');
|
||||
setRejectModalOpen(false);
|
||||
setRejectReason('');
|
||||
fetchInviteData();
|
||||
fetchWithdrawals();
|
||||
fetchStats();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const handlePay = async (wid: number) => {
|
||||
try { await payInviteWithdrawal(wid); message.success('已确认打款'); fetchInviteData(); } catch {}
|
||||
try { await payInviteWithdrawal(wid); message.success('已确认打款'); fetchWithdrawals(); fetchStats(); } catch {}
|
||||
};
|
||||
|
||||
if (isCreate) {
|
||||
return (
|
||||
<Card title="创建活动">
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
<Card hoverable style={{ width: 300 }} onClick={() => handleCreate('invite_cashback')}>
|
||||
<Card.Meta title="邀请返现" description="邀请好友下单,邀请人获得返现奖励。首单5%,二单0.5%。" />
|
||||
</Card>
|
||||
</div>
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Button onClick={() => navigate('/activity/list')}>返回列表</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const handleToggle = async (enabled: boolean) => {
|
||||
try {
|
||||
await updateInviteConfig({ enabled });
|
||||
setConfig({ ...config, enabled });
|
||||
message.success(enabled ? '已启用' : '已停用');
|
||||
} catch {}
|
||||
};
|
||||
|
||||
if (loading) return <Card loading />;
|
||||
if (!activity) return <Card>活动不存在</Card>;
|
||||
const handleSaveConfig = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await updateInviteConfig(values);
|
||||
message.success('配置已保存');
|
||||
setConfig({ ...config, ...values });
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const invColumns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
@@ -181,34 +183,7 @@ const ActivityDetail: React.FC = () => {
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'info',
|
||||
label: '活动信息',
|
||||
children: (
|
||||
<Descriptions column={2} bordered>
|
||||
<Descriptions.Item label="活动名称">{activity.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="活动类型">邀请返现</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={activity.enabled ? 'green' : 'default'}>{activity.enabled ? '启用' : '停用'}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="活动时间">
|
||||
{activity.startTime
|
||||
? `${dayjs(activity.startTime).format('YYYY-MM-DD')} ~ ${dayjs(activity.endTime).format('YYYY-MM-DD')}`
|
||||
: '不限'}
|
||||
</Descriptions.Item>
|
||||
{activity.config && (
|
||||
<>
|
||||
<Descriptions.Item label="首单返现比例">{(activity.config.firstOrderRate * 100).toFixed(1)}%</Descriptions.Item>
|
||||
<Descriptions.Item label="二单返现比例">{(activity.config.secondOrderRate * 100).toFixed(1)}%</Descriptions.Item>
|
||||
<Descriptions.Item label="最低返现">¥{activity.config.minCashback}</Descriptions.Item>
|
||||
<Descriptions.Item label="最高返现">¥{activity.config.maxCashback}</Descriptions.Item>
|
||||
<Descriptions.Item label="提现门槛">¥{activity.config.withdrawThreshold}</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'stats',
|
||||
key: 'overview',
|
||||
label: '数据概览',
|
||||
children: (
|
||||
<div>
|
||||
@@ -238,13 +213,45 @@ const ActivityDetail: React.FC = () => {
|
||||
pagination={{ current: wdPage, total: wdTotal, pageSize: 10, onChange: setWdPage }} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '活动配置',
|
||||
children: (
|
||||
<Card loading={configLoading}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<span style={{ fontSize: 16, fontWeight: 600 }}>邀请返现活动</span>
|
||||
<Switch checked={config?.enabled} onChange={handleToggle} checkedChildren="启用" unCheckedChildren="停用" />
|
||||
</div>
|
||||
<Form form={form} layout="vertical" style={{ maxWidth: 500 }}>
|
||||
<Form.Item label="首单返现比例" name="firstOrderRate" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={1} step={0.01} addonAfter="%" style={{ width: '100%' }}
|
||||
formatter={v => `${(Number(v) * 100).toFixed(1)}`} parser={v => Number(v?.replace('%', '')) / 100} />
|
||||
</Form.Item>
|
||||
<Form.Item label="二单返现比例" name="secondOrderRate" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={1} step={0.001} addonAfter="%" style={{ width: '100%' }}
|
||||
formatter={v => `${(Number(v) * 100).toFixed(1)}`} parser={v => Number(v?.replace('%', '')) / 100} />
|
||||
</Form.Item>
|
||||
<Form.Item label="最低返现金额" name="minCashback" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} step={0.01} prefix="¥" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="最高返现金额" name="maxCashback" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} step={1} prefix="¥" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="提现门槛" name="withdrawThreshold" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} step={1} prefix="¥" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={handleSaveConfig}>保存配置</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button onClick={() => navigate('/activity/list')}>返回列表</Button>
|
||||
</div>
|
||||
<h2 style={{ marginBottom: 16 }}>邀请返现管理</h2>
|
||||
<Card>
|
||||
<Tabs items={tabItems} />
|
||||
</Card>
|
||||
@@ -255,4 +262,4 @@ const ActivityDetail: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityDetail;
|
||||
export default InviteManage;
|
||||
@@ -1,11 +1,88 @@
|
||||
import React from 'react';
|
||||
import { Empty } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd';
|
||||
import { getServiceFeeConfig, updateServiceFeeConfig } from '@/api/config';
|
||||
|
||||
const SystemSettings: React.FC = () => (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>系统设置</h2>
|
||||
<Empty description="系统设置功能开发中" />
|
||||
</div>
|
||||
);
|
||||
const SystemSettings: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
export default SystemSettings;
|
||||
const fetchConfig = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getServiceFeeConfig();
|
||||
form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 });
|
||||
} catch (e) {
|
||||
form.setFieldsValue({ serviceFeeRate: 0.05 });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchConfig(); }, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
await updateServiceFeeConfig(values.serviceFeeRate);
|
||||
message.success('服务费配置已保存');
|
||||
fetchConfig();
|
||||
} catch (e: any) {
|
||||
if (e?.message) message.error(e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>系统设置</h2>
|
||||
|
||||
<Card title="服务费配置" style={{ maxWidth: 600 }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="软件服务费比例"
|
||||
name="serviceFeeRate"
|
||||
rules={[
|
||||
{ required: true, message: '请输入服务费比例' },
|
||||
{ type: 'number', min: 0, max: 1, message: '比例必须在0-1之间' },
|
||||
]}
|
||||
extra="用户支付金额中,该比例将作为软件服务费归平台所有,剩余部分为商家预计收入"
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
formatter={(v) => `${(Number(v) * 100).toFixed(2)}%`}
|
||||
parser={(v) => Number(v?.replace('%', '')) / 100}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" loading={saving} onClick={handleSave}>
|
||||
保存配置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{ color: '#666', fontSize: 14 }}>
|
||||
<p><strong>计算公式:</strong></p>
|
||||
<p>软件服务费 = 实付金额 × 服务费比例</p>
|
||||
<p>商家预计收入 = 实付金额 - 软件服务费</p>
|
||||
<p style={{ marginTop: 12 }}>
|
||||
<strong>示例:</strong>实付金额 ¥100.00,服务费比例 5%
|
||||
</p>
|
||||
<p>软件服务费 = ¥5.00,商家预计收入 = ¥95.00</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSettings;
|
||||
@@ -1,78 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Table, Button, Tag, Space, message } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getActivities, toggleActivity } from '@/api/activity';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const ACTIVITY_TYPES: Record<string, string> = {
|
||||
invite_cashback: '邀请返现',
|
||||
};
|
||||
|
||||
const ActivityList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [list, setList] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getActivities({ page, pageSize: 10 });
|
||||
setList(res.data?.list || []);
|
||||
setTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchList(); }, [page]);
|
||||
|
||||
const handleToggle = async (record: any) => {
|
||||
try {
|
||||
await toggleActivity(record.id);
|
||||
message.success(record.enabled ? '已停用' : '已启用');
|
||||
fetchList();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '活动名称', dataIndex: 'name' },
|
||||
{ title: '活动类型', dataIndex: 'type', render: (v: string) => ACTIVITY_TYPES[v] || v },
|
||||
{ title: '状态', dataIndex: 'enabled', render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag> },
|
||||
{
|
||||
title: '活动时间',
|
||||
render: (_: any, r: any) => {
|
||||
if (!r.startTime) return '不限';
|
||||
return `${dayjs(r.startTime).format('YYYY-MM-DD')} ~ ${dayjs(r.endTime).format('YYYY-MM-DD')}`;
|
||||
},
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'createdAt', render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
|
||||
{
|
||||
title: '操作',
|
||||
render: (_: any, r: any) => (
|
||||
<Space>
|
||||
<Button type="primary" size="small" onClick={() => navigate(`/activity/detail/${r.id}`)}>查看详情</Button>
|
||||
<Button size="small" danger={r.enabled} type={r.enabled ? 'primary' : 'default'} onClick={() => handleToggle(r)}>
|
||||
{r.enabled ? '停用' : '启用'}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card title="活动列表" extra={<Button type="primary" onClick={() => navigate('/activity/detail/create')}>创建活动</Button>}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
loading={loading}
|
||||
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityList;
|
||||
@@ -100,19 +100,8 @@ const Earnings: React.FC = () => {
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="提现手续费"
|
||||
value={data?.totalFee || 0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="提现佣金"
|
||||
value={data?.totalWithdrawCommission || 0}
|
||||
title="服务费收入"
|
||||
value={data?.totalServiceFee || 0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
|
||||
@@ -71,8 +71,6 @@ const Withdrawals: React.FC = () => {
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: '商家', dataIndex: ['merchant', 'shopName'], width: 120, ellipsis: true },
|
||||
{ title: '提现金额', dataIndex: 'amount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '手续费', dataIndex: 'fee', width: 90, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '佣金', dataIndex: 'commissionAmount', width: 90, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '实际到账', dataIndex: 'actualAmount', width: 120, render: (v) => <span style={{ color: '#1890ff', fontWeight: 600 }}>¥{Number(v).toFixed(2)}</span> },
|
||||
{ title: '开户银行', dataIndex: 'bankName', width: 120 },
|
||||
{ title: '银行账号', dataIndex: 'bankAccount', width: 150 },
|
||||
|
||||
@@ -16,6 +16,7 @@ import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
|
||||
import { ReviewModule } from './modules/review/review.module';
|
||||
import { FinanceModule } from './modules/finance/finance.module';
|
||||
import { ActivityModule } from './modules/activity/activity.module';
|
||||
import { PlatformConfigModule } from './modules/config/config.module';
|
||||
import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module';
|
||||
|
||||
@Module({
|
||||
@@ -53,6 +54,7 @@ import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module
|
||||
RoomCalendarModule,
|
||||
OrderModule,
|
||||
AdminAuthModule,
|
||||
PlatformConfigModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -44,6 +44,12 @@ export class Order {
|
||||
@Column({ name: 'room_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '房费总额' })
|
||||
roomAmount: number;
|
||||
|
||||
@Column({ name: 'service_fee', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '软件服务费' })
|
||||
serviceFee: number;
|
||||
|
||||
@Column({ name: 'merchant_income', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '商家预计收入' })
|
||||
merchantIncome: number;
|
||||
|
||||
@Column({ name: 'coupon_discount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '优惠券抵扣' })
|
||||
couponDiscount: number;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('platform_configs')
|
||||
export class PlatformConfig {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'config_key', length: 50, unique: true, comment: '配置键' })
|
||||
configKey: string;
|
||||
|
||||
@Column({ name: 'config_value', length: 500, comment: '配置值' })
|
||||
configValue: string;
|
||||
|
||||
@Column({ length: 200, nullable: true, comment: '配置说明' })
|
||||
description: string;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -124,6 +124,18 @@ export class AdminActivityController {
|
||||
return this.activityService.getAdminStats();
|
||||
}
|
||||
|
||||
@Get('invite/config')
|
||||
@ApiOperation({ summary: '获取邀请活动配置' })
|
||||
async getInviteConfig() {
|
||||
return this.activityService.getInviteCashbackConfig();
|
||||
}
|
||||
|
||||
@Put('invite/config')
|
||||
@ApiOperation({ summary: '更新邀请活动配置' })
|
||||
async updateInviteConfig(@Body() dto: UpdateActivityDto) {
|
||||
return this.activityService.updateInviteCashbackConfig(dto);
|
||||
}
|
||||
|
||||
@Get('invite/records')
|
||||
@ApiOperation({ summary: '邀请记录列表' })
|
||||
async getRecords(@Query() dto: QueryInviteRecordsDto) {
|
||||
|
||||
@@ -435,6 +435,36 @@ export class ActivityService {
|
||||
|
||||
// ===== 管理端功能 =====
|
||||
|
||||
async getInviteCashbackConfig() {
|
||||
const activity = await this.activityRepo.findOne({
|
||||
where: { type: 'invite_cashback' },
|
||||
});
|
||||
if (!activity) {
|
||||
// 创建默认配置
|
||||
activity = this.activityRepo.create({
|
||||
name: '邀请返现',
|
||||
type: 'invite_cashback',
|
||||
enabled: true,
|
||||
config: {
|
||||
firstOrderRate: 0.05,
|
||||
secondOrderRate: 0.005,
|
||||
minCashback: 0.01,
|
||||
maxCashback: 50,
|
||||
withdrawThreshold: 10,
|
||||
},
|
||||
});
|
||||
await this.activityRepo.save(activity);
|
||||
}
|
||||
return activity;
|
||||
}
|
||||
|
||||
async updateInviteCashbackConfig(dto: UpdateActivityDto) {
|
||||
const activity = await this.getInviteCashbackConfig();
|
||||
if (dto.enabled !== undefined) activity.enabled = dto.enabled;
|
||||
if (dto.config) activity.config = { ...activity.config, ...dto.config };
|
||||
return this.activityRepo.save(activity);
|
||||
}
|
||||
|
||||
async getAdminStats(activityId?: number) {
|
||||
const where: any = {};
|
||||
if (activityId) where.activityId = activityId;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Controller, Get, Put, Body, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { ConfigService } from './config.service';
|
||||
import { JwtAuthGuard, RolesGuard, Roles } from '@/common';
|
||||
|
||||
@ApiTags('管理端-系统配置')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@Controller('admin/config')
|
||||
export class ConfigController {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
@Get('service-fee')
|
||||
@ApiOperation({ summary: '获取服务费配置' })
|
||||
async getServiceFeeConfig() {
|
||||
return this.configService.getServiceFeeConfig();
|
||||
}
|
||||
|
||||
@Put('service-fee')
|
||||
@ApiOperation({ summary: '更新服务费配置' })
|
||||
async updateServiceFeeConfig(@Body() body: { rate: number }) {
|
||||
await this.configService.updateServiceFeeRate(body.rate);
|
||||
return this.configService.getServiceFeeConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PlatformConfig } from '@/entities/platform-config.entity';
|
||||
import { ConfigService } from './config.service';
|
||||
import { ConfigController } from './config.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([PlatformConfig])],
|
||||
controllers: [ConfigController],
|
||||
providers: [ConfigService],
|
||||
exports: [ConfigService],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PlatformConfig } from '@/entities/platform-config.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
constructor(
|
||||
@InjectRepository(PlatformConfig)
|
||||
private configRepo: Repository<PlatformConfig>,
|
||||
) {}
|
||||
|
||||
async getConfig(key: string): Promise<string | null> {
|
||||
const config = await this.configRepo.findOne({ where: { configKey: key } });
|
||||
return config?.configValue || null;
|
||||
}
|
||||
|
||||
async setConfig(key: string, value: string, description?: string): Promise<void> {
|
||||
let config = await this.configRepo.findOne({ where: { configKey: key } });
|
||||
if (config) {
|
||||
config.configValue = value;
|
||||
if (description) config.description = description;
|
||||
} else {
|
||||
config = this.configRepo.create({ configKey: key, configValue: value, description });
|
||||
}
|
||||
await this.configRepo.save(config);
|
||||
}
|
||||
|
||||
async getServiceFeeRate(): Promise<number> {
|
||||
const value = await this.getConfig('service_fee_rate');
|
||||
const rate = parseFloat(value || '0.05');
|
||||
return isNaN(rate) ? 0.05 : rate;
|
||||
}
|
||||
|
||||
async getCommissionRate(): Promise<number> {
|
||||
const value = await this.getConfig('commission_rate');
|
||||
const rate = parseFloat(value || '0.10');
|
||||
return isNaN(rate) ? 0.10 : rate;
|
||||
}
|
||||
|
||||
async getServiceFeeConfig(): Promise<{ rate: number; description: string }> {
|
||||
const value = await this.getConfig('service_fee_rate');
|
||||
const config = await this.configRepo.findOne({ where: { configKey: 'service_fee_rate' } });
|
||||
const rate = parseFloat(value || '0.05');
|
||||
return {
|
||||
rate: isNaN(rate) ? 0.05 : rate,
|
||||
description: config?.description || '软件服务费比例',
|
||||
};
|
||||
}
|
||||
|
||||
async updateServiceFeeRate(rate: number): Promise<void> {
|
||||
if (rate < 0 || rate > 1) {
|
||||
throw new Error('服务费比例必须在0-1之间');
|
||||
}
|
||||
await this.setConfig('service_fee_rate', rate.toString(), '软件服务费比例');
|
||||
}
|
||||
}
|
||||
@@ -119,13 +119,6 @@ export class FinanceService {
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
// 计算手续费率
|
||||
const feeRate = 0.006;
|
||||
const fee = Math.round(dto.amount * feeRate * 100) / 100;
|
||||
const commissionRate = Number(merchant.walletBalance) > 0 ? 0.10 : 0;
|
||||
const commissionAmount = Math.round(dto.amount * commissionRate * 100) / 100;
|
||||
const actualAmount = Math.round((dto.amount - fee - commissionAmount) * 100) / 100;
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
@@ -138,9 +131,9 @@ export class FinanceService {
|
||||
merchantId,
|
||||
settlementIds: approvedSettlements.map(s => s.id),
|
||||
amount: dto.amount,
|
||||
fee,
|
||||
commissionAmount,
|
||||
actualAmount,
|
||||
fee: 0,
|
||||
commissionAmount: 0,
|
||||
actualAmount: dto.amount,
|
||||
bankName: dto.bankName,
|
||||
bankAccount: dto.bankAccount,
|
||||
accountName: dto.accountName,
|
||||
@@ -361,8 +354,13 @@ export class FinanceService {
|
||||
const withdrawalStats = await wQb
|
||||
.select('COUNT(*)', 'totalWithdrawals')
|
||||
.addSelect('COALESCE(SUM(CASE WHEN w.status = \'paid\' THEN w.amount ELSE 0 END), 0)', 'paidAmount')
|
||||
.addSelect('COALESCE(SUM(w.fee), 0)', 'totalFee')
|
||||
.addSelect('COALESCE(SUM(w.commission_amount), 0)', 'totalWithdrawCommission')
|
||||
.getRawOne();
|
||||
|
||||
// 服务费统计
|
||||
const serviceFeeStats = await this.orderRepo
|
||||
.createQueryBuilder('o')
|
||||
.where('o.status IN (:...statuses)', { statuses: ['completed', 'checked_in'] })
|
||||
.select('COALESCE(SUM(o.service_fee), 0)', 'totalServiceFee')
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
@@ -371,9 +369,8 @@ export class FinanceService {
|
||||
settlementAmount: Number(settlementStats?.totalSettlementAmount || 0),
|
||||
totalSettlements: Number(settlementStats?.totalSettlements || 0),
|
||||
paidAmount: Number(withdrawalStats?.paidAmount || 0),
|
||||
totalFee: Number(withdrawalStats?.totalFee || 0),
|
||||
totalWithdrawCommission: Number(withdrawalStats?.totalWithdrawCommission || 0),
|
||||
totalWithdrawals: Number(withdrawalStats?.totalWithdrawals || 0),
|
||||
totalServiceFee: Number(serviceFeeStats?.totalServiceFee || 0),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import { OrderService } from './order.service';
|
||||
import { UserOrderController, MerchantOrderController, AdminOrderController } from './order.controller';
|
||||
import { MerchantModule } from '../merchant/merchant.module';
|
||||
import { ActivityModule } from '../activity/activity.module';
|
||||
import { PlatformConfigModule } from '../config/config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Order, Room, RoomCalendar]),
|
||||
MerchantModule,
|
||||
forwardRef(() => ActivityModule),
|
||||
PlatformConfigModule,
|
||||
],
|
||||
controllers: [UserOrderController, MerchantOrderController, AdminOrderController],
|
||||
providers: [OrderService],
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Room } from '@/entities/room.entity';
|
||||
import { RoomCalendar } from '@/entities/room-calendar.entity';
|
||||
import { CreateOrderDto, QueryOrderDto, ConfirmOrderDto } from './dto/order.dto';
|
||||
import { ActivityService } from '@/modules/activity/activity.service';
|
||||
import { ConfigService } from '@/modules/config/config.service';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
@@ -17,6 +18,7 @@ export class OrderService {
|
||||
@InjectRepository(RoomCalendar)
|
||||
private calendarRepo: Repository<RoomCalendar>,
|
||||
private readonly activityService: ActivityService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async create(userId: number, dto: CreateOrderDto) {
|
||||
@@ -36,6 +38,11 @@ export class OrderService {
|
||||
const totalAmount = roomAmount;
|
||||
const payAmount = totalAmount - couponDiscount;
|
||||
|
||||
// 计算软件服务费和商家收入
|
||||
const serviceFeeRate = await this.configService.getServiceFeeRate();
|
||||
const serviceFee = Math.round(payAmount * serviceFeeRate * 100) / 100;
|
||||
const merchantIncome = Math.round((payAmount - serviceFee) * 100) / 100;
|
||||
|
||||
const now = new Date();
|
||||
const orderNo = [
|
||||
now.getFullYear(),
|
||||
@@ -59,6 +66,8 @@ export class OrderService {
|
||||
guestCount: dto.guestCount || 1,
|
||||
roomPrice: room.price,
|
||||
roomAmount,
|
||||
serviceFee,
|
||||
merchantIncome,
|
||||
couponDiscount,
|
||||
totalAmount,
|
||||
payAmount,
|
||||
|
||||
@@ -209,7 +209,8 @@ CREATE TABLE `orders` (
|
||||
`room_count` TINYINT UNSIGNED DEFAULT 1 COMMENT '房间数',
|
||||
`room_price` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '房费单价',
|
||||
`room_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '房费总额',
|
||||
`service_fee` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '服务费',
|
||||
`service_fee` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '软件服务费',
|
||||
`merchant_income` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '商家预计收入',
|
||||
`coupon_discount` DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '优惠券抵扣',
|
||||
`total_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '订单总金额',
|
||||
`pay_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '实付金额',
|
||||
|
||||
+64
-21
@@ -136,12 +136,32 @@
|
||||
|
||||
```
|
||||
待支付 ──支付成功──> 待确认 ──商家确认──> 待入住 ──入住──> 已入住 ──离店──> 已完成
|
||||
│ │
|
||||
└──超时/取消──> 已取消 └──商家拒绝──> 已取消
|
||||
│ │ │
|
||||
└──超时/取消──> 已取消 └──商家拒绝──> 已取消 └──用户退款──> 退款中
|
||||
│
|
||||
├──商家通过──> 已退款(恢复房量)
|
||||
└──商家拒绝──> 已取消
|
||||
|
||||
已完成 ──申请退款──> 退款中 ──平台处理──> 已退款
|
||||
待确认 ──用户退款──> 退款中 ──商家通过──> 已退款(恢复房量)
|
||||
```
|
||||
|
||||
### 用户订单操作
|
||||
|
||||
| 操作 | 接口 | 适用状态 | 说明 |
|
||||
| -------- | ------------------------- | ---------------------------- | ------------------------ |
|
||||
| 取消订单 | PUT /orders/:id/cancel | pending_pay, pending_confirm | 取消订单,已支付则退款 |
|
||||
| 申请退款 | PUT /orders/:id/refund | pending_confirm, pending_checkin | 提交退款申请 |
|
||||
|
||||
### 商家订单操作
|
||||
|
||||
| 操作 | 接口 | 适用状态 | 说明 |
|
||||
| ---------- | ----------------------------------------- | ----------------- | ----------------------------- |
|
||||
| 确认订单 | PUT /seller/orders/:id/confirm | pending_confirm | 确认后等待用户入住 |
|
||||
| 拒绝订单 | PUT /seller/orders/:id/reject | pending_confirm | 拒绝并退款 |
|
||||
| 办理入住 | PUT /seller/orders/:id/checkin | pending_checkin | 标记已入住 |
|
||||
| 通过退款 | PUT /seller/orders/:id/approve-refund | refunding | 同意退款,恢复房量日历库存 |
|
||||
| 拒绝退款 | PUT /seller/orders/:id/reject-refund | refunding | 拒绝退款,订单变为已取消 |
|
||||
|
||||
### 自动化规则
|
||||
|
||||
- 待支付订单: 30分钟未支付自动取消
|
||||
@@ -152,11 +172,17 @@
|
||||
|
||||
```
|
||||
房费总额 = 房价/晚 × 入住晚数 × 房间数
|
||||
服务费 = 房费总额 × 服务费比例(默认5%)
|
||||
订单总额 = 房费总额 + 服务费
|
||||
订单总额 = 房费总额
|
||||
实付金额 = 订单总额 - 优惠券抵扣
|
||||
软件服务费 = 实付金额 × 服务费比例(默认5%,平台后台可配置)
|
||||
商家预计收入 = 实付金额 - 软件服务费
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 软件服务费归平台所有,在订单创建时计算
|
||||
- 商家预计收入 = 实付金额 - 软件服务费
|
||||
- 费用明细仅在商家订单详情中展示,用户端不显示
|
||||
|
||||
### 佣金计算
|
||||
|
||||
```
|
||||
@@ -242,9 +268,9 @@
|
||||
### 提现规则
|
||||
|
||||
- 最低提现金额: 100元
|
||||
- 提现手续费: 默认0.6%
|
||||
- 提现手续费: 无
|
||||
- 到账时间: T+1工作日
|
||||
- 需绑定银行卡且实名认证
|
||||
- 需绑定银行卡
|
||||
|
||||
### 对账维度
|
||||
|
||||
@@ -330,6 +356,10 @@
|
||||
|
||||
## 11. 邀请返现活动模块
|
||||
|
||||
### 模块概述
|
||||
|
||||
邀请返现是平台级营销活动,独立于其他促销活动管理。平台管理后台有专门的「邀请返现」菜单入口,不与其他活动混在一起。
|
||||
|
||||
### 活动概述
|
||||
|
||||
平台级营销活动,用户(邀请人)通过分享邀请链接/海报邀请新用户注册并完成订单,邀请人获得返现奖励。被邀请人无额外收益。
|
||||
@@ -517,19 +547,19 @@
|
||||
|
||||
**平台管理端(需管理员Token):**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ---- | ---------------------------------------------- | ------------- |
|
||||
| GET | /admin/activity/list | 活动列表 |
|
||||
| POST | /admin/activity/create | 创建活动 |
|
||||
| PUT | /admin/activity/:id/update | 编辑活动 |
|
||||
| PUT | /admin/activity/:id/toggle | 启用/停用活动 |
|
||||
| GET | /admin/activity/invite/stats | 邀请数据总览 |
|
||||
| GET | /admin/activity/invite/records | 邀请记录列表 |
|
||||
| GET | /admin/activity/invite/cashbacks | 返现记录列表 |
|
||||
| GET | /admin/activity/invite/withdrawals | 提现审核列表 |
|
||||
| PUT | /admin/activity/invite/withdrawals/:id/approve | 审核通过 |
|
||||
| PUT | /admin/activity/invite/withdrawals/:id/reject | 审核拒绝 |
|
||||
| PUT | /admin/activity/invite/withdrawals/:id/pay | 确认打款 |
|
||||
| 方法 | 路径 | 说明 |
|
||||
| ---- | ---------------------------------------------- | --------------- |
|
||||
| GET | /admin/activity/invite/stats | 邀请数据总览 |
|
||||
| GET | /admin/activity/invite/config | 获取活动配置 |
|
||||
| PUT | /admin/activity/invite/config | 更新活动配置 |
|
||||
| GET | /admin/activity/invite/records | 邀请记录列表 |
|
||||
| GET | /admin/activity/invite/cashbacks | 返现记录列表 |
|
||||
| GET | /admin/activity/invite/withdrawals | 提现审核列表 |
|
||||
| PUT | /admin/activity/invite/withdrawals/:id/approve | 审核通过 |
|
||||
| PUT | /admin/activity/invite/withdrawals/:id/reject | 审核拒绝 |
|
||||
| PUT | /admin/activity/invite/withdrawals/:id/pay | 确认打款 |
|
||||
|
||||
**注意**: 平台管理后台不再提供通用的活动列表CRUD,邀请返现活动有独立的配置管理页面。
|
||||
|
||||
### 小程序页面路由
|
||||
|
||||
@@ -538,4 +568,17 @@
|
||||
| 邀请首页 | /pages/invite/index | 邀请统计、海报生成、分享 |
|
||||
| 邀请记录 | /pages/invite/records | 邀请人数、被邀请人列表 |
|
||||
| 返现记录 | /pages/invite/cashbacks | 返现明细列表 |
|
||||
| 提现页面 | /pages/invite/withdraw | 余额展示、提现操作、提现记录 |
|
||||
| 提现页面 | /pages/invite/withdraw | 余额展示、提现操作 |
|
||||
| 提现记录 | /pages/invite/withdrawals | 提现历史列表 |
|
||||
| 邀请海报 | /pages/invite/poster | 生成分享海报 |
|
||||
|
||||
### 平台管理后台页面
|
||||
|
||||
| 页面 | 路径 | 说明 |
|
||||
| ------------ | -------- | ---------------------------------------------- |
|
||||
| 邀请返现管理 | /invite | 数据概览、提现审核、活动配置三个Tab |
|
||||
|
||||
**页面结构**:
|
||||
- 数据概览Tab: 统计卡片 + 邀请记录列表 + 返现记录列表
|
||||
- 提现审核Tab: 提现申请列表,支持审核通过/拒绝/确认打款
|
||||
- 活动配置Tab: 启用/停用开关 + 返现比例/金额限制/提现门槛配置表单
|
||||
|
||||
@@ -28,7 +28,7 @@ INSERT INTO `member_levels` (`name`, `level`, `min_points`, `discount`, `benefit
|
||||
-- 平台配置
|
||||
INSERT INTO `platform_configs` (`config_key`, `config_value`, `description`) VALUES
|
||||
('commission_rate', '0.10', '默认平台佣金比例'),
|
||||
('withdraw_fee_rate', '0.006', '提现手续费率'),
|
||||
('service_fee_rate', '0.05', '软件服务费比例'),
|
||||
('min_deposit', '5000', '最低保证金金额'),
|
||||
('auto_cancel_minutes', '30', '未支付订单自动取消时间(分钟)'),
|
||||
('auto_complete_hours', '24', '入住后自动完成订单时间(小时)'),
|
||||
|
||||
+76
-31
@@ -19,7 +19,7 @@
|
||||
| rooms | 房源表 | merchant_id, name, type, price, status, audit_status |
|
||||
| room_calendar | 房量房价日历 | room_id, date, price, stock, status |
|
||||
| room_calendar_logs | 房态变更日志 | room_id, operator_id, change_type |
|
||||
| orders | 订单表 | order_no, user_id, merchant_id, room_id, status |
|
||||
| orders | 订单表 | order_no, user_id, merchant_id, room_id, pay_amount, service_fee, merchant_income, status |
|
||||
| reviews | 评价表 | order_id, user_id, rating, content |
|
||||
| favorites | 收藏表 | user_id, room_id |
|
||||
| coupons | 优惠券模板 | name, type, value, scope |
|
||||
@@ -100,6 +100,29 @@ orders (1) ──── (1) settlements
|
||||
- `refunding` - 退款中
|
||||
- `refunded` - 已退款
|
||||
|
||||
### 订单状态流转
|
||||
|
||||
```
|
||||
待支付 ──支付成功──> 待确认 ──商家确认──> 待入住 ──入住──> 已入住 ──离店──> 已完成
|
||||
│ │ │
|
||||
└──超时/取消──> 已取消 └──商家拒绝──> 已取消 └──用户退款──> 退款中
|
||||
│
|
||||
├──商家通过──> 已退款(恢复房量)
|
||||
└──商家拒绝──> 已取消
|
||||
|
||||
待确认 ──用户退款──> 退款中 ──商家通过──> 已退款(恢复房量)
|
||||
```
|
||||
|
||||
### 订单取消/退款规则
|
||||
|
||||
| 操作 | 适用状态 | 说明 |
|
||||
| ------------ | ---------------------------- | --------------------------------------- |
|
||||
| 用户取消 | pending_pay | 直接取消,无退款 |
|
||||
| 用户取消 | pending_confirm | 取消并退款,恢复房量 |
|
||||
| 用户退款申请 | pending_confirm, pending_checkin | 状态变为 refunding,等待商家审核 |
|
||||
| 商家通过退款 | refunding | 状态变为 refunded,恢复房量日历库存 |
|
||||
| 商家拒绝退款 | refunding | 状态变为 cancelled |
|
||||
|
||||
### 房源状态 (rooms.status)
|
||||
|
||||
- `on_sale` - 在售
|
||||
@@ -210,6 +233,26 @@ mysql -u root -p rent_platform < database/seeds/001_init_data.sql
|
||||
|
||||
---
|
||||
|
||||
## 平台配置表 (platform_configs)
|
||||
|
||||
| 配置键 | 默认值 | 说明 |
|
||||
| ----------------- | ------ | ------------------------ |
|
||||
| commission_rate | 0.10 | 默认平台佣金比例 |
|
||||
| service_fee_rate | 0.05 | 软件服务费比例(可配置) |
|
||||
| min_deposit | 5000 | 最低保证金金额 |
|
||||
| auto_cancel_minutes | 30 | 未支付订单自动取消时间 |
|
||||
| auto_complete_hours | 24 | 入住后自动完成订单时间 |
|
||||
| sms_enabled | true | 是否启用短信通知 |
|
||||
| max_images_per_room | 20 | 每个房源最大图片数 |
|
||||
| max_images_per_review | 9 | 每条评价最大图片数 |
|
||||
|
||||
**服务费计算**:
|
||||
- 软件服务费 = 实付金额 × service_fee_rate
|
||||
- 商家预计收入 = 实付金额 - 软件服务费
|
||||
- 配置可通过平台管理后台「系统设置」页面调整
|
||||
|
||||
---
|
||||
|
||||
## 商家入驻流程
|
||||
|
||||
### 流程概述
|
||||
@@ -295,7 +338,9 @@ interface SellerLoginResult {
|
||||
| ------------- | ---------------------------- | ---------------------------- |
|
||||
| 个人中心 | /pages/mine/index | 入口:商家中心、商家入驻按钮 |
|
||||
| 商家注册/登录 | /pages/seller-register/index | 商家账号注册登录 |
|
||||
| 商家中心 | /pages/merchant/home | 商家管理入口 |
|
||||
| 商家中心 | /pages/seller/home | 商家管理入口 |
|
||||
| 商家订单列表 | /pages/seller/orders | 商家订单管理 |
|
||||
| 商家订单详情 | /pages/seller/order-detail | 商家订单详情 |
|
||||
| 创建店铺 | /pages/shop-create/index | 填写店铺信息提交审核 |
|
||||
| 修改店铺 | /pages/shop-edit/index | 修改店铺信息重新审核 |
|
||||
|
||||
@@ -437,39 +482,39 @@ async update(id, dto) {
|
||||
2. **商家实名认证**: 身份证上传、银行卡绑定
|
||||
3. **图片上传接口**: 营业执照真实上传功能
|
||||
|
||||
- 显示状态标签"已冻结"
|
||||
- 显示提示"店铺已被冻结,请联系平台客服"
|
||||
- 不显示修改按钮
|
||||
- 不显示数据概览和功能菜单
|
||||
---
|
||||
|
||||
### 修改店铺重新审核逻辑
|
||||
## 商家订单管理
|
||||
|
||||
**后端逻辑** (`merchant.service.ts`):
|
||||
### 商家订单接口
|
||||
|
||||
```typescript
|
||||
async update(id, dto) {
|
||||
const merchant = await this.findById(id);
|
||||
// 审核通过或拒绝后修改,重置为pending
|
||||
if (merchant.status === 'approved' || merchant.status === 'rejected') {
|
||||
await this.merchantRepo.update(id, { ...dto, status: 'pending', rejectReason: null });
|
||||
} else {
|
||||
await this.merchantRepo.update(id, dto);
|
||||
}
|
||||
return this.findById(id);
|
||||
}
|
||||
```
|
||||
| 接口 | 路径 | 说明 |
|
||||
| -------------- | --------------------------------- | ---------------------------- |
|
||||
| 订单列表 | GET /api/seller/orders | 支持状态筛选、订单号搜索 |
|
||||
| 订单详情 | GET /api/seller/orders/:id | 获取订单详情 |
|
||||
| 确认订单 | PUT /api/seller/orders/:id/confirm| pending_confirm → pending_checkin |
|
||||
| 拒绝订单 | PUT /api/seller/orders/:id/reject | pending_confirm → cancelled |
|
||||
| 办理入住 | PUT /api/seller/orders/:id/checkin| pending_checkin → checked_in |
|
||||
| 通过退款 | PUT /api/seller/orders/:id/approve-refund | refunding → refunded,恢复房量 |
|
||||
| 拒绝退款 | PUT /api/seller/orders/:id/reject-refund | refunding → cancelled |
|
||||
|
||||
**修改流程**:
|
||||
### 商家订单状态Tab
|
||||
|
||||
```
|
||||
点击"修改店铺信息" → 填写表单 → 提交修改
|
||||
→ 调用 PUT /merchant/update
|
||||
→ 后端自动将 status 重置为 pending,清空 rejectReason
|
||||
→ 跳转商家中心显示"审核中"状态
|
||||
```
|
||||
| Tab名称 | 对应状态 |
|
||||
| --------- | --------------------------------------------- |
|
||||
| 全部 | 所有状态 |
|
||||
| 待确认 | pending_confirm |
|
||||
| 待入住 | pending_checkin |
|
||||
| 已入住 | checked_in |
|
||||
| 已完成 | completed |
|
||||
| 已取消 | cancelled |
|
||||
| 已退款 | refunded, refunding |
|
||||
|
||||
### 后续迭代事项
|
||||
### 商家订单操作权限
|
||||
|
||||
1. **商家管理后台集成**: 商家后台跳转入口
|
||||
2. **商家实名认证**: 身份证上传、银行卡绑定
|
||||
3. **图片上传接口**: 营业执照真实上传功能
|
||||
| 状态 | 可操作 |
|
||||
| ---------------- | -------------------------- |
|
||||
| pending_confirm | 确认、拒绝 |
|
||||
| pending_checkin | 办理入住 |
|
||||
| refunding | 通过退款、拒绝退款 |
|
||||
| 其他状态 | 无操作 |
|
||||
Reference in New Issue
Block a user