feat: 迭代

This commit is contained in:
2026-06-01 09:36:52 +08:00
parent e8bce5e924
commit f021b43f05
38 changed files with 1785 additions and 88 deletions
+4 -1
View File
@@ -97,7 +97,10 @@
"Bash(cd /d/project/company/rent/apps/server && tail -100 logs/*.log 2>/dev/null | grep -A 5 -B 5 \"周结算\" | tail -50 || echo \"日志文件不存在\")", "Bash(cd /d/project/company/rent/apps/server && tail -100 logs/*.log 2>/dev/null | grep -A 5 -B 5 \"周结算\" | tail -50 || echo \"日志文件不存在\")",
"Read(//d/d/project/company/rent/apps/server/**)", "Read(//d/d/project/company/rent/apps/server/**)",
"Bash(node -e \"const dayjs = require\\('dayjs'\\); console.log\\('今天是周几:', dayjs\\('2026-05-28'\\).day\\(\\)\\); console.log\\('0=周日, 1=周一, ..., 6=周六'\\);\")", "Bash(node -e \"const dayjs = require\\('dayjs'\\); console.log\\('今天是周几:', dayjs\\('2026-05-28'\\).day\\(\\)\\); console.log\\('0=周日, 1=周一, ..., 6=周六'\\);\")",
"Bash(git checkout *)" "Bash(git checkout *)",
"Bash(grep -E \"\\\\.\\(tsx|ts|jsx|js\\)$\")",
"Bash(xargs grep -l \"提现\\\\|withdraw\")",
"Bash(xargs grep -l \"admin\")"
], ],
"additionalDirectories": [ "additionalDirectories": [
"\\tmp", "\\tmp",
+5
View File
@@ -52,3 +52,8 @@ export function getPendingSettlementOrders(params: any) {
export function getPendingSettlementSummary() { export function getPendingSettlementSummary() {
return request.get('/api/merchant/finance/settlements/pending/summary'); return request.get('/api/merchant/finance/settlements/pending/summary');
} }
// 配置相关
export function getWithdrawConfig() {
return request.get('/api/admin/config/withdraw');
}
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Card, Button, Table, message, Modal, Form, InputNumber, Input, Space, Alert } from 'antd'; import { Card, Button, Table, message, Modal, Form, InputNumber, Input, Space, Alert } from 'antd';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ExportOutlined } from '@ant-design/icons'; import { ExportOutlined } from '@ant-design/icons';
import { getMerchantAccount, getMerchantTransactions, applyWithdrawal } from '@/api/finance'; import { getMerchantAccount, getMerchantTransactions, applyWithdrawal, getWithdrawConfig } from '@/api/finance';
import { getMerchantInfo } from '@/api/auth'; import { getMerchantInfo } from '@/api/auth';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import type { Account, Transaction, TransactionType } from '@rent/shared-types'; import type { Account, Transaction, TransactionType } from '@rent/shared-types';
@@ -18,6 +18,7 @@ const Wallet: React.FC = () => {
const [accountLoading, setAccountLoading] = useState(false); const [accountLoading, setAccountLoading] = useState(false);
const [withdrawModalVisible, setWithdrawModalVisible] = useState(false); const [withdrawModalVisible, setWithdrawModalVisible] = useState(false);
const [merchantInfo, setMerchantInfo] = useState<any>(null); const [merchantInfo, setMerchantInfo] = useState<any>(null);
const [minWithdrawAmount, setMinWithdrawAmount] = useState(100);
const [form] = Form.useForm(); const [form] = Form.useForm();
const { const {
@@ -43,6 +44,7 @@ const Wallet: React.FC = () => {
useEffect(() => { useEffect(() => {
fetchAccount(); fetchAccount();
fetchWithdrawConfig();
}, []); }, []);
const fetchAccount = async () => { const fetchAccount = async () => {
@@ -57,6 +59,17 @@ const Wallet: React.FC = () => {
} }
}; };
const fetchWithdrawConfig = async () => {
try {
const res: any = await getWithdrawConfig();
if (res.data?.merchantMinWithdrawAmount) {
setMinWithdrawAmount(res.data.merchantMinWithdrawAmount);
}
} catch (error) {
console.error('获取提现配置失败', error);
}
};
const handleWithdraw = async () => { const handleWithdraw = async () => {
if (!account || account.availableAmount <= 0) { if (!account || account.availableAmount <= 0) {
message.warning('可用余额不足,无法提现'); message.warning('可用余额不足,无法提现');
@@ -194,7 +207,7 @@ const Wallet: React.FC = () => {
{formatMoney(account?.availableAmount || 0)} {formatMoney(account?.availableAmount || 0)}
</div> </div>
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}> <div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
100 {minWithdrawAmount}
</div> </div>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -204,8 +217,8 @@ const Wallet: React.FC = () => {
{ required: true, message: '请输入提现金额' }, { required: true, message: '请输入提现金额' },
{ {
validator: (_, value) => { validator: (_, value) => {
if (value && value < 100) { if (value && value < minWithdrawAmount) {
return Promise.reject('提现金额不能小于100元'); return Promise.reject(`提现金额不能小于${minWithdrawAmount}`);
} }
if (value && value > (account?.availableAmount || 0)) { if (value && value > (account?.availableAmount || 0)) {
return Promise.reject('提现金额不能大于可用余额'); return Promise.reject('提现金额不能大于可用余额');
@@ -221,7 +234,7 @@ const Wallet: React.FC = () => {
<InputNumber <InputNumber
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="请输入提现金额" placeholder="请输入提现金额"
min={100} min={minWithdrawAmount}
max={account?.availableAmount || 0} max={account?.availableAmount || 0}
precision={2} precision={2}
addonAfter="元" addonAfter="元"
+43
View File
@@ -35,3 +35,46 @@ export function refundOrder(orderNo: string, reason: string) {
export function payOrder(orderNo: string, paymentMethod: 'wechat' | 'alipay' | 'balance' = 'wechat') { export function payOrder(orderNo: string, paymentMethod: 'wechat' | 'alipay' | 'balance' = 'wechat') {
return post('/api/app/orders/pay', { orderNo, paymentMethod }); return post('/api/app/orders/pay', { orderNo, paymentMethod });
} }
/**
* 调用微信支付
*/
export async function wxPay(orderNo: string) {
try {
// 1. 调用后端接口获取支付参数
const res = await payOrder(orderNo, 'wechat');
if (!res.data || !res.data.payParams) {
throw new Error('获取支付参数失败');
}
const { payParams } = res.data;
// 2. 调用微信支付
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: (res) => {
console.log('支付成功', res);
resolve(res);
},
fail: (err) => {
console.error('支付失败', err);
if (err.errMsg === 'requestPayment:fail cancel') {
reject(new Error('用户取消支付'));
} else {
reject(new Error(err.errMsg || '支付失败'));
}
},
});
});
} catch (error) {
console.error('发起支付失败', error);
throw error;
}
}
+2
View File
@@ -29,6 +29,7 @@ import PlatformWithdrawals from '@/pages/finance/PlatformWithdrawals';
import BankCards from '@/pages/finance/BankCards'; import BankCards from '@/pages/finance/BankCards';
import InviteManage from '@/pages/InviteManage'; import InviteManage from '@/pages/InviteManage';
import ReviewManage from '@/pages/ReviewManage'; import ReviewManage from '@/pages/ReviewManage';
import AdminManage from '@/pages/AdminManage';
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const token = localStorage.getItem('admin_token'); const token = localStorage.getItem('admin_token');
@@ -71,6 +72,7 @@ const App: React.FC = () => (
<Route path="coupons/create" element={<CouponForm />} /> <Route path="coupons/create" element={<CouponForm />} />
<Route path="coupons/edit/:id" element={<CouponForm />} /> <Route path="coupons/edit/:id" element={<CouponForm />} />
<Route path="promotions" element={<Promotion />} /> <Route path="promotions" element={<Promotion />} />
<Route path="admins" element={<AdminManage />} />
<Route path="settings" element={<SystemSettings />} /> <Route path="settings" element={<SystemSettings />} />
</Route> </Route>
</Routes> </Routes>
+69
View File
@@ -67,3 +67,72 @@ export function getPlatformStatistics() {
export function getOrderTrend(params: { startDate: string; endDate: string }) { export function getOrderTrend(params: { startDate: string; endDate: string }) {
return request.get('/api/admin/finance/reports/trend', { params }); return request.get('/api/admin/finance/reports/trend', { params });
} }
// 管理员管理
export interface Admin {
id: number;
username: string;
name: string;
phone?: string;
email?: string;
role: 'super_admin' | 'admin' | 'operator';
status: 'active' | 'frozen';
lastLoginAt?: string;
lastLoginIp?: string;
createdAt: string;
updatedAt: string;
}
export interface CreateAdminParams {
username: string;
password: string;
name: string;
phone?: string;
email?: string;
role: 'super_admin' | 'admin' | 'operator';
}
export interface UpdateAdminParams {
name?: string;
phone?: string;
email?: string;
role?: 'super_admin' | 'admin' | 'operator';
status?: 'active' | 'frozen';
}
export interface QueryAdminParams {
username?: string;
name?: string;
role?: 'super_admin' | 'admin' | 'operator';
status?: 'active' | 'frozen';
page?: number;
pageSize?: number;
}
export function getAdminList(params: QueryAdminParams) {
return request.get('/api/admin/admins', { params });
}
export function getAdminById(id: number) {
return request.get(`/api/admin/admins/${id}`);
}
export function createAdmin(data: CreateAdminParams) {
return request.post('/api/admin/admins', data);
}
export function updateAdmin(id: number, data: UpdateAdminParams) {
return request.put(`/api/admin/admins/${id}`, data);
}
export function updateAdminPassword(id: number, password: string) {
return request.put(`/api/admin/admins/${id}/password`, { password });
}
export function toggleAdminStatus(id: number) {
return request.put(`/api/admin/admins/${id}/toggle-status`);
}
export function deleteAdmin(id: number) {
return request.delete(`/api/admin/admins/${id}`);
}
+6
View File
@@ -7,3 +7,9 @@ export const updateServiceFeeConfig = (rate: number) => request.put('/api/admin/
export const getStorageConfig = () => request.get('/api/admin/config/storage'); export const getStorageConfig = () => request.get('/api/admin/config/storage');
export const updateStorageConfig = (data: Record<string, string>) => request.put('/api/admin/config/storage', data); export const updateStorageConfig = (data: Record<string, string>) => request.put('/api/admin/config/storage', data);
export const getWithdrawConfig = () => request.get('/api/admin/config/withdraw');
export const updateMerchantMinWithdrawAmount = (amount: number) => request.put('/api/admin/config/withdraw/merchant-min', { amount });
export const updatePlatformMinWithdrawAmount = (amount: number) => request.put('/api/admin/config/withdraw/platform-min', { amount });
@@ -21,6 +21,7 @@ import {
BarChartOutlined, BarChartOutlined,
TagOutlined, TagOutlined,
CreditCardOutlined, CreditCardOutlined,
SafetyOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useAuthStore } from '@/store/auth'; import { useAuthStore } from '@/store/auth';
@@ -53,6 +54,7 @@ const menuItems = [
{ key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' }, { key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' },
{ key: '/coupons', icon: <TagOutlined />, label: '优惠券管理' }, { key: '/coupons', icon: <TagOutlined />, label: '优惠券管理' },
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' }, { key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
{ key: '/admins', icon: <SafetyOutlined />, label: '管理员管理' },
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' }, { key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
]; ];
@@ -0,0 +1,416 @@
import React, { useState } from 'react';
import { Card, Table, Button, Space, Tag, Modal, Form, Input, Select, message, Popconfirm } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, LockOutlined, StopOutlined, CheckCircleOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { formatDateTime } from '@rent/shared-utils';
import {
getAdminList,
createAdmin,
updateAdmin,
updateAdminPassword,
toggleAdminStatus,
deleteAdmin,
type Admin,
type CreateAdminParams,
type UpdateAdminParams,
type QueryAdminParams
} from '@/api/admin';
const ROLE_MAP = {
super_admin: { label: '超级管理员', color: 'red' },
admin: { label: '管理员', color: 'blue' },
operator: { label: '运营人员', color: 'green' },
};
const STATUS_MAP = {
active: { label: '正常', color: 'success' },
frozen: { label: '冻结', color: 'error' },
};
const AdminManage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<Admin[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [filters, setFilters] = useState<QueryAdminParams>({});
const [createModalVisible, setCreateModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
const [currentAdmin, setCurrentAdmin] = useState<Admin | null>(null);
const [createForm] = Form.useForm();
const [editForm] = Form.useForm();
const [passwordForm] = Form.useForm();
React.useEffect(() => {
fetchData();
}, [page, pageSize, filters]);
const fetchData = async () => {
setLoading(true);
try {
const res: any = await getAdminList({ ...filters, page, pageSize });
setDataSource(res.data.items || []);
setTotal(res.data.total || 0);
} catch (error: any) {
message.error(error.response?.data?.message || '获取管理员列表失败');
} finally {
setLoading(false);
}
};
const handleCreate = () => {
createForm.resetFields();
setCreateModalVisible(true);
};
const handleCreateSubmit = async () => {
try {
const values = await createForm.validateFields();
await createAdmin(values);
message.success('创建成功');
setCreateModalVisible(false);
fetchData();
} catch (error: any) {
if (error.response) {
message.error(error.response?.data?.message || '创建失败');
}
}
};
const handleEdit = (record: Admin) => {
setCurrentAdmin(record);
editForm.setFieldsValue({
name: record.name,
phone: record.phone,
email: record.email,
role: record.role,
status: record.status,
});
setEditModalVisible(true);
};
const handleEditSubmit = async () => {
if (!currentAdmin) return;
try {
const values = await editForm.validateFields();
await updateAdmin(currentAdmin.id, values);
message.success('更新成功');
setEditModalVisible(false);
fetchData();
} catch (error: any) {
message.error(error.response?.data?.message || '更新失败');
}
};
const handleResetPassword = (record: Admin) => {
setCurrentAdmin(record);
passwordForm.resetFields();
setPasswordModalVisible(true);
};
const handlePasswordSubmit = async () => {
if (!currentAdmin) return;
try {
const values = await passwordForm.validateFields();
await updateAdminPassword(currentAdmin.id, values.password);
message.success('密码重置成功');
setPasswordModalVisible(false);
} catch (error: any) {
message.error(error.response?.data?.message || '密码重置失败');
}
};
const handleToggleStatus = async (record: Admin) => {
try {
await toggleAdminStatus(record.id);
message.success('状态切换成功');
fetchData();
} catch (error: any) {
message.error(error.response?.data?.message || '状态切换失败');
}
};
const handleDelete = async (record: Admin) => {
try {
await deleteAdmin(record.id);
message.success('删除成功');
fetchData();
} catch (error: any) {
message.error(error.response?.data?.message || '删除失败');
}
};
const columns: ColumnsType<Admin> = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 120,
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120,
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
width: 180,
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
width: 120,
render: (role: string) => {
const roleInfo = ROLE_MAP[role as keyof typeof ROLE_MAP];
return <Tag color={roleInfo?.color}>{roleInfo?.label || role}</Tag>;
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const statusInfo = STATUS_MAP[status as keyof typeof STATUS_MAP];
return <Tag color={statusInfo?.color}>{statusInfo?.label || status}</Tag>;
},
},
{
title: '最后登录时间',
dataIndex: 'lastLoginAt',
key: 'lastLoginAt',
width: 180,
render: (date: string) => date ? formatDateTime(date) : '-',
},
{
title: '最后登录IP',
dataIndex: 'lastLoginIp',
key: 'lastLoginIp',
width: 140,
render: (ip: string) => ip || '-',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (date: string) => formatDateTime(date),
},
{
title: '操作',
key: 'action',
width: 280,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
</Button>
<Button type="link" size="small" icon={<LockOutlined />} onClick={() => handleResetPassword(record)}>
</Button>
{record.role !== 'super_admin' && (
<>
<Button
type="link"
size="small"
icon={record.status === 'active' ? <StopOutlined /> : <CheckCircleOutlined />}
onClick={() => handleToggleStatus(record)}
>
{record.status === 'active' ? '冻结' : '解冻'}
</Button>
<Popconfirm
title="确定要删除该管理员吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</>
)}
</Space>
),
},
];
return (
<div>
<h2 style={{ marginBottom: 24 }}></h2>
<Card>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button>
</Space>
<Table
columns={columns}
dataSource={dataSource}
rowKey="id"
loading={loading}
scroll={{ x: 1600 }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setPage(page);
setPageSize(pageSize);
},
}}
/>
</Card>
{/* 创建管理员 */}
<Modal
title="新增管理员"
open={createModalVisible}
onOk={handleCreateSubmit}
onCancel={() => setCreateModalVisible(false)}
width={600}
>
<Form form={createForm} layout="vertical">
<Form.Item
name="username"
label="用户名"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 3, max: 50, message: '用户名长度为3-50个字符' },
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, max: 50, message: '密码长度为6-50个字符' },
]}
>
<Input.Password placeholder="请输入密码" />
</Form.Item>
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="phone" label="手机号">
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select placeholder="请选择角色">
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="super_admin"></Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* 编辑管理员 */}
<Modal
title="编辑管理员"
open={editModalVisible}
onOk={handleEditSubmit}
onCancel={() => setEditModalVisible(false)}
width={600}
>
<Form form={editForm} layout="vertical">
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="phone" label="手机号">
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select placeholder="请选择角色">
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="super_admin"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select placeholder="请选择状态">
<Select.Option value="active"></Select.Option>
<Select.Option value="frozen"></Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* 重置密码 */}
<Modal
title="重置密码"
open={passwordModalVisible}
onOk={handlePasswordSubmit}
onCancel={() => setPasswordModalVisible(false)}
width={500}
>
<Form form={passwordForm} layout="vertical">
<Form.Item
name="password"
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, max: 50, message: '密码长度为6-50个字符' },
]}
>
<Input.Password placeholder="请输入新密码" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AdminManage;
@@ -1,20 +1,33 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd'; import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd';
import { getServiceFeeConfig, updateServiceFeeConfig } from '@/api/config'; import { getServiceFeeConfig, updateServiceFeeConfig, getWithdrawConfig, updateMerchantMinWithdrawAmount, updatePlatformMinWithdrawAmount } from '@/api/config';
import StorageSettings from './StorageSettings'; import StorageSettings from './StorageSettings';
const SystemSettings: React.FC = () => { const SystemSettings: React.FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [savingWithdraw, setSavingWithdraw] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [withdrawForm] = Form.useForm();
const fetchConfig = async () => { const fetchConfig = async () => {
setLoading(true); setLoading(true);
try { try {
const res: any = await getServiceFeeConfig(); const [feeRes, withdrawRes]: any = await Promise.all([
form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 }); getServiceFeeConfig(),
getWithdrawConfig(),
]);
form.setFieldsValue({ serviceFeeRate: feeRes.data?.rate || 0.05 });
withdrawForm.setFieldsValue({
merchantMinWithdrawAmount: withdrawRes.data?.merchantMinWithdrawAmount || 100,
platformMinWithdrawAmount: withdrawRes.data?.platformMinWithdrawAmount || 10,
});
} catch (e) { } catch (e) {
form.setFieldsValue({ serviceFeeRate: 0.05 }); form.setFieldsValue({ serviceFeeRate: 0.05 });
withdrawForm.setFieldsValue({
merchantMinWithdrawAmount: 100,
platformMinWithdrawAmount: 10,
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -36,6 +49,23 @@ const SystemSettings: React.FC = () => {
} }
}; };
const handleSaveWithdraw = async () => {
try {
const values = await withdrawForm.validateFields();
setSavingWithdraw(true);
await Promise.all([
updateMerchantMinWithdrawAmount(values.merchantMinWithdrawAmount),
updatePlatformMinWithdrawAmount(values.platformMinWithdrawAmount),
]);
message.success('提现配置已保存');
fetchConfig();
} catch (e: any) {
if (e?.message) message.error(e.message);
} finally {
setSavingWithdraw(false);
}
};
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />; if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
return ( return (
@@ -83,6 +113,59 @@ const SystemSettings: React.FC = () => {
</div> </div>
</Card> </Card>
<Card title="提现配置" style={{ maxWidth: 600, marginBottom: 24 }}>
<Form form={withdrawForm} layout="vertical">
<Form.Item
label="商家提现最低金额"
name="merchantMinWithdrawAmount"
rules={[
{ required: true, message: '请输入商家提现最低金额' },
{ type: 'number', min: 0, message: '金额不能为负数' },
]}
extra="商家申请提现时的最低金额限制"
>
<InputNumber
min={0}
step={10}
precision={2}
style={{ width: '100%' }}
addonAfter="元"
/>
</Form.Item>
<Form.Item
label="平台提现最低金额"
name="platformMinWithdrawAmount"
rules={[
{ required: true, message: '请输入平台提现最低金额' },
{ type: 'number', min: 0, message: '金额不能为负数' },
]}
extra="平台账户申请提现时的最低金额限制"
>
<InputNumber
min={0}
step={10}
precision={2}
style={{ width: '100%' }}
addonAfter="元"
/>
</Form.Item>
<Form.Item>
<Button type="primary" loading={savingWithdraw} onClick={handleSaveWithdraw}>
</Button>
</Form.Item>
</Form>
<Divider />
<div style={{ color: '#666', fontSize: 14 }}>
<p><strong></strong></p>
<p> </p>
<p> </p>
<p> "提现门槛"</p>
</div>
</Card>
<StorageSettings /> <StorageSettings />
</div> </div>
); );
@@ -3,6 +3,7 @@ import { Card, Row, Col, Statistic, Descriptions, Button, Space, Modal, Form, In
import { WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, DollarOutlined, ExportOutlined, HistoryOutlined } from '@ant-design/icons'; import { WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, DollarOutlined, ExportOutlined, HistoryOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getPlatformAccounts, createPlatformWithdrawal, getBankCards } from '@/api/finance'; import { getPlatformAccounts, createPlatformWithdrawal, getBankCards } from '@/api/finance';
import { getWithdrawConfig } from '@/api/config';
import { formatMoney, formatDateTime } from '@rent/shared-utils'; import { formatMoney, formatDateTime } from '@rent/shared-utils';
interface PlatformAccount { interface PlatformAccount {
@@ -34,11 +35,13 @@ const PlatformWallet: React.FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [withdrawModalVisible, setWithdrawModalVisible] = useState(false); const [withdrawModalVisible, setWithdrawModalVisible] = useState(false);
const [bankCards, setBankCards] = useState<BankCard[]>([]); const [bankCards, setBankCards] = useState<BankCard[]>([]);
const [minWithdrawAmount, setMinWithdrawAmount] = useState(10);
const [form] = Form.useForm(); const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
fetchAccount(); fetchAccount();
fetchBankCards(); fetchBankCards();
fetchWithdrawConfig();
}, []); }, []);
const fetchAccount = async () => { const fetchAccount = async () => {
@@ -67,6 +70,17 @@ const PlatformWallet: React.FC = () => {
} }
}; };
const fetchWithdrawConfig = async () => {
try {
const res: any = await getWithdrawConfig();
if (res.data?.platformMinWithdrawAmount) {
setMinWithdrawAmount(res.data.platformMinWithdrawAmount);
}
} catch (error) {
console.error('获取提现配置失败', error);
}
};
const handleWithdraw = () => { const handleWithdraw = () => {
if (bankCards.length === 0) { if (bankCards.length === 0) {
message.warning('请先添加银行卡'); message.warning('请先添加银行卡');
@@ -328,6 +342,9 @@ const PlatformWallet: React.FC = () => {
{ required: true, message: '请输入提现金额' }, { required: true, message: '请输入提现金额' },
{ {
validator: (_, value) => { validator: (_, value) => {
if (value && value < minWithdrawAmount) {
return Promise.reject(`提现金额不能小于最低提现金额(${minWithdrawAmount}元)`);
}
if (value && value > withdrawableAmount) { if (value && value > withdrawableAmount) {
return Promise.reject(`提现金额不能大于可提现金额(${withdrawableAmount.toFixed(2)}元)`); return Promise.reject(`提现金额不能大于可提现金额(${withdrawableAmount.toFixed(2)}元)`);
} }
@@ -338,11 +355,12 @@ const PlatformWallet: React.FC = () => {
}, },
}, },
]} ]}
extra={`最低提现金额:${minWithdrawAmount}`}
> >
<InputNumber <InputNumber
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="请输入提现金额" placeholder="请输入提现金额"
min={0} min={minWithdrawAmount}
max={withdrawableAmount} max={withdrawableAmount}
precision={2} precision={2}
addonAfter="元" addonAfter="元"
+9 -5
View File
@@ -30,11 +30,15 @@ WECHAT_APPID=wx6b2d69c900f8f93a
WECHAT_SECRET= WECHAT_SECRET=
# 微信支付配置 # 微信支付配置
WECHAT_MCHID= WECHAT_MCHID=1234567890
WECHAT_SERIAL_NO= WECHAT_SERIAL_NO=your_certificate_serial_number
WECHAT_APIV3_KEY= WECHAT_APIV3_KEY=your_32_character_apiv3_key_here
WECHAT_PRIVATE_KEY= WECHAT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT_HERE\n-----END PRIVATE KEY-----"
WECHAT_REFUND_NOTIFY_URL=https://your-domain.com/api/payment/wechat/refund-notify WECHAT_PAY_NOTIFY_URL=https://yourdomain.com/api/app/payment/wechat/notify
WECHAT_REFUND_NOTIFY_URL=https://yourdomain.com/api/app/payment/wechat/refund-notify
# API基础地址
API_BASE_URL=https://yourdomain.com
# 支付宝小程序 # 支付宝小程序
ALIPAY_APPID= ALIPAY_APPID=
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { WechatPayService } from './wechat-pay.service';
@Module({
imports: [ConfigModule],
providers: [WechatPayService],
exports: [WechatPayService],
})
export class PaymentModule {}
@@ -0,0 +1,199 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Wechatpay, Payment } from 'wechatpay-node-v3';
@Injectable()
export class WechatPayService {
private readonly logger = new Logger(WechatPayService.name);
private pay: Wechatpay;
constructor(private configService: ConfigService) {
const appid = this.configService.get<string>('WECHAT_APPID');
const mchid = this.configService.get<string>('WECHAT_MCHID');
const privateKey = this.configService.get<string>('WECHAT_PRIVATE_KEY');
const serialNo = this.configService.get<string>('WECHAT_SERIAL_NO');
const apiv3Key = this.configService.get<string>('WECHAT_APIV3_KEY');
if (!appid || !mchid || !privateKey || !serialNo || !apiv3Key) {
this.logger.warn('微信支付配置不完整,支付功能将不可用');
return;
}
this.pay = new Wechatpay({
appid,
mchid,
privateKey: Buffer.from(privateKey.replace(/\\n/g, '\n')),
serialNo,
apiv3Key,
});
}
/**
* JSAPI下单(小程序支付)
*/
async createJsapiOrder(params: {
orderNo: string;
description: string;
amount: number;
openid: string;
notifyUrl: string;
}) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.transactions_jsapi({
appid: this.configService.get<string>('WECHAT_APPID'),
mchid: this.configService.get<string>('WECHAT_MCHID'),
description: params.description,
out_trade_no: params.orderNo,
notify_url: params.notifyUrl,
amount: {
total: Math.round(params.amount * 100), // 转换为分
currency: 'CNY',
},
payer: {
openid: params.openid,
},
});
this.logger.log(`微信支付下单成功: ${params.orderNo}`);
return result;
} catch (error) {
this.logger.error(`微信支付下单失败: ${error.message}`, error.stack);
throw new Error(`微信支付下单失败: ${error.message}`);
}
}
/**
* 查询订单
*/
async queryOrder(orderNo: string) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.query({
out_trade_no: orderNo,
});
return result;
} catch (error) {
this.logger.error(`查询微信支付订单失败: ${error.message}`, error.stack);
throw new Error(`查询订单失败: ${error.message}`);
}
}
/**
* 关闭订单
*/
async closeOrder(orderNo: string) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
await this.pay.close({
out_trade_no: orderNo,
});
this.logger.log(`关闭微信支付订单: ${orderNo}`);
} catch (error) {
this.logger.error(`关闭微信支付订单失败: ${error.message}`, error.stack);
throw new Error(`关闭订单失败: ${error.message}`);
}
}
/**
* 申请退款
*/
async refund(params: {
orderNo: string;
refundNo: string;
totalAmount: number;
refundAmount: number;
reason: string;
notifyUrl?: string;
}) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.refunds({
out_trade_no: params.orderNo,
out_refund_no: params.refundNo,
reason: params.reason,
notify_url: params.notifyUrl,
amount: {
refund: Math.round(params.refundAmount * 100),
total: Math.round(params.totalAmount * 100),
currency: 'CNY',
},
});
this.logger.log(`微信支付退款成功: ${params.refundNo}`);
return result;
} catch (error) {
this.logger.error(`微信支付退款失败: ${error.message}`, error.stack);
throw new Error(`退款失败: ${error.message}`);
}
}
/**
* 查询退款
*/
async queryRefund(refundNo: string) {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
const result = await this.pay.find_refunds({
out_refund_no: refundNo,
});
return result;
} catch (error) {
this.logger.error(`查询微信退款失败: ${error.message}`, error.stack);
throw new Error(`查询退款失败: ${error.message}`);
}
}
/**
* 验证回调签名
*/
verifySignature(params: {
timestamp: string;
nonce: string;
body: string;
signature: string;
serial: string;
}): boolean {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
return this.pay.verifySign(params);
} catch (error) {
this.logger.error(`验证微信支付签名失败: ${error.message}`, error.stack);
return false;
}
}
/**
* 解密回调数据
*/
decryptData(ciphertext: string, nonce: string, associatedData: string): any {
if (!this.pay) {
throw new Error('微信支付未配置');
}
try {
return this.pay.decipher_gcm(ciphertext, associatedData, nonce);
} catch (error) {
this.logger.error(`解密微信支付数据失败: ${error.message}`, error.stack);
throw new Error(`解密数据失败: ${error.message}`);
}
}
}
@@ -0,0 +1,21 @@
export enum AdminRole {
SUPER_ADMIN = 'super_admin',
ADMIN = 'admin',
OPERATOR = 'operator',
}
export enum AdminStatus {
ACTIVE = 'active',
FROZEN = 'frozen',
}
export const ADMIN_ROLE_LABELS = {
[AdminRole.SUPER_ADMIN]: '超级管理员',
[AdminRole.ADMIN]: '管理员',
[AdminRole.OPERATOR]: '运营人员',
};
export const ADMIN_STATUS_LABELS = {
[AdminStatus.ACTIVE]: '正常',
[AdminStatus.FROZEN]: '冻结',
};
@@ -1,6 +1,7 @@
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator'; import { ROLES_KEY } from '../decorators/roles.decorator';
import { AdminRole } from '../constants/admin.constant';
@Injectable() @Injectable()
export class RolesGuard implements CanActivate { export class RolesGuard implements CanActivate {
@@ -17,6 +18,12 @@ export class RolesGuard implements CanActivate {
} }
const { user } = context.switchToHttp().getRequest(); const { user } = context.switchToHttp().getRequest();
// 超级管理员拥有所有权限
if (user?.role === AdminRole.SUPER_ADMIN) {
return true;
}
if (!user || !requiredRoles.includes(user.role)) { if (!user || !requiredRoles.includes(user.role)) {
throw new ForbiddenException('无权限访问'); throw new ForbiddenException('无权限访问');
} }
+1
View File
@@ -6,3 +6,4 @@ export * from './guards/roles.guard';
export * from './decorators/roles.decorator'; export * from './decorators/roles.decorator';
export * from './decorators/current-user.decorator'; export * from './decorators/current-user.decorator';
export * from './decorators/current-seller.decorator'; export * from './decorators/current-seller.decorator';
export * from './constants/admin.constant';
+5 -4
View File
@@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
@Entity('admins') @Entity('admins')
export class Admin { export class Admin {
@@ -22,12 +23,12 @@ export class Admin {
email: string; email: string;
@Index() @Index()
@Column({ type: 'enum', enum: ['super_admin', 'admin', 'operator'], default: 'admin', comment: '角色' }) @Column({ type: 'enum', enum: AdminRole, default: AdminRole.ADMIN, comment: '角色' })
role: 'super_admin' | 'admin' | 'operator'; role: AdminRole;
@Index() @Index()
@Column({ type: 'enum', enum: ['active', 'frozen'], default: 'active', comment: '状态' }) @Column({ type: 'enum', enum: AdminStatus, default: AdminStatus.ACTIVE, comment: '状态' })
status: 'active' | 'frozen'; status: AdminStatus;
@Column({ name: 'last_login_at', type: 'datetime', nullable: true, comment: '最后登录时间' }) @Column({ name: 'last_login_at', type: 'datetime', nullable: true, comment: '最后登录时间' })
lastLoginAt: Date; lastLoginAt: Date;
@@ -0,0 +1,61 @@
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
import { AdminManageService } from './admin-manage.service';
import { CreateAdminDto, UpdateAdminDto, UpdateAdminPasswordDto, QueryAdminDto } from './dto/admin.dto';
import { JwtAuthGuard, RolesGuard, Roles, AdminRole } from '@/common';
@ApiTags('管理端-管理员管理')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN)
@Controller('admin/admins')
export class AdminManageController {
constructor(private readonly adminManageService: AdminManageService) {}
@Get()
@ApiOperation({ summary: '获取管理员列表' })
async getAdminList(@Query() query: QueryAdminDto) {
return this.adminManageService.getAdminList(query);
}
@Get(':id')
@ApiOperation({ summary: '获取管理员详情' })
@ApiParam({ name: 'id', description: '管理员ID' })
async getAdminById(@Param('id') id: number) {
return this.adminManageService.getAdminById(id);
}
@Post()
@ApiOperation({ summary: '创建管理员' })
async createAdmin(@Body() dto: CreateAdminDto) {
return this.adminManageService.createAdmin(dto);
}
@Put(':id')
@ApiOperation({ summary: '更新管理员信息' })
@ApiParam({ name: 'id', description: '管理员ID' })
async updateAdmin(@Param('id') id: number, @Body() dto: UpdateAdminDto) {
return this.adminManageService.updateAdmin(id, dto);
}
@Put(':id/password')
@ApiOperation({ summary: '重置管理员密码' })
@ApiParam({ name: 'id', description: '管理员ID' })
async updateAdminPassword(@Param('id') id: number, @Body() dto: UpdateAdminPasswordDto) {
return this.adminManageService.updateAdminPassword(id, dto);
}
@Put(':id/toggle-status')
@ApiOperation({ summary: '切换管理员状态' })
@ApiParam({ name: 'id', description: '管理员ID' })
async toggleAdminStatus(@Param('id') id: number) {
return this.adminManageService.toggleAdminStatus(id);
}
@Delete(':id')
@ApiOperation({ summary: '删除管理员' })
@ApiParam({ name: 'id', description: '管理员ID' })
async deleteAdmin(@Param('id') id: number) {
return this.adminManageService.deleteAdmin(id);
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Admin } from '@/entities/admin.entity';
import { AdminManageController } from './admin-manage.controller';
import { AdminManageService } from './admin-manage.service';
@Module({
imports: [TypeOrmModule.forFeature([Admin])],
controllers: [AdminManageController],
providers: [AdminManageService],
exports: [AdminManageService],
})
export class AdminManageModule {}
@@ -0,0 +1,119 @@
import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import { Admin } from '@/entities/admin.entity';
import { CreateAdminDto, UpdateAdminDto, UpdateAdminPasswordDto, QueryAdminDto } from './dto/admin.dto';
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminManageService {
constructor(
@InjectRepository(Admin)
private adminRepo: Repository<Admin>,
) {}
async getAdminList(query: QueryAdminDto) {
const { username, name, role, status, page = 1, pageSize = 20 } = query;
const queryBuilder = this.adminRepo.createQueryBuilder('admin');
if (username) {
queryBuilder.andWhere('admin.username LIKE :username', { username: `%${username}%` });
}
if (name) {
queryBuilder.andWhere('admin.name LIKE :name', { name: `%${name}%` });
}
if (role) {
queryBuilder.andWhere('admin.role = :role', { role });
}
if (status) {
queryBuilder.andWhere('admin.status = :status', { status });
}
queryBuilder.orderBy('admin.createdAt', 'DESC');
const skip = (page - 1) * pageSize;
queryBuilder.skip(skip).take(pageSize);
const [items, total] = await queryBuilder.getManyAndCount();
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
async getAdminById(id: number) {
const admin = await this.adminRepo.findOne({ where: { id } });
if (!admin) {
throw new NotFoundException('管理员不存在');
}
return admin;
}
async createAdmin(dto: CreateAdminDto) {
const existingAdmin = await this.adminRepo.findOne({ where: { username: dto.username } });
if (existingAdmin) {
throw new ConflictException('用户名已存在');
}
const hashedPassword = await bcrypt.hash(dto.password, 10);
const admin = this.adminRepo.create({
...dto,
password: hashedPassword,
});
return this.adminRepo.save(admin);
}
async updateAdmin(id: number, dto: UpdateAdminDto) {
const admin = await this.getAdminById(id);
Object.assign(admin, dto);
return this.adminRepo.save(admin);
}
async updateAdminPassword(id: number, dto: UpdateAdminPasswordDto) {
const admin = await this.getAdminById(id);
const hashedPassword = await bcrypt.hash(dto.password, 10);
admin.password = hashedPassword;
await this.adminRepo.save(admin);
return { message: '密码修改成功' };
}
async deleteAdmin(id: number) {
const admin = await this.getAdminById(id);
if (admin.role === AdminRole.SUPER_ADMIN) {
throw new BadRequestException('不能删除超级管理员');
}
await this.adminRepo.remove(admin);
return { message: '删除成功' };
}
async toggleAdminStatus(id: number) {
const admin = await this.getAdminById(id);
if (admin.role === AdminRole.SUPER_ADMIN) {
throw new BadRequestException('不能冻结超级管理员');
}
admin.status = admin.status === AdminStatus.ACTIVE ? AdminStatus.FROZEN : AdminStatus.ACTIVE;
return this.adminRepo.save(admin);
}
}
@@ -0,0 +1,106 @@
import { IsString, IsEmail, IsEnum, IsOptional, MinLength, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
export class CreateAdminDto {
@ApiProperty({ description: '用户名', example: 'admin001' })
@IsString()
@MinLength(3)
@MaxLength(50)
username: string;
@ApiProperty({ description: '密码', example: 'Admin@123' })
@IsString()
@MinLength(6)
@MaxLength(50)
password: string;
@ApiProperty({ description: '姓名', example: '张三' })
@IsString()
@MaxLength(50)
name: string;
@ApiPropertyOptional({ description: '手机号', example: '13800138000' })
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
@ApiPropertyOptional({ description: '邮箱', example: 'admin@example.com' })
@IsOptional()
@IsEmail()
@MaxLength(100)
email?: string;
@ApiProperty({ description: '角色', enum: AdminRole, example: AdminRole.ADMIN })
@IsEnum(AdminRole)
role: AdminRole;
}
export class UpdateAdminDto {
@ApiPropertyOptional({ description: '姓名', example: '张三' })
@IsOptional()
@IsString()
@MaxLength(50)
name?: string;
@ApiPropertyOptional({ description: '手机号', example: '13800138000' })
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
@ApiPropertyOptional({ description: '邮箱', example: 'admin@example.com' })
@IsOptional()
@IsEmail()
@MaxLength(100)
email?: string;
@ApiPropertyOptional({ description: '角色', enum: AdminRole, example: AdminRole.ADMIN })
@IsOptional()
@IsEnum(AdminRole)
role?: AdminRole;
@ApiPropertyOptional({ description: '状态', enum: AdminStatus, example: AdminStatus.ACTIVE })
@IsOptional()
@IsEnum(AdminStatus)
status?: AdminStatus;
}
export class UpdateAdminPasswordDto {
@ApiProperty({ description: '新密码', example: 'NewPass@123' })
@IsString()
@MinLength(6)
@MaxLength(50)
password: string;
}
export class QueryAdminDto {
@ApiPropertyOptional({ description: '用户名', example: 'admin' })
@IsOptional()
@IsString()
username?: string;
@ApiPropertyOptional({ description: '姓名', example: '张三' })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({ description: '角色', enum: AdminRole })
@IsOptional()
@IsEnum(AdminRole)
role?: AdminRole;
@ApiPropertyOptional({ description: '状态', enum: AdminStatus })
@IsOptional()
@IsEnum(AdminStatus)
status?: AdminStatus;
@ApiPropertyOptional({ description: '页码', example: 1 })
@IsOptional()
page?: number;
@ApiPropertyOptional({ description: '每页数量', example: 20 })
@IsOptional()
pageSize?: number;
}
@@ -10,6 +10,7 @@ import { AdminActivityModule } from './activity/activity.module';
import { AdminConfigModule } from './config/config.module'; import { AdminConfigModule } from './config/config.module';
import { AdminFinanceModule } from './finance/finance.module'; import { AdminFinanceModule } from './finance/finance.module';
import { AdminWebsiteModule } from './website/website.module'; import { AdminWebsiteModule } from './website/website.module';
import { AdminManageModule } from './admin-manage/admin-manage.module';
@Module({ @Module({
imports: [ imports: [
@@ -24,6 +25,7 @@ import { AdminWebsiteModule } from './website/website.module';
AdminConfigModule, AdminConfigModule,
AdminFinanceModule, AdminFinanceModule,
AdminWebsiteModule, AdminWebsiteModule,
AdminManageModule,
], ],
}) })
export class AdminModule {} export class AdminModule {}
@@ -11,6 +11,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { Admin } from '@/entities/admin.entity'; import { Admin } from '@/entities/admin.entity';
import { AdminStatus } from '@/common/constants/admin.constant';
import { import {
AdminLoginDto, AdminLoginDto,
CreateAdminDto, CreateAdminDto,
@@ -131,12 +132,12 @@ export class AdminAuthService {
} }
async freeze(id: number) { async freeze(id: number) {
await this.adminRepo.update(id, { status: 'frozen' }); await this.adminRepo.update(id, { status: AdminStatus.FROZEN });
return { message: '已冻结' }; return { message: '已冻结' };
} }
async unfreeze(id: number) { async unfreeze(id: number) {
await this.adminRepo.update(id, { status: 'active' }); await this.adminRepo.update(id, { status: AdminStatus.ACTIVE });
return { message: '已解冻' }; return { message: '已解冻' };
} }
@@ -144,8 +145,7 @@ export class AdminAuthService {
const payload = { const payload = {
sub: admin.id, sub: admin.id,
username: admin.username, username: admin.username,
role: 'admin', role: admin.role,
adminRole: admin.role,
type: 'admin', type: 'admin',
}; };
const accessToken = await this.jwtService.signAsync(payload); const accessToken = await this.jwtService.signAsync(payload);
@@ -40,4 +40,31 @@ export class AdminConfigController {
await this.uploadService.updateStorageConfig(body); await this.uploadService.updateStorageConfig(body);
return this.uploadService.getStorageConfig(); return this.uploadService.getStorageConfig();
} }
@Get('withdraw')
@ApiOperation({ summary: '获取提现配置' })
async getWithdrawConfig() {
const [merchantMin, platformMin] = await Promise.all([
this.configService.getMerchantMinWithdrawAmount(),
this.configService.getPlatformMinWithdrawAmount(),
]);
return {
merchantMinWithdrawAmount: merchantMin,
platformMinWithdrawAmount: platformMin,
};
}
@Put('withdraw/merchant-min')
@ApiOperation({ summary: '设置商家提现最低金额' })
async setMerchantMinWithdrawAmount(@Body() body: { amount: number }) {
await this.configService.setMerchantMinWithdrawAmount(body.amount);
return { amount: await this.configService.getMerchantMinWithdrawAmount() };
}
@Put('withdraw/platform-min')
@ApiOperation({ summary: '设置平台提现最低金额' })
async setPlatformMinWithdrawAmount(@Body() body: { amount: number }) {
await this.configService.setPlatformMinWithdrawAmount(body.amount);
return { amount: await this.configService.getPlatformMinWithdrawAmount() };
}
} }
@@ -12,6 +12,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { WithdrawalService } from '@/modules/shared/finance/withdrawal.service'; import { WithdrawalService } from '@/modules/shared/finance/withdrawal.service';
import { JwtAuthGuard, RolesGuard } from '@/common'; import { JwtAuthGuard, RolesGuard } from '@/common';
import { Roles } from '@/common/decorators/roles.decorator'; import { Roles } from '@/common/decorators/roles.decorator';
import { AdminRole } from '@/common/constants/admin.constant';
import { CurrentUser } from '@/common/decorators/current-user.decorator'; import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { import {
CreatePlatformWithdrawalDto, CreatePlatformWithdrawalDto,
@@ -25,7 +26,7 @@ import {
@ApiTags('提现管理(管理员)') @ApiTags('提现管理(管理员)')
@Controller('admin/finance/withdrawals') @Controller('admin/finance/withdrawals')
@UseGuards(JwtAuthGuard, RolesGuard) @UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin') @Roles(AdminRole.SUPER_ADMIN)
@ApiBearerAuth() @ApiBearerAuth()
export class WithdrawalAdminController { export class WithdrawalAdminController {
constructor(private readonly withdrawalService: WithdrawalService) {} constructor(private readonly withdrawalService: WithdrawalService) {}
@@ -15,6 +15,7 @@ import { UserFinanceModule } from './finance/finance.module';
import { UserActivityModule } from './activity/activity.module'; import { UserActivityModule } from './activity/activity.module';
import { RoomModule } from './room/room.module'; import { RoomModule } from './room/room.module';
import { LocationModule } from './location/location.module'; import { LocationModule } from './location/location.module';
import { PaymentModule } from './payment/payment.module';
import { MerchantController } from './merchant/merchant.controller'; import { MerchantController } from './merchant/merchant.controller';
import { MerchantService } from '@/modules/merchant/merchant.service'; import { MerchantService } from '@/modules/merchant/merchant.service';
@@ -31,6 +32,7 @@ import { MerchantService } from '@/modules/merchant/merchant.service';
UserActivityModule, UserActivityModule,
RoomModule, RoomModule,
LocationModule, LocationModule,
PaymentModule,
], ],
controllers: [MerchantController], controllers: [MerchantController],
providers: [MerchantService], providers: [MerchantService],
@@ -10,6 +10,7 @@ import { UserActivityModule } from '@/modules/app/activity/activity.module';
import { ConfigModule } from '@/modules/shared/config/config.module'; import { ConfigModule } from '@/modules/shared/config/config.module';
import { FinanceModule } from '@/modules/shared/finance/finance.module'; import { FinanceModule } from '@/modules/shared/finance/finance.module';
import { UserCouponModule } from '@/modules/app/coupon/coupon.module'; import { UserCouponModule } from '@/modules/app/coupon/coupon.module';
import { PaymentModule } from '@/modules/shared/payment/payment.module';
@Module({ @Module({
imports: [ imports: [
@@ -18,6 +19,7 @@ import { UserCouponModule } from '@/modules/app/coupon/coupon.module';
ConfigModule, ConfigModule,
FinanceModule, FinanceModule,
UserCouponModule, UserCouponModule,
PaymentModule,
], ],
controllers: [OrderController], controllers: [OrderController],
providers: [OrderService], providers: [OrderService],
@@ -11,6 +11,7 @@ import { ConfigService } from '@/modules/shared/config/config.service';
import { RefundService } from '@/modules/shared/finance/refund.service'; import { RefundService } from '@/modules/shared/finance/refund.service';
import { AccountService } from '@/modules/shared/finance/account.service'; import { AccountService } from '@/modules/shared/finance/account.service';
import { CouponService } from '@/modules/app/coupon/coupon.service'; import { CouponService } from '@/modules/app/coupon/coupon.service';
import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service';
@Injectable() @Injectable()
export class OrderService { export class OrderService {
@@ -28,6 +29,7 @@ export class OrderService {
private readonly refundService: RefundService, private readonly refundService: RefundService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly couponService: CouponService, private readonly couponService: CouponService,
private readonly wechatPayService: WechatPayService,
) {} ) {}
/** /**
@@ -320,58 +322,47 @@ export class OrderService {
throw new BadRequestException('当前订单状态不可支付'); throw new BadRequestException('当前订单状态不可支付');
} }
// 使用事务确保所有操作原子性 // 仅支持微信支付
const queryRunner = this.orderRepo.manager.connection.createQueryRunner(); if (paymentMethod !== 'wechat') {
await queryRunner.connect(); throw new BadRequestException('当前仅支持微信支付');
await queryRunner.startTransaction(); }
// 获取用户openid
const user = await this.orderRepo.manager.findOne('User', {
where: { id: userId },
select: ['id', 'openid'],
});
if (!user || !user.openid) {
throw new BadRequestException('用户未绑定微信,无法使用微信支付');
}
// 调用微信支付统一下单
const notifyUrl = this.configService.get<string>('WECHAT_PAY_NOTIFY_URL') ||
`${this.configService.get<string>('API_BASE_URL')}/api/app/payment/wechat/notify`;
try { try {
// 1. 更新订单状态为已支付 const payResult = await this.wechatPayService.createJsapiOrder({
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`; orderNo: order.orderNo,
await queryRunner.manager.update(Order, order.id, { description: `${order.room?.name || '房间'}预订`,
status: 'pending_confirm', amount: order.payAmount,
paymentMethod, openid: user.openid,
paymentNo, notifyUrl,
paidAt: new Date(),
}); });
// 2. 记录系统总账户收入(用户实付金额) // 更新订单支付方式
const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`; await this.orderRepo.update(order.id, {
await this.accountService.addSystemIncome( paymentMethod: 'wechat',
order.payAmount, });
transactionNo,
'order_payment',
order.id,
order.orderNo,
`用户支付订单:${order.orderNo}`,
);
// 3. 扣减房态库存 // 返回小程序支付参数
const checkIn = new Date(order.checkInDate); return {
const checkOut = new Date(order.checkOutDate); orderNo: order.orderNo,
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) { payAmount: order.payAmount,
const dateStr = d.toISOString().split('T')[0]; payParams: payResult,
const calendar = await queryRunner.manager.findOne(RoomCalendar, { };
where: { roomId: order.roomId, date: dateStr },
});
if (!calendar) {
throw new BadRequestException(`房态日历数据异常:${dateStr}`);
}
await queryRunner.manager.update(RoomCalendar, calendar.id, {
sold: calendar.sold + order.roomCount,
});
}
// 提交事务
await queryRunner.commitTransaction();
return { message: '支付成功', paymentNo };
} catch (error) { } catch (error) {
// 回滚事务 throw new BadRequestException(`发起支付失败: ${error.message}`);
await queryRunner.rollbackTransaction();
throw error;
} finally {
// 释放连接
await queryRunner.release();
} }
} }
@@ -0,0 +1,91 @@
import { Controller, Post, Body, Headers, HttpCode, HttpStatus, Logger, RawBodyRequest, Req } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiExcludeEndpoint } from '@nestjs/swagger';
import { PaymentService } from './payment.service';
import { Request } from 'express';
@ApiTags('支付回调')
@Controller('app/payment')
export class PaymentController {
private readonly logger = new Logger(PaymentController.name);
constructor(private readonly paymentService: PaymentService) {}
@Post('wechat/notify')
@HttpCode(HttpStatus.OK)
@ApiExcludeEndpoint()
async wechatNotify(
@Req() req: RawBodyRequest<Request>,
@Headers('wechatpay-signature') signature: string,
@Headers('wechatpay-timestamp') timestamp: string,
@Headers('wechatpay-nonce') nonce: string,
@Headers('wechatpay-serial') serial: string,
@Body() body: any,
) {
this.logger.log('收到微信支付回调');
try {
// 获取原始请求体
const rawBody = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(body);
// 验证签名
const isValid = await this.paymentService.verifyWechatSignature({
timestamp,
nonce,
body: rawBody,
signature,
serial,
});
if (!isValid) {
this.logger.error('微信支付回调签名验证失败');
return { code: 'FAIL', message: '签名验证失败' };
}
// 处理回调
await this.paymentService.handleWechatNotify(body);
return { code: 'SUCCESS', message: '成功' };
} catch (error) {
this.logger.error(`处理微信支付回调失败: ${error.message}`, error.stack);
return { code: 'FAIL', message: error.message };
}
}
@Post('wechat/refund-notify')
@HttpCode(HttpStatus.OK)
@ApiExcludeEndpoint()
async wechatRefundNotify(
@Req() req: RawBodyRequest<Request>,
@Headers('wechatpay-signature') signature: string,
@Headers('wechatpay-timestamp') timestamp: string,
@Headers('wechatpay-nonce') nonce: string,
@Headers('wechatpay-serial') serial: string,
@Body() body: any,
) {
this.logger.log('收到微信退款回调');
try {
const rawBody = req.rawBody ? req.rawBody.toString('utf8') : JSON.stringify(body);
const isValid = await this.paymentService.verifyWechatSignature({
timestamp,
nonce,
body: rawBody,
signature,
serial,
});
if (!isValid) {
this.logger.error('微信退款回调签名验证失败');
return { code: 'FAIL', message: '签名验证失败' };
}
await this.paymentService.handleWechatRefundNotify(body);
return { code: 'SUCCESS', message: '成功' };
} catch (error) {
this.logger.error(`处理微信退款回调失败: ${error.message}`, error.stack);
return { code: 'FAIL', message: error.message };
}
}
}
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PaymentController } from './payment.controller';
import { PaymentService } from './payment.service';
import { Order } from '@/entities/order.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { PaymentModule as SharedPaymentModule } from '@/modules/shared/payment/payment.module';
import { FinanceModule } from '@/modules/shared/finance/finance.module';
@Module({
imports: [
TypeOrmModule.forFeature([Order, RoomCalendar]),
SharedPaymentModule,
FinanceModule,
],
controllers: [PaymentController],
providers: [PaymentService],
exports: [PaymentService],
})
export class PaymentModule {}
@@ -0,0 +1,172 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '@/entities/order.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service';
import { AccountService } from '@/modules/shared/finance/account.service';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
constructor(
@InjectRepository(Order)
private orderRepo: Repository<Order>,
@InjectRepository(RoomCalendar)
private calendarRepo: Repository<RoomCalendar>,
private readonly wechatPayService: WechatPayService,
private readonly accountService: AccountService,
) {}
/**
* 验证微信支付签名
*/
async verifyWechatSignature(params: {
timestamp: string;
nonce: string;
body: string;
signature: string;
serial: string;
}): Promise<boolean> {
return this.wechatPayService.verifySignature(params);
}
/**
* 处理微信支付回调
*/
async handleWechatNotify(body: any) {
const { resource } = body;
if (!resource) {
throw new Error('回调数据格式错误');
}
// 解密数据
const decryptedData = this.wechatPayService.decryptData(
resource.ciphertext,
resource.nonce,
resource.associated_data,
);
const {
out_trade_no: orderNo,
transaction_id: transactionId,
trade_state: tradeState,
trade_state_desc: tradeStateDesc,
} = decryptedData;
this.logger.log(`微信支付回调: 订单号=${orderNo}, 交易状态=${tradeState}`);
// 查询订单
const order = await this.orderRepo.findOne({
where: { orderNo },
relations: ['room'],
});
if (!order) {
throw new Error(`订单不存在: ${orderNo}`);
}
// 如果订单已支付,直接返回成功
if (order.status !== 'pending_pay') {
this.logger.warn(`订单已处理: ${orderNo}, 当前状态=${order.status}`);
return;
}
// 只处理支付成功的回调
if (tradeState !== 'SUCCESS') {
this.logger.warn(`支付未成功: ${orderNo}, 状态=${tradeState}, 描述=${tradeStateDesc}`);
return;
}
// 使用事务处理支付成功逻辑
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. 更新订单状态为已支付
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await queryRunner.manager.update(Order, order.id, {
status: 'pending_confirm',
paymentNo,
transactionId,
paidAt: new Date(),
});
// 2. 记录系统总账户收入(用户实付金额)
const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.accountService.addSystemIncome(
order.payAmount,
transactionNo,
'order_payment',
order.id,
order.orderNo,
`用户支付订单:${order.orderNo}`,
);
// 3. 扣减房态库存(注意:创建订单时已经扣减了sold,这里不需要再扣减)
// 如果创建订单时没有扣减库存,则需要在这里扣减
// 根据之前的代码,创建订单时已经扣减了库存,所以这里不需要再次扣减
// 提交事务
await queryRunner.commitTransaction();
this.logger.log(`订单支付成功: ${orderNo}`);
} catch (error) {
// 回滚事务
await queryRunner.rollbackTransaction();
this.logger.error(`处理支付回调失败: ${error.message}`, error.stack);
throw error;
} finally {
// 释放连接
await queryRunner.release();
}
}
/**
* 处理微信退款回调
*/
async handleWechatRefundNotify(body: any) {
const { resource } = body;
if (!resource) {
throw new Error('回调数据格式错误');
}
// 解密数据
const decryptedData = this.wechatPayService.decryptData(
resource.ciphertext,
resource.nonce,
resource.associated_data,
);
const {
out_trade_no: orderNo,
out_refund_no: refundNo,
refund_status: refundStatus,
} = decryptedData;
this.logger.log(`微信退款回调: 订单号=${orderNo}, 退款单号=${refundNo}, 状态=${refundStatus}`);
// 查询订单
const order = await this.orderRepo.findOne({
where: { orderNo },
});
if (!order) {
throw new Error(`订单不存在: ${orderNo}`);
}
// 只处理退款成功的回调
if (refundStatus === 'SUCCESS') {
await this.orderRepo.update(order.id, {
status: 'refunded',
refundAt: new Date(),
});
this.logger.log(`订单退款成功: ${orderNo}`);
} else if (refundStatus === 'ABNORMAL') {
this.logger.error(`订单退款异常: ${orderNo}`);
}
}
}
@@ -54,4 +54,50 @@ export class ConfigService {
} }
await this.setConfig('service_fee_rate', rate.toString(), '软件服务费比例'); await this.setConfig('service_fee_rate', rate.toString(), '软件服务费比例');
} }
async getAllConfigs() {
return this.configRepo.find({ order: { createdAt: 'DESC' } });
}
async deleteConfig(key: string): Promise<void> {
await this.configRepo.delete({ configKey: key });
}
/**
* 获取商家提现最低金额
*/
async getMerchantMinWithdrawAmount(): Promise<number> {
const value = await this.getConfig('merchant_min_withdraw_amount');
const amount = parseFloat(value || '100');
return isNaN(amount) ? 100 : amount;
}
/**
* 获取平台提现最低金额
*/
async getPlatformMinWithdrawAmount(): Promise<number> {
const value = await this.getConfig('platform_min_withdraw_amount');
const amount = parseFloat(value || '10');
return isNaN(amount) ? 10 : amount;
}
/**
* 设置商家提现最低金额
*/
async setMerchantMinWithdrawAmount(amount: number): Promise<void> {
if (amount < 0) {
throw new Error('提现最低金额不能为负数');
}
await this.setConfig('merchant_min_withdraw_amount', amount.toString(), '商家提现最低金额(元)');
}
/**
* 设置平台提现最低金额
*/
async setPlatformMinWithdrawAmount(amount: number): Promise<void> {
if (amount < 0) {
throw new Error('提现最低金额不能为负数');
}
await this.setConfig('platform_min_withdraw_amount', amount.toString(), '平台提现最低金额(元)');
}
} }
@@ -27,6 +27,7 @@ import { ReportService } from './report.service';
import { RefundService } from './refund.service'; import { RefundService } from './refund.service';
import { BankCardService } from './bank-card.service'; import { BankCardService } from './bank-card.service';
import { MerchantModule } from '@/modules/merchant/merchant.module'; import { MerchantModule } from '@/modules/merchant/merchant.module';
import { ConfigModule } from '../config/config.module';
@Module({ @Module({
imports: [ imports: [
@@ -51,6 +52,7 @@ import { MerchantModule } from '@/modules/merchant/merchant.module';
Order, Order,
]), ]),
forwardRef(() => MerchantModule), forwardRef(() => MerchantModule),
ConfigModule,
], ],
providers: [ providers: [
SettlementService, SettlementService,
@@ -9,6 +9,7 @@ import { PlatformAccount } from '@/entities/platform-account.entity';
import { MerchantTransaction } from '@/entities/merchant-transaction.entity'; import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
import { AccountService } from './account.service'; import { AccountService } from './account.service';
import { TransactionService } from './transaction.service'; import { TransactionService } from './transaction.service';
import { ConfigService } from '../config/config.service';
@Injectable() @Injectable()
export class WithdrawalService { export class WithdrawalService {
@@ -23,6 +24,7 @@ export class WithdrawalService {
private merchantTransactionRepo: Repository<MerchantTransaction>, private merchantTransactionRepo: Repository<MerchantTransaction>,
private accountService: AccountService, private accountService: AccountService,
private transactionService: TransactionService, private transactionService: TransactionService,
private configService: ConfigService,
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {}
@@ -35,9 +37,8 @@ export class WithdrawalService {
}) { }) {
const { amount, paymentChannel } = dto; const { amount, paymentChannel } = dto;
if (amount < 10) { // 用户提现最低金额由邀请活动配置中的 withdrawThreshold 控制
throw new BadRequestException('最低提现金额为10元'); // 这里不再检查最低金额,由调用方(邀请活动模块)负责验证
}
const account = await this.accountService.getUserAccount(userId); const account = await this.accountService.getUserAccount(userId);
@@ -86,8 +87,9 @@ export class WithdrawalService {
}) { }) {
const { amount, bankName, bankAccount, accountName } = dto; const { amount, bankName, bankAccount, accountName } = dto;
if (amount < 100) { const minAmount = await this.configService.getMerchantMinWithdrawAmount();
throw new BadRequestException('最低提现金额为100元'); if (amount < minAmount) {
throw new BadRequestException(`最低提现金额为${minAmount}`);
} }
const account = await this.accountService.getMerchantAccount(merchantId); const account = await this.accountService.getMerchantAccount(merchantId);
@@ -152,8 +154,9 @@ export class WithdrawalService {
}) { }) {
const { amount, bankName, bankAccount, accountName } = dto; const { amount, bankName, bankAccount, accountName } = dto;
if (amount < 10) { const minAmount = await this.configService.getPlatformMinWithdrawAmount();
throw new BadRequestException('最低提现金额为10元'); if (amount < minAmount) {
throw new BadRequestException(`最低提现金额为${minAmount}`);
} }
const account = await this.accountService.getPlatformAccount(); const account = await this.accountService.getPlatformAccount();
+3 -1
View File
@@ -64,7 +64,9 @@ INSERT INTO `platform_configs` (`config_key`, `config_value`, `description`) VAL
('auto_complete_hours', '24', '入住后自动完成订单时间(小时)'), ('auto_complete_hours', '24', '入住后自动完成订单时间(小时)'),
('sms_enabled', 'true', '是否启用短信通知'), ('sms_enabled', 'true', '是否启用短信通知'),
('max_images_per_room', '20', '每个房源最大图片数'), ('max_images_per_room', '20', '每个房源最大图片数'),
('max_images_per_review', '9', '每条评价最大图片数'); ('max_images_per_review', '9', '每条评价最大图片数'),
('merchant_min_withdraw_amount', '100', '商家提现最低金额(元)'),
('platform_min_withdraw_amount', '10', '平台提现最低金额(元)');
-- ============================================================ -- ============================================================
-- 7. 营销活动 - 邀请返现活动 -- 7. 营销活动 - 邀请返现活动
+134
View File
@@ -0,0 +1,134 @@
# 微信支付配置说明
## 环境变量配置
`apps/server/.env.local` 文件中添加以下配置:
```env
# 微信小程序配置
WECHAT_APPID=你的小程序AppID
WECHAT_SECRET=你的小程序AppSecret
# 微信支付配置
WECHAT_MCHID=你的商户号
WECHAT_SERIAL_NO=你的API证书序列号
WECHAT_APIV3_KEY=你的APIv3密钥
# 微信支付私钥(需要转义换行符)
WECHAT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n你的私钥内容\n-----END PRIVATE KEY-----"
# 支付回调地址(需要配置为外网可访问的地址)
WECHAT_PAY_NOTIFY_URL=https://你的域名/api/app/payment/wechat/notify
# API基础地址
API_BASE_URL=https://你的域名
```
## 获取微信支付配置参数
### 1. 获取商户号 (WECHAT_MCHID)
- 登录微信支付商户平台:https://pay.weixin.qq.com
- 在"账户中心" -> "商户信息"中查看商户号
### 2. 获取API证书序列号 (WECHAT_SERIAL_NO)
- 在微信支付商户平台,进入"账户中心" -> "API安全"
- 下载API证书(apiclient_cert.pem
- 使用以下命令查看证书序列号:
```bash
openssl x509 -in apiclient_cert.pem -noout -serial
```
### 3. 获取APIv3密钥 (WECHAT_APIV3_KEY)
- 在微信支付商户平台,进入"账户中心" -> "API安全"
- 设置APIv3密钥(32位字符串)
### 4. 获取API私钥 (WECHAT_PRIVATE_KEY)
- 下载API证书后,会得到 `apiclient_key.pem` 文件
- 将文件内容复制到环境变量中,注意:
- 需要将换行符替换为 `\n`
- 整个内容用双引号包裹
- 示例:`"-----BEGIN PRIVATE KEY-----\nMIIEvQI...\n-----END PRIVATE KEY-----"`
## 配置支付回调地址
### 1. 在微信支付商户平台配置
- 登录微信支付商户平台
- 进入"产品中心" -> "开发配置"
- 配置"JSAPI支付"回调地址:`https://你的域名/api/app/payment/wechat/notify`
### 2. 确保回调地址可访问
- 回调地址必须是外网可访问的HTTPS地址
- 本地开发可以使用内网穿透工具(如ngrok、frp)
## 小程序端使用
在小程序页面中调用支付:
```typescript
import { wxPay } from '@/api/user/order';
// 创建订单后调用支付
async function handlePay(orderNo: string) {
try {
uni.showLoading({ title: '正在支付...' });
await wxPay(orderNo);
uni.hideLoading();
uni.showToast({ title: '支付成功', icon: 'success' });
// 跳转到订单详情或订单列表
uni.navigateTo({ url: `/pages/order/detail?orderNo=${orderNo}` });
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || '支付失败',
icon: 'none'
});
}
}
```
## 支付流程
1. **用户创建订单**:调用 `POST /api/app/orders` 创建订单,订单状态为 `pending_pay`
2. **发起支付**:调用 `POST /api/app/orders/pay`,后端调用微信支付统一下单接口
3. **小程序调起支付**:使用返回的支付参数调用 `uni.requestPayment`
4. **支付回调**:微信支付成功后回调 `POST /api/app/payment/wechat/notify`
5. **更新订单状态**:后端验证签名后更新订单状态为 `pending_confirm`
## 注意事项
1. **证书安全**
- 不要将私钥提交到代码仓库
- 使用 `.env.local` 文件存储敏感信息
- `.env.local` 应该在 `.gitignore` 中
2. **回调验证**
- 后端会自动验证微信支付回调的签名
- 确保回调地址配置正确且可访问
3. **订单幂等性**
- 支付回调可能会重复通知
- 代码已处理订单状态判断,避免重复处理
4. **测试环境**
- 微信支付需要使用真实的商户号和证书
- 可以使用微信支付的沙箱环境进行测试
## 退款功能
退款功能已集成在 `WechatPayService` 中,可以通过以下方式调用:
```typescript
await this.wechatPayService.refund({
orderNo: '订单号',
refundNo: '退款单号',
totalAmount: 100, // 订单总金额(元)
refundAmount: 100, // 退款金额(元)
reason: '退款原因',
notifyUrl: '退款回调地址(可选)',
});
```
退款回调地址:`POST /api/app/payment/wechat/refund-notify`
+8 -8
View File
@@ -13031,8 +13031,8 @@ snapshots:
'@next/eslint-plugin-next': 16.2.6 '@next/eslint-plugin-next': 16.2.6
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0))
@@ -13058,7 +13058,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@@ -13069,21 +13069,21 @@ snapshots:
tinyglobby: 0.2.16 tinyglobby: 0.2.16
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -13094,7 +13094,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
hasown: 2.0.3 hasown: 2.0.3
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3