feat: 迭代
This commit is contained in:
@@ -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 \"日志文件不存在\")",
|
||||
"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(git checkout *)"
|
||||
"Bash(git checkout *)",
|
||||
"Bash(grep -E \"\\\\.\\(tsx|ts|jsx|js\\)$\")",
|
||||
"Bash(xargs grep -l \"提现\\\\|withdraw\")",
|
||||
"Bash(xargs grep -l \"admin\")"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"\\tmp",
|
||||
|
||||
@@ -52,3 +52,8 @@ export function getPendingSettlementOrders(params: any) {
|
||||
export function getPendingSettlementSummary() {
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
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 type { ColumnsType } from 'antd/es/table';
|
||||
import type { Account, Transaction, TransactionType } from '@rent/shared-types';
|
||||
@@ -18,6 +18,7 @@ const Wallet: React.FC = () => {
|
||||
const [accountLoading, setAccountLoading] = useState(false);
|
||||
const [withdrawModalVisible, setWithdrawModalVisible] = useState(false);
|
||||
const [merchantInfo, setMerchantInfo] = useState<any>(null);
|
||||
const [minWithdrawAmount, setMinWithdrawAmount] = useState(100);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const {
|
||||
@@ -43,6 +44,7 @@ const Wallet: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccount();
|
||||
fetchWithdrawConfig();
|
||||
}, []);
|
||||
|
||||
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 () => {
|
||||
if (!account || account.availableAmount <= 0) {
|
||||
message.warning('可用余额不足,无法提现');
|
||||
@@ -194,7 +207,7 @@ const Wallet: React.FC = () => {
|
||||
{formatMoney(account?.availableAmount || 0)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
||||
最低提现金额:100元
|
||||
最低提现金额:{minWithdrawAmount}元
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -204,8 +217,8 @@ const Wallet: React.FC = () => {
|
||||
{ required: true, message: '请输入提现金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value && value < 100) {
|
||||
return Promise.reject('提现金额不能小于100元');
|
||||
if (value && value < minWithdrawAmount) {
|
||||
return Promise.reject(`提现金额不能小于${minWithdrawAmount}元`);
|
||||
}
|
||||
if (value && value > (account?.availableAmount || 0)) {
|
||||
return Promise.reject('提现金额不能大于可用余额');
|
||||
@@ -221,7 +234,7 @@ const Wallet: React.FC = () => {
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入提现金额"
|
||||
min={100}
|
||||
min={minWithdrawAmount}
|
||||
max={account?.availableAmount || 0}
|
||||
precision={2}
|
||||
addonAfter="元"
|
||||
|
||||
@@ -35,3 +35,46 @@ export function refundOrder(orderNo: string, reason: string) {
|
||||
export function payOrder(orderNo: string, paymentMethod: 'wechat' | 'alipay' | 'balance' = 'wechat') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import PlatformWithdrawals from '@/pages/finance/PlatformWithdrawals';
|
||||
import BankCards from '@/pages/finance/BankCards';
|
||||
import InviteManage from '@/pages/InviteManage';
|
||||
import ReviewManage from '@/pages/ReviewManage';
|
||||
import AdminManage from '@/pages/AdminManage';
|
||||
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
@@ -71,6 +72,7 @@ const App: React.FC = () => (
|
||||
<Route path="coupons/create" element={<CouponForm />} />
|
||||
<Route path="coupons/edit/:id" element={<CouponForm />} />
|
||||
<Route path="promotions" element={<Promotion />} />
|
||||
<Route path="admins" element={<AdminManage />} />
|
||||
<Route path="settings" element={<SystemSettings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -67,3 +67,72 @@ export function getPlatformStatistics() {
|
||||
export function getOrderTrend(params: { startDate: string; endDate: string }) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -7,3 +7,9 @@ export const updateServiceFeeConfig = (rate: number) => request.put('/api/admin/
|
||||
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 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,
|
||||
TagOutlined,
|
||||
CreditCardOutlined,
|
||||
SafetyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
|
||||
@@ -53,6 +54,7 @@ const menuItems = [
|
||||
{ key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' },
|
||||
{ key: '/coupons', icon: <TagOutlined />, label: '优惠券管理' },
|
||||
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
|
||||
{ key: '/admins', icon: <SafetyOutlined />, 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 { 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';
|
||||
|
||||
const SystemSettings: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savingWithdraw, setSavingWithdraw] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [withdrawForm] = Form.useForm();
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getServiceFeeConfig();
|
||||
form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 });
|
||||
const [feeRes, withdrawRes]: any = await Promise.all([
|
||||
getServiceFeeConfig(),
|
||||
getWithdrawConfig(),
|
||||
]);
|
||||
form.setFieldsValue({ serviceFeeRate: feeRes.data?.rate || 0.05 });
|
||||
withdrawForm.setFieldsValue({
|
||||
merchantMinWithdrawAmount: withdrawRes.data?.merchantMinWithdrawAmount || 100,
|
||||
platformMinWithdrawAmount: withdrawRes.data?.platformMinWithdrawAmount || 10,
|
||||
});
|
||||
} catch (e) {
|
||||
form.setFieldsValue({ serviceFeeRate: 0.05 });
|
||||
withdrawForm.setFieldsValue({
|
||||
merchantMinWithdrawAmount: 100,
|
||||
platformMinWithdrawAmount: 10,
|
||||
});
|
||||
} finally {
|
||||
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 }} />;
|
||||
|
||||
return (
|
||||
@@ -83,6 +113,59 @@ const SystemSettings: React.FC = () => {
|
||||
</div>
|
||||
</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 />
|
||||
</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 { useNavigate } from 'react-router-dom';
|
||||
import { getPlatformAccounts, createPlatformWithdrawal, getBankCards } from '@/api/finance';
|
||||
import { getWithdrawConfig } from '@/api/config';
|
||||
import { formatMoney, formatDateTime } from '@rent/shared-utils';
|
||||
|
||||
interface PlatformAccount {
|
||||
@@ -34,11 +35,13 @@ const PlatformWallet: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [withdrawModalVisible, setWithdrawModalVisible] = useState(false);
|
||||
const [bankCards, setBankCards] = useState<BankCard[]>([]);
|
||||
const [minWithdrawAmount, setMinWithdrawAmount] = useState(10);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccount();
|
||||
fetchBankCards();
|
||||
fetchWithdrawConfig();
|
||||
}, []);
|
||||
|
||||
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 = () => {
|
||||
if (bankCards.length === 0) {
|
||||
message.warning('请先添加银行卡');
|
||||
@@ -328,6 +342,9 @@ const PlatformWallet: React.FC = () => {
|
||||
{ required: true, message: '请输入提现金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value && value < minWithdrawAmount) {
|
||||
return Promise.reject(`提现金额不能小于最低提现金额(${minWithdrawAmount}元)`);
|
||||
}
|
||||
if (value && value > withdrawableAmount) {
|
||||
return Promise.reject(`提现金额不能大于可提现金额(${withdrawableAmount.toFixed(2)}元)`);
|
||||
}
|
||||
@@ -338,11 +355,12 @@ const PlatformWallet: React.FC = () => {
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={`最低提现金额:${minWithdrawAmount}元`}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入提现金额"
|
||||
min={0}
|
||||
min={minWithdrawAmount}
|
||||
max={withdrawableAmount}
|
||||
precision={2}
|
||||
addonAfter="元"
|
||||
|
||||
@@ -30,11 +30,15 @@ WECHAT_APPID=wx6b2d69c900f8f93a
|
||||
WECHAT_SECRET=
|
||||
|
||||
# 微信支付配置
|
||||
WECHAT_MCHID=
|
||||
WECHAT_SERIAL_NO=
|
||||
WECHAT_APIV3_KEY=
|
||||
WECHAT_PRIVATE_KEY=
|
||||
WECHAT_REFUND_NOTIFY_URL=https://your-domain.com/api/payment/wechat/refund-notify
|
||||
WECHAT_MCHID=1234567890
|
||||
WECHAT_SERIAL_NO=your_certificate_serial_number
|
||||
WECHAT_APIV3_KEY=your_32_character_apiv3_key_here
|
||||
WECHAT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT_HERE\n-----END PRIVATE KEY-----"
|
||||
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=
|
||||
|
||||
@@ -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 { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
import { AdminRole } from '../constants/admin.constant';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
@@ -17,6 +18,12 @@ export class RolesGuard implements CanActivate {
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if (user?.role === AdminRole.SUPER_ADMIN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!user || !requiredRoles.includes(user.role)) {
|
||||
throw new ForbiddenException('无权限访问');
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './guards/roles.guard';
|
||||
export * from './decorators/roles.decorator';
|
||||
export * from './decorators/current-user.decorator';
|
||||
export * from './decorators/current-seller.decorator';
|
||||
export * from './constants/admin.constant';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
import { AdminRole, AdminStatus } from '@/common/constants/admin.constant';
|
||||
|
||||
@Entity('admins')
|
||||
export class Admin {
|
||||
@@ -22,12 +23,12 @@ export class Admin {
|
||||
email: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'enum', enum: ['super_admin', 'admin', 'operator'], default: 'admin', comment: '角色' })
|
||||
role: 'super_admin' | 'admin' | 'operator';
|
||||
@Column({ type: 'enum', enum: AdminRole, default: AdminRole.ADMIN, comment: '角色' })
|
||||
role: AdminRole;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'enum', enum: ['active', 'frozen'], default: 'active', comment: '状态' })
|
||||
status: 'active' | 'frozen';
|
||||
@Column({ type: 'enum', enum: AdminStatus, default: AdminStatus.ACTIVE, comment: '状态' })
|
||||
status: AdminStatus;
|
||||
|
||||
@Column({ name: 'last_login_at', type: 'datetime', nullable: true, comment: '最后登录时间' })
|
||||
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 { AdminFinanceModule } from './finance/finance.module';
|
||||
import { AdminWebsiteModule } from './website/website.module';
|
||||
import { AdminManageModule } from './admin-manage/admin-manage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -24,6 +25,7 @@ import { AdminWebsiteModule } from './website/website.module';
|
||||
AdminConfigModule,
|
||||
AdminFinanceModule,
|
||||
AdminWebsiteModule,
|
||||
AdminManageModule,
|
||||
],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Admin } from '@/entities/admin.entity';
|
||||
import { AdminStatus } from '@/common/constants/admin.constant';
|
||||
import {
|
||||
AdminLoginDto,
|
||||
CreateAdminDto,
|
||||
@@ -131,12 +132,12 @@ export class AdminAuthService {
|
||||
}
|
||||
|
||||
async freeze(id: number) {
|
||||
await this.adminRepo.update(id, { status: 'frozen' });
|
||||
await this.adminRepo.update(id, { status: AdminStatus.FROZEN });
|
||||
return { message: '已冻结' };
|
||||
}
|
||||
|
||||
async unfreeze(id: number) {
|
||||
await this.adminRepo.update(id, { status: 'active' });
|
||||
await this.adminRepo.update(id, { status: AdminStatus.ACTIVE });
|
||||
return { message: '已解冻' };
|
||||
}
|
||||
|
||||
@@ -144,8 +145,7 @@ export class AdminAuthService {
|
||||
const payload = {
|
||||
sub: admin.id,
|
||||
username: admin.username,
|
||||
role: 'admin',
|
||||
adminRole: admin.role,
|
||||
role: admin.role,
|
||||
type: 'admin',
|
||||
};
|
||||
const accessToken = await this.jwtService.signAsync(payload);
|
||||
|
||||
@@ -40,4 +40,31 @@ export class AdminConfigController {
|
||||
await this.uploadService.updateStorageConfig(body);
|
||||
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 { JwtAuthGuard, RolesGuard } from '@/common';
|
||||
import { Roles } from '@/common/decorators/roles.decorator';
|
||||
import { AdminRole } from '@/common/constants/admin.constant';
|
||||
import { CurrentUser } from '@/common/decorators/current-user.decorator';
|
||||
import {
|
||||
CreatePlatformWithdrawalDto,
|
||||
@@ -25,7 +26,7 @@ import {
|
||||
@ApiTags('提现管理(管理员)')
|
||||
@Controller('admin/finance/withdrawals')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@Roles(AdminRole.SUPER_ADMIN)
|
||||
@ApiBearerAuth()
|
||||
export class WithdrawalAdminController {
|
||||
constructor(private readonly withdrawalService: WithdrawalService) {}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { UserFinanceModule } from './finance/finance.module';
|
||||
import { UserActivityModule } from './activity/activity.module';
|
||||
import { RoomModule } from './room/room.module';
|
||||
import { LocationModule } from './location/location.module';
|
||||
import { PaymentModule } from './payment/payment.module';
|
||||
import { MerchantController } from './merchant/merchant.controller';
|
||||
import { MerchantService } from '@/modules/merchant/merchant.service';
|
||||
|
||||
@@ -31,6 +32,7 @@ import { MerchantService } from '@/modules/merchant/merchant.service';
|
||||
UserActivityModule,
|
||||
RoomModule,
|
||||
LocationModule,
|
||||
PaymentModule,
|
||||
],
|
||||
controllers: [MerchantController],
|
||||
providers: [MerchantService],
|
||||
|
||||
@@ -10,6 +10,7 @@ import { UserActivityModule } from '@/modules/app/activity/activity.module';
|
||||
import { ConfigModule } from '@/modules/shared/config/config.module';
|
||||
import { FinanceModule } from '@/modules/shared/finance/finance.module';
|
||||
import { UserCouponModule } from '@/modules/app/coupon/coupon.module';
|
||||
import { PaymentModule } from '@/modules/shared/payment/payment.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -18,6 +19,7 @@ import { UserCouponModule } from '@/modules/app/coupon/coupon.module';
|
||||
ConfigModule,
|
||||
FinanceModule,
|
||||
UserCouponModule,
|
||||
PaymentModule,
|
||||
],
|
||||
controllers: [OrderController],
|
||||
providers: [OrderService],
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ConfigService } from '@/modules/shared/config/config.service';
|
||||
import { RefundService } from '@/modules/shared/finance/refund.service';
|
||||
import { AccountService } from '@/modules/shared/finance/account.service';
|
||||
import { CouponService } from '@/modules/app/coupon/coupon.service';
|
||||
import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
@@ -28,6 +29,7 @@ export class OrderService {
|
||||
private readonly refundService: RefundService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly couponService: CouponService,
|
||||
private readonly wechatPayService: WechatPayService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -320,58 +322,47 @@ export class OrderService {
|
||||
throw new BadRequestException('当前订单状态不可支付');
|
||||
}
|
||||
|
||||
// 使用事务确保所有操作原子性
|
||||
const queryRunner = this.orderRepo.manager.connection.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
// 仅支持微信支付
|
||||
if (paymentMethod !== 'wechat') {
|
||||
throw new BadRequestException('当前仅支持微信支付');
|
||||
}
|
||||
|
||||
// 获取用户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 {
|
||||
// 1. 更新订单状态为已支付
|
||||
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
|
||||
await queryRunner.manager.update(Order, order.id, {
|
||||
status: 'pending_confirm',
|
||||
paymentMethod,
|
||||
paymentNo,
|
||||
paidAt: new Date(),
|
||||
const payResult = await this.wechatPayService.createJsapiOrder({
|
||||
orderNo: order.orderNo,
|
||||
description: `${order.room?.name || '房间'}预订`,
|
||||
amount: order.payAmount,
|
||||
openid: user.openid,
|
||||
notifyUrl,
|
||||
});
|
||||
|
||||
// 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}`,
|
||||
);
|
||||
// 更新订单支付方式
|
||||
await this.orderRepo.update(order.id, {
|
||||
paymentMethod: 'wechat',
|
||||
});
|
||||
|
||||
// 3. 扣减房态库存
|
||||
const checkIn = new Date(order.checkInDate);
|
||||
const checkOut = new Date(order.checkOutDate);
|
||||
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
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 };
|
||||
// 返回小程序支付参数
|
||||
return {
|
||||
orderNo: order.orderNo,
|
||||
payAmount: order.payAmount,
|
||||
payParams: payResult,
|
||||
};
|
||||
} catch (error) {
|
||||
// 回滚事务
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
// 释放连接
|
||||
await queryRunner.release();
|
||||
throw new BadRequestException(`发起支付失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(), '软件服务费比例');
|
||||
}
|
||||
|
||||
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 { BankCardService } from './bank-card.service';
|
||||
import { MerchantModule } from '@/modules/merchant/merchant.module';
|
||||
import { ConfigModule } from '../config/config.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -51,6 +52,7 @@ import { MerchantModule } from '@/modules/merchant/merchant.module';
|
||||
Order,
|
||||
]),
|
||||
forwardRef(() => MerchantModule),
|
||||
ConfigModule,
|
||||
],
|
||||
providers: [
|
||||
SettlementService,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PlatformAccount } from '@/entities/platform-account.entity';
|
||||
import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
|
||||
import { AccountService } from './account.service';
|
||||
import { TransactionService } from './transaction.service';
|
||||
import { ConfigService } from '../config/config.service';
|
||||
|
||||
@Injectable()
|
||||
export class WithdrawalService {
|
||||
@@ -23,6 +24,7 @@ export class WithdrawalService {
|
||||
private merchantTransactionRepo: Repository<MerchantTransaction>,
|
||||
private accountService: AccountService,
|
||||
private transactionService: TransactionService,
|
||||
private configService: ConfigService,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
@@ -35,9 +37,8 @@ export class WithdrawalService {
|
||||
}) {
|
||||
const { amount, paymentChannel } = dto;
|
||||
|
||||
if (amount < 10) {
|
||||
throw new BadRequestException('最低提现金额为10元');
|
||||
}
|
||||
// 用户提现最低金额由邀请活动配置中的 withdrawThreshold 控制
|
||||
// 这里不再检查最低金额,由调用方(邀请活动模块)负责验证
|
||||
|
||||
const account = await this.accountService.getUserAccount(userId);
|
||||
|
||||
@@ -86,8 +87,9 @@ export class WithdrawalService {
|
||||
}) {
|
||||
const { amount, bankName, bankAccount, accountName } = dto;
|
||||
|
||||
if (amount < 100) {
|
||||
throw new BadRequestException('最低提现金额为100元');
|
||||
const minAmount = await this.configService.getMerchantMinWithdrawAmount();
|
||||
if (amount < minAmount) {
|
||||
throw new BadRequestException(`最低提现金额为${minAmount}元`);
|
||||
}
|
||||
|
||||
const account = await this.accountService.getMerchantAccount(merchantId);
|
||||
@@ -152,8 +154,9 @@ export class WithdrawalService {
|
||||
}) {
|
||||
const { amount, bankName, bankAccount, accountName } = dto;
|
||||
|
||||
if (amount < 10) {
|
||||
throw new BadRequestException('最低提现金额为10元');
|
||||
const minAmount = await this.configService.getPlatformMinWithdrawAmount();
|
||||
if (amount < minAmount) {
|
||||
throw new BadRequestException(`最低提现金额为${minAmount}元`);
|
||||
}
|
||||
|
||||
const account = await this.accountService.getPlatformAccount();
|
||||
|
||||
@@ -64,7 +64,9 @@ INSERT INTO `platform_configs` (`config_key`, `config_value`, `description`) VAL
|
||||
('auto_complete_hours', '24', '入住后自动完成订单时间(小时)'),
|
||||
('sms_enabled', 'true', '是否启用短信通知'),
|
||||
('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. 营销活动 - 邀请返现活动
|
||||
|
||||
@@ -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`
|
||||
Generated
+8
-8
@@ -13031,8 +13031,8 @@ snapshots:
|
||||
'@next/eslint-plugin-next': 16.2.6
|
||||
eslint: 9.39.4(jiti@2.7.0)
|
||||
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-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(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-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-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))
|
||||
@@ -13058,7 +13058,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -13069,21 +13069,21 @@ snapshots:
|
||||
tinyglobby: 0.2.16
|
||||
unrs-resolver: 1.11.1
|
||||
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:
|
||||
- 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:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
eslint: 9.39.4(jiti@2.7.0)
|
||||
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:
|
||||
- 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:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -13094,7 +13094,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.4(jiti@2.7.0)
|
||||
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
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
||||
Reference in New Issue
Block a user