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 \"日志文件不存在\")",
|
"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",
|
||||||
|
|||||||
@@ -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="元"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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="元"
|
||||||
|
|||||||
@@ -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('无权限访问');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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. 营销活动 - 邀请返现活动
|
||||||
|
|||||||
@@ -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
|
'@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
|
||||||
|
|||||||
Reference in New Issue
Block a user