From f021b43f054922a226de9f43c6528639fc0ce3b5 Mon Sep 17 00:00:00 2001 From: xiaoquan <838115837@qq.com> Date: Mon, 1 Jun 2026 09:36:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- apps/merchant-admin/src/api/finance.ts | 5 + .../src/pages/finance/Wallet.tsx | 23 +- apps/miniapp/src/api/user/order.ts | 43 ++ apps/platform-admin/src/App.tsx | 2 + apps/platform-admin/src/api/admin.ts | 69 +++ apps/platform-admin/src/api/config.ts | 8 +- .../platform-admin/src/layouts/MainLayout.tsx | 2 + apps/platform-admin/src/pages/AdminManage.tsx | 416 ++++++++++++++++++ .../src/pages/SystemSettings.tsx | 89 +++- .../src/pages/finance/PlatformWallet.tsx | 20 +- apps/server/.env.example | 14 +- .../modules/shared/payment/payment.module.ts | 10 + .../shared/payment/wechat-pay.service.ts | 199 +++++++++ .../src/common/constants/admin.constant.ts | 21 + apps/server/src/common/guards/roles.guard.ts | 7 + apps/server/src/common/index.ts | 3 +- apps/server/src/entities/admin.entity.ts | 9 +- .../admin-manage/admin-manage.controller.ts | 61 +++ .../admin/admin-manage/admin-manage.module.ts | 13 + .../admin-manage/admin-manage.service.ts | 119 +++++ .../admin/admin-manage/dto/admin.dto.ts | 106 +++++ apps/server/src/modules/admin/admin.module.ts | 2 + .../src/modules/admin/auth/auth.service.ts | 8 +- .../modules/admin/config/config.controller.ts | 27 ++ .../finance/withdrawal-admin.controller.ts | 3 +- apps/server/src/modules/app/app.module.ts | 2 + .../src/modules/app/order/order.module.ts | 2 + .../src/modules/app/order/order.service.ts | 83 ++-- .../modules/app/payment/payment.controller.ts | 91 ++++ .../src/modules/app/payment/payment.module.ts | 20 + .../modules/app/payment/payment.service.ts | 172 ++++++++ .../modules/shared/config/config.service.ts | 46 ++ .../modules/shared/finance/finance.module.ts | 2 + .../shared/finance/withdrawal.service.ts | 17 +- database/seeds/001_init_data.sql | 4 +- docs/WECHAT_PAY_SETUP.md | 134 ++++++ pnpm-lock.yaml | 16 +- 38 files changed, 1785 insertions(+), 88 deletions(-) create mode 100644 apps/platform-admin/src/pages/AdminManage.tsx create mode 100644 apps/server/apps/server/src/modules/shared/payment/payment.module.ts create mode 100644 apps/server/apps/server/src/modules/shared/payment/wechat-pay.service.ts create mode 100644 apps/server/src/common/constants/admin.constant.ts create mode 100644 apps/server/src/modules/admin/admin-manage/admin-manage.controller.ts create mode 100644 apps/server/src/modules/admin/admin-manage/admin-manage.module.ts create mode 100644 apps/server/src/modules/admin/admin-manage/admin-manage.service.ts create mode 100644 apps/server/src/modules/admin/admin-manage/dto/admin.dto.ts create mode 100644 apps/server/src/modules/app/payment/payment.controller.ts create mode 100644 apps/server/src/modules/app/payment/payment.module.ts create mode 100644 apps/server/src/modules/app/payment/payment.service.ts create mode 100644 docs/WECHAT_PAY_SETUP.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cf694fe..62240f8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -97,7 +97,10 @@ "Bash(cd /d/project/company/rent/apps/server && tail -100 logs/*.log 2>/dev/null | grep -A 5 -B 5 \"周结算\" | tail -50 || echo \"日志文件不存在\")", "Read(//d/d/project/company/rent/apps/server/**)", "Bash(node -e \"const dayjs = require\\('dayjs'\\); console.log\\('今天是周几:', dayjs\\('2026-05-28'\\).day\\(\\)\\); console.log\\('0=周日, 1=周一, ..., 6=周六'\\);\")", - "Bash(git checkout *)" + "Bash(git checkout *)", + "Bash(grep -E \"\\\\.\\(tsx|ts|jsx|js\\)$\")", + "Bash(xargs grep -l \"提现\\\\|withdraw\")", + "Bash(xargs grep -l \"admin\")" ], "additionalDirectories": [ "\\tmp", diff --git a/apps/merchant-admin/src/api/finance.ts b/apps/merchant-admin/src/api/finance.ts index e06b10b..29e66cc 100644 --- a/apps/merchant-admin/src/api/finance.ts +++ b/apps/merchant-admin/src/api/finance.ts @@ -52,3 +52,8 @@ export function getPendingSettlementOrders(params: any) { export function getPendingSettlementSummary() { return request.get('/api/merchant/finance/settlements/pending/summary'); } + +// 配置相关 +export function getWithdrawConfig() { + return request.get('/api/admin/config/withdraw'); +} diff --git a/apps/merchant-admin/src/pages/finance/Wallet.tsx b/apps/merchant-admin/src/pages/finance/Wallet.tsx index affc173..6209abe 100644 --- a/apps/merchant-admin/src/pages/finance/Wallet.tsx +++ b/apps/merchant-admin/src/pages/finance/Wallet.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Card, Button, Table, message, Modal, Form, InputNumber, Input, Space, Alert } from 'antd'; import { useNavigate } from 'react-router-dom'; import { ExportOutlined } from '@ant-design/icons'; -import { getMerchantAccount, getMerchantTransactions, applyWithdrawal } from '@/api/finance'; +import { getMerchantAccount, getMerchantTransactions, applyWithdrawal, getWithdrawConfig } from '@/api/finance'; import { getMerchantInfo } from '@/api/auth'; import type { ColumnsType } from 'antd/es/table'; import type { Account, Transaction, TransactionType } from '@rent/shared-types'; @@ -18,6 +18,7 @@ const Wallet: React.FC = () => { const [accountLoading, setAccountLoading] = useState(false); const [withdrawModalVisible, setWithdrawModalVisible] = useState(false); const [merchantInfo, setMerchantInfo] = useState(null); + const [minWithdrawAmount, setMinWithdrawAmount] = useState(100); const [form] = Form.useForm(); const { @@ -43,6 +44,7 @@ const Wallet: React.FC = () => { useEffect(() => { fetchAccount(); + fetchWithdrawConfig(); }, []); const fetchAccount = async () => { @@ -57,6 +59,17 @@ const Wallet: React.FC = () => { } }; + const fetchWithdrawConfig = async () => { + try { + const res: any = await getWithdrawConfig(); + if (res.data?.merchantMinWithdrawAmount) { + setMinWithdrawAmount(res.data.merchantMinWithdrawAmount); + } + } catch (error) { + console.error('获取提现配置失败', error); + } + }; + const handleWithdraw = async () => { if (!account || account.availableAmount <= 0) { message.warning('可用余额不足,无法提现'); @@ -194,7 +207,7 @@ const Wallet: React.FC = () => { {formatMoney(account?.availableAmount || 0)}
- 最低提现金额:100元 + 最低提现金额:{minWithdrawAmount}元
{ { required: true, message: '请输入提现金额' }, { validator: (_, value) => { - if (value && value < 100) { - return Promise.reject('提现金额不能小于100元'); + if (value && value < minWithdrawAmount) { + return Promise.reject(`提现金额不能小于${minWithdrawAmount}元`); } if (value && value > (account?.availableAmount || 0)) { return Promise.reject('提现金额不能大于可用余额'); @@ -221,7 +234,7 @@ const Wallet: React.FC = () => { { + 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; + } +} diff --git a/apps/platform-admin/src/App.tsx b/apps/platform-admin/src/App.tsx index 0fbb88b..a07690a 100644 --- a/apps/platform-admin/src/App.tsx +++ b/apps/platform-admin/src/App.tsx @@ -29,6 +29,7 @@ import PlatformWithdrawals from '@/pages/finance/PlatformWithdrawals'; import BankCards from '@/pages/finance/BankCards'; import InviteManage from '@/pages/InviteManage'; import ReviewManage from '@/pages/ReviewManage'; +import AdminManage from '@/pages/AdminManage'; const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { const token = localStorage.getItem('admin_token'); @@ -71,6 +72,7 @@ const App: React.FC = () => ( } /> } /> } /> + } /> } /> diff --git a/apps/platform-admin/src/api/admin.ts b/apps/platform-admin/src/api/admin.ts index 9b5e0c7..54634b0 100644 --- a/apps/platform-admin/src/api/admin.ts +++ b/apps/platform-admin/src/api/admin.ts @@ -67,3 +67,72 @@ export function getPlatformStatistics() { export function getOrderTrend(params: { startDate: string; endDate: string }) { return request.get('/api/admin/finance/reports/trend', { params }); } + +// 管理员管理 +export interface Admin { + id: number; + username: string; + name: string; + phone?: string; + email?: string; + role: 'super_admin' | 'admin' | 'operator'; + status: 'active' | 'frozen'; + lastLoginAt?: string; + lastLoginIp?: string; + createdAt: string; + updatedAt: string; +} + +export interface CreateAdminParams { + username: string; + password: string; + name: string; + phone?: string; + email?: string; + role: 'super_admin' | 'admin' | 'operator'; +} + +export interface UpdateAdminParams { + name?: string; + phone?: string; + email?: string; + role?: 'super_admin' | 'admin' | 'operator'; + status?: 'active' | 'frozen'; +} + +export interface QueryAdminParams { + username?: string; + name?: string; + role?: 'super_admin' | 'admin' | 'operator'; + status?: 'active' | 'frozen'; + page?: number; + pageSize?: number; +} + +export function getAdminList(params: QueryAdminParams) { + return request.get('/api/admin/admins', { params }); +} + +export function getAdminById(id: number) { + return request.get(`/api/admin/admins/${id}`); +} + +export function createAdmin(data: CreateAdminParams) { + return request.post('/api/admin/admins', data); +} + +export function updateAdmin(id: number, data: UpdateAdminParams) { + return request.put(`/api/admin/admins/${id}`, data); +} + +export function updateAdminPassword(id: number, password: string) { + return request.put(`/api/admin/admins/${id}/password`, { password }); +} + +export function toggleAdminStatus(id: number) { + return request.put(`/api/admin/admins/${id}/toggle-status`); +} + +export function deleteAdmin(id: number) { + return request.delete(`/api/admin/admins/${id}`); +} diff --git a/apps/platform-admin/src/api/config.ts b/apps/platform-admin/src/api/config.ts index 9c575e6..53a2e63 100644 --- a/apps/platform-admin/src/api/config.ts +++ b/apps/platform-admin/src/api/config.ts @@ -6,4 +6,10 @@ export const updateServiceFeeConfig = (rate: number) => request.put('/api/admin/ export const getStorageConfig = () => request.get('/api/admin/config/storage'); -export const updateStorageConfig = (data: Record) => request.put('/api/admin/config/storage', data); \ No newline at end of file +export const updateStorageConfig = (data: Record) => 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 }); \ No newline at end of file diff --git a/apps/platform-admin/src/layouts/MainLayout.tsx b/apps/platform-admin/src/layouts/MainLayout.tsx index 6ee8430..4e29b6e 100644 --- a/apps/platform-admin/src/layouts/MainLayout.tsx +++ b/apps/platform-admin/src/layouts/MainLayout.tsx @@ -21,6 +21,7 @@ import { BarChartOutlined, TagOutlined, CreditCardOutlined, + SafetyOutlined, } from '@ant-design/icons'; import { useAuthStore } from '@/store/auth'; @@ -53,6 +54,7 @@ const menuItems = [ { key: '/invite', icon: , label: '邀请返现' }, { key: '/coupons', icon: , label: '优惠券管理' }, { key: '/promotions', icon: , label: '推广管理' }, + { key: '/admins', icon: , label: '管理员管理' }, { key: '/settings', icon: , label: '系统设置' }, ]; diff --git a/apps/platform-admin/src/pages/AdminManage.tsx b/apps/platform-admin/src/pages/AdminManage.tsx new file mode 100644 index 0000000..06f5857 --- /dev/null +++ b/apps/platform-admin/src/pages/AdminManage.tsx @@ -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([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [filters, setFilters] = useState({}); + + const [createModalVisible, setCreateModalVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [passwordModalVisible, setPasswordModalVisible] = useState(false); + const [currentAdmin, setCurrentAdmin] = useState(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 = [ + { + 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 {roleInfo?.label || role}; + }, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (status: string) => { + const statusInfo = STATUS_MAP[status as keyof typeof STATUS_MAP]; + return {statusInfo?.label || status}; + }, + }, + { + 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) => ( + + + + {record.role !== 'super_admin' && ( + <> + + handleDelete(record)} + okText="确定" + cancelText="取消" + > + + + + )} + + ), + }, + ]; + + return ( +
+

管理员管理

+ + + + + + + `共 ${total} 条`, + onChange: (page, pageSize) => { + setPage(page); + setPageSize(pageSize); + }, + }} + /> + + + {/* 创建管理员 */} + setCreateModalVisible(false)} + width={600} + > +
+ + + + + + + + + + + + + + + + + + + +
+ + {/* 编辑管理员 */} + setEditModalVisible(false)} + width={600} + > +
+ + + + + + + + + + + + + + + + +
+ + {/* 重置密码 */} + setPasswordModalVisible(false)} + width={500} + > +
+ + + + +
+ + ); +}; + +export default AdminManage; diff --git a/apps/platform-admin/src/pages/SystemSettings.tsx b/apps/platform-admin/src/pages/SystemSettings.tsx index 0c0626b..3b137fd 100644 --- a/apps/platform-admin/src/pages/SystemSettings.tsx +++ b/apps/platform-admin/src/pages/SystemSettings.tsx @@ -1,20 +1,33 @@ import React, { useEffect, useState } from 'react'; import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd'; -import { getServiceFeeConfig, updateServiceFeeConfig } from '@/api/config'; +import { getServiceFeeConfig, updateServiceFeeConfig, getWithdrawConfig, updateMerchantMinWithdrawAmount, updatePlatformMinWithdrawAmount } from '@/api/config'; import StorageSettings from './StorageSettings'; const SystemSettings: React.FC = () => { const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); + const [savingWithdraw, setSavingWithdraw] = useState(false); const [form] = Form.useForm(); + const [withdrawForm] = Form.useForm(); const fetchConfig = async () => { setLoading(true); try { - const res: any = await getServiceFeeConfig(); - form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 }); + const [feeRes, withdrawRes]: any = await Promise.all([ + getServiceFeeConfig(), + getWithdrawConfig(), + ]); + form.setFieldsValue({ serviceFeeRate: feeRes.data?.rate || 0.05 }); + withdrawForm.setFieldsValue({ + merchantMinWithdrawAmount: withdrawRes.data?.merchantMinWithdrawAmount || 100, + platformMinWithdrawAmount: withdrawRes.data?.platformMinWithdrawAmount || 10, + }); } catch (e) { form.setFieldsValue({ serviceFeeRate: 0.05 }); + withdrawForm.setFieldsValue({ + merchantMinWithdrawAmount: 100, + platformMinWithdrawAmount: 10, + }); } finally { setLoading(false); } @@ -36,6 +49,23 @@ const SystemSettings: React.FC = () => { } }; + const handleSaveWithdraw = async () => { + try { + const values = await withdrawForm.validateFields(); + setSavingWithdraw(true); + await Promise.all([ + updateMerchantMinWithdrawAmount(values.merchantMinWithdrawAmount), + updatePlatformMinWithdrawAmount(values.platformMinWithdrawAmount), + ]); + message.success('提现配置已保存'); + fetchConfig(); + } catch (e: any) { + if (e?.message) message.error(e.message); + } finally { + setSavingWithdraw(false); + } + }; + if (loading) return ; return ( @@ -83,6 +113,59 @@ const SystemSettings: React.FC = () => { + +
+ + + + + + + + + + + + + +
+

说明:

+

• 商家提现:商家在商家后台申请提现时,提现金额必须大于等于设置的最低金额

+

• 平台提现:平台管理员在平台后台申请提现时,提现金额必须大于等于设置的最低金额

+

• 用户提现:用户提现最低金额由邀请活动配置中的"提现门槛"控制

+
+
+ ); diff --git a/apps/platform-admin/src/pages/finance/PlatformWallet.tsx b/apps/platform-admin/src/pages/finance/PlatformWallet.tsx index 7907f52..0fcb7af 100644 --- a/apps/platform-admin/src/pages/finance/PlatformWallet.tsx +++ b/apps/platform-admin/src/pages/finance/PlatformWallet.tsx @@ -3,6 +3,7 @@ import { Card, Row, Col, Statistic, Descriptions, Button, Space, Modal, Form, In import { WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, DollarOutlined, ExportOutlined, HistoryOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { getPlatformAccounts, createPlatformWithdrawal, getBankCards } from '@/api/finance'; +import { getWithdrawConfig } from '@/api/config'; import { formatMoney, formatDateTime } from '@rent/shared-utils'; interface PlatformAccount { @@ -34,11 +35,13 @@ const PlatformWallet: React.FC = () => { const [loading, setLoading] = useState(false); const [withdrawModalVisible, setWithdrawModalVisible] = useState(false); const [bankCards, setBankCards] = useState([]); + const [minWithdrawAmount, setMinWithdrawAmount] = useState(10); const [form] = Form.useForm(); useEffect(() => { fetchAccount(); fetchBankCards(); + fetchWithdrawConfig(); }, []); const fetchAccount = async () => { @@ -67,6 +70,17 @@ const PlatformWallet: React.FC = () => { } }; + const fetchWithdrawConfig = async () => { + try { + const res: any = await getWithdrawConfig(); + if (res.data?.platformMinWithdrawAmount) { + setMinWithdrawAmount(res.data.platformMinWithdrawAmount); + } + } catch (error) { + console.error('获取提现配置失败', error); + } + }; + const handleWithdraw = () => { if (bankCards.length === 0) { message.warning('请先添加银行卡'); @@ -328,6 +342,9 @@ const PlatformWallet: React.FC = () => { { required: true, message: '请输入提现金额' }, { validator: (_, value) => { + if (value && value < minWithdrawAmount) { + return Promise.reject(`提现金额不能小于最低提现金额(${minWithdrawAmount}元)`); + } if (value && value > withdrawableAmount) { return Promise.reject(`提现金额不能大于可提现金额(${withdrawableAmount.toFixed(2)}元)`); } @@ -338,11 +355,12 @@ const PlatformWallet: React.FC = () => { }, }, ]} + extra={`最低提现金额:${minWithdrawAmount}元`} > ('WECHAT_APPID'); + const mchid = this.configService.get('WECHAT_MCHID'); + const privateKey = this.configService.get('WECHAT_PRIVATE_KEY'); + const serialNo = this.configService.get('WECHAT_SERIAL_NO'); + const apiv3Key = this.configService.get('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('WECHAT_APPID'), + mchid: this.configService.get('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}`); + } + } +} diff --git a/apps/server/src/common/constants/admin.constant.ts b/apps/server/src/common/constants/admin.constant.ts new file mode 100644 index 0000000..4c3e5d9 --- /dev/null +++ b/apps/server/src/common/constants/admin.constant.ts @@ -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]: '冻结', +}; diff --git a/apps/server/src/common/guards/roles.guard.ts b/apps/server/src/common/guards/roles.guard.ts index 01fd667..004de8e 100644 --- a/apps/server/src/common/guards/roles.guard.ts +++ b/apps/server/src/common/guards/roles.guard.ts @@ -1,6 +1,7 @@ import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from '../decorators/roles.decorator'; +import { AdminRole } from '../constants/admin.constant'; @Injectable() export class RolesGuard implements CanActivate { @@ -17,6 +18,12 @@ export class RolesGuard implements CanActivate { } const { user } = context.switchToHttp().getRequest(); + + // 超级管理员拥有所有权限 + if (user?.role === AdminRole.SUPER_ADMIN) { + return true; + } + if (!user || !requiredRoles.includes(user.role)) { throw new ForbiddenException('无权限访问'); } diff --git a/apps/server/src/common/index.ts b/apps/server/src/common/index.ts index abe7012..846ea49 100644 --- a/apps/server/src/common/index.ts +++ b/apps/server/src/common/index.ts @@ -5,4 +5,5 @@ export * from './guards/seller-jwt-auth.guard'; export * from './guards/roles.guard'; export * from './decorators/roles.decorator'; export * from './decorators/current-user.decorator'; -export * from './decorators/current-seller.decorator'; \ No newline at end of file +export * from './decorators/current-seller.decorator'; +export * from './constants/admin.constant'; \ No newline at end of file diff --git a/apps/server/src/entities/admin.entity.ts b/apps/server/src/entities/admin.entity.ts index 12c8660..d85e35b 100644 --- a/apps/server/src/entities/admin.entity.ts +++ b/apps/server/src/entities/admin.entity.ts @@ -1,4 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'; +import { AdminRole, AdminStatus } from '@/common/constants/admin.constant'; @Entity('admins') export class Admin { @@ -22,12 +23,12 @@ export class Admin { email: string; @Index() - @Column({ type: 'enum', enum: ['super_admin', 'admin', 'operator'], default: 'admin', comment: '角色' }) - role: 'super_admin' | 'admin' | 'operator'; + @Column({ type: 'enum', enum: AdminRole, default: AdminRole.ADMIN, comment: '角色' }) + role: AdminRole; @Index() - @Column({ type: 'enum', enum: ['active', 'frozen'], default: 'active', comment: '状态' }) - status: 'active' | 'frozen'; + @Column({ type: 'enum', enum: AdminStatus, default: AdminStatus.ACTIVE, comment: '状态' }) + status: AdminStatus; @Column({ name: 'last_login_at', type: 'datetime', nullable: true, comment: '最后登录时间' }) lastLoginAt: Date; diff --git a/apps/server/src/modules/admin/admin-manage/admin-manage.controller.ts b/apps/server/src/modules/admin/admin-manage/admin-manage.controller.ts new file mode 100644 index 0000000..4e6ae58 --- /dev/null +++ b/apps/server/src/modules/admin/admin-manage/admin-manage.controller.ts @@ -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); + } +} diff --git a/apps/server/src/modules/admin/admin-manage/admin-manage.module.ts b/apps/server/src/modules/admin/admin-manage/admin-manage.module.ts new file mode 100644 index 0000000..a054cd4 --- /dev/null +++ b/apps/server/src/modules/admin/admin-manage/admin-manage.module.ts @@ -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 {} diff --git a/apps/server/src/modules/admin/admin-manage/admin-manage.service.ts b/apps/server/src/modules/admin/admin-manage/admin-manage.service.ts new file mode 100644 index 0000000..6e41236 --- /dev/null +++ b/apps/server/src/modules/admin/admin-manage/admin-manage.service.ts @@ -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, + ) {} + + 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); + } +} diff --git a/apps/server/src/modules/admin/admin-manage/dto/admin.dto.ts b/apps/server/src/modules/admin/admin-manage/dto/admin.dto.ts new file mode 100644 index 0000000..14e21ea --- /dev/null +++ b/apps/server/src/modules/admin/admin-manage/dto/admin.dto.ts @@ -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; +} diff --git a/apps/server/src/modules/admin/admin.module.ts b/apps/server/src/modules/admin/admin.module.ts index 0852e2c..e742a14 100644 --- a/apps/server/src/modules/admin/admin.module.ts +++ b/apps/server/src/modules/admin/admin.module.ts @@ -10,6 +10,7 @@ import { AdminActivityModule } from './activity/activity.module'; import { AdminConfigModule } from './config/config.module'; import { AdminFinanceModule } from './finance/finance.module'; import { AdminWebsiteModule } from './website/website.module'; +import { AdminManageModule } from './admin-manage/admin-manage.module'; @Module({ imports: [ @@ -24,6 +25,7 @@ import { AdminWebsiteModule } from './website/website.module'; AdminConfigModule, AdminFinanceModule, AdminWebsiteModule, + AdminManageModule, ], }) export class AdminModule {} diff --git a/apps/server/src/modules/admin/auth/auth.service.ts b/apps/server/src/modules/admin/auth/auth.service.ts index de52b0c..1dd5289 100644 --- a/apps/server/src/modules/admin/auth/auth.service.ts +++ b/apps/server/src/modules/admin/auth/auth.service.ts @@ -11,6 +11,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { Admin } from '@/entities/admin.entity'; +import { AdminStatus } from '@/common/constants/admin.constant'; import { AdminLoginDto, CreateAdminDto, @@ -131,12 +132,12 @@ export class AdminAuthService { } async freeze(id: number) { - await this.adminRepo.update(id, { status: 'frozen' }); + await this.adminRepo.update(id, { status: AdminStatus.FROZEN }); return { message: '已冻结' }; } async unfreeze(id: number) { - await this.adminRepo.update(id, { status: 'active' }); + await this.adminRepo.update(id, { status: AdminStatus.ACTIVE }); return { message: '已解冻' }; } @@ -144,8 +145,7 @@ export class AdminAuthService { const payload = { sub: admin.id, username: admin.username, - role: 'admin', - adminRole: admin.role, + role: admin.role, type: 'admin', }; const accessToken = await this.jwtService.signAsync(payload); diff --git a/apps/server/src/modules/admin/config/config.controller.ts b/apps/server/src/modules/admin/config/config.controller.ts index 94cad60..b2f7d26 100644 --- a/apps/server/src/modules/admin/config/config.controller.ts +++ b/apps/server/src/modules/admin/config/config.controller.ts @@ -40,4 +40,31 @@ export class AdminConfigController { await this.uploadService.updateStorageConfig(body); return this.uploadService.getStorageConfig(); } + + @Get('withdraw') + @ApiOperation({ summary: '获取提现配置' }) + async getWithdrawConfig() { + const [merchantMin, platformMin] = await Promise.all([ + this.configService.getMerchantMinWithdrawAmount(), + this.configService.getPlatformMinWithdrawAmount(), + ]); + return { + merchantMinWithdrawAmount: merchantMin, + platformMinWithdrawAmount: platformMin, + }; + } + + @Put('withdraw/merchant-min') + @ApiOperation({ summary: '设置商家提现最低金额' }) + async setMerchantMinWithdrawAmount(@Body() body: { amount: number }) { + await this.configService.setMerchantMinWithdrawAmount(body.amount); + return { amount: await this.configService.getMerchantMinWithdrawAmount() }; + } + + @Put('withdraw/platform-min') + @ApiOperation({ summary: '设置平台提现最低金额' }) + async setPlatformMinWithdrawAmount(@Body() body: { amount: number }) { + await this.configService.setPlatformMinWithdrawAmount(body.amount); + return { amount: await this.configService.getPlatformMinWithdrawAmount() }; + } } diff --git a/apps/server/src/modules/admin/finance/withdrawal-admin.controller.ts b/apps/server/src/modules/admin/finance/withdrawal-admin.controller.ts index 97c839f..4b3f542 100644 --- a/apps/server/src/modules/admin/finance/withdrawal-admin.controller.ts +++ b/apps/server/src/modules/admin/finance/withdrawal-admin.controller.ts @@ -12,6 +12,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { WithdrawalService } from '@/modules/shared/finance/withdrawal.service'; import { JwtAuthGuard, RolesGuard } from '@/common'; import { Roles } from '@/common/decorators/roles.decorator'; +import { AdminRole } from '@/common/constants/admin.constant'; import { CurrentUser } from '@/common/decorators/current-user.decorator'; import { CreatePlatformWithdrawalDto, @@ -25,7 +26,7 @@ import { @ApiTags('提现管理(管理员)') @Controller('admin/finance/withdrawals') @UseGuards(JwtAuthGuard, RolesGuard) -@Roles('admin') +@Roles(AdminRole.SUPER_ADMIN) @ApiBearerAuth() export class WithdrawalAdminController { constructor(private readonly withdrawalService: WithdrawalService) {} diff --git a/apps/server/src/modules/app/app.module.ts b/apps/server/src/modules/app/app.module.ts index 74b1537..6ab7215 100644 --- a/apps/server/src/modules/app/app.module.ts +++ b/apps/server/src/modules/app/app.module.ts @@ -15,6 +15,7 @@ import { UserFinanceModule } from './finance/finance.module'; import { UserActivityModule } from './activity/activity.module'; import { RoomModule } from './room/room.module'; import { LocationModule } from './location/location.module'; +import { PaymentModule } from './payment/payment.module'; import { MerchantController } from './merchant/merchant.controller'; import { MerchantService } from '@/modules/merchant/merchant.service'; @@ -31,6 +32,7 @@ import { MerchantService } from '@/modules/merchant/merchant.service'; UserActivityModule, RoomModule, LocationModule, + PaymentModule, ], controllers: [MerchantController], providers: [MerchantService], diff --git a/apps/server/src/modules/app/order/order.module.ts b/apps/server/src/modules/app/order/order.module.ts index 94d1d58..9983c97 100644 --- a/apps/server/src/modules/app/order/order.module.ts +++ b/apps/server/src/modules/app/order/order.module.ts @@ -10,6 +10,7 @@ import { UserActivityModule } from '@/modules/app/activity/activity.module'; import { ConfigModule } from '@/modules/shared/config/config.module'; import { FinanceModule } from '@/modules/shared/finance/finance.module'; import { UserCouponModule } from '@/modules/app/coupon/coupon.module'; +import { PaymentModule } from '@/modules/shared/payment/payment.module'; @Module({ imports: [ @@ -18,6 +19,7 @@ import { UserCouponModule } from '@/modules/app/coupon/coupon.module'; ConfigModule, FinanceModule, UserCouponModule, + PaymentModule, ], controllers: [OrderController], providers: [OrderService], diff --git a/apps/server/src/modules/app/order/order.service.ts b/apps/server/src/modules/app/order/order.service.ts index bab1af0..dcd1fbb 100644 --- a/apps/server/src/modules/app/order/order.service.ts +++ b/apps/server/src/modules/app/order/order.service.ts @@ -11,6 +11,7 @@ import { ConfigService } from '@/modules/shared/config/config.service'; import { RefundService } from '@/modules/shared/finance/refund.service'; import { AccountService } from '@/modules/shared/finance/account.service'; import { CouponService } from '@/modules/app/coupon/coupon.service'; +import { WechatPayService } from '@/modules/shared/payment/wechat-pay.service'; @Injectable() export class OrderService { @@ -28,6 +29,7 @@ export class OrderService { private readonly refundService: RefundService, private readonly accountService: AccountService, private readonly couponService: CouponService, + private readonly wechatPayService: WechatPayService, ) {} /** @@ -320,58 +322,47 @@ export class OrderService { throw new BadRequestException('当前订单状态不可支付'); } - // 使用事务确保所有操作原子性 - const queryRunner = this.orderRepo.manager.connection.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); + // 仅支持微信支付 + if (paymentMethod !== 'wechat') { + throw new BadRequestException('当前仅支持微信支付'); + } + + // 获取用户openid + const user = await this.orderRepo.manager.findOne('User', { + where: { id: userId }, + select: ['id', 'openid'], + }); + + if (!user || !user.openid) { + throw new BadRequestException('用户未绑定微信,无法使用微信支付'); + } + + // 调用微信支付统一下单 + const notifyUrl = this.configService.get('WECHAT_PAY_NOTIFY_URL') || + `${this.configService.get('API_BASE_URL')}/api/app/payment/wechat/notify`; try { - // 1. 更新订单状态为已支付 - const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`; - await queryRunner.manager.update(Order, order.id, { - status: 'pending_confirm', - paymentMethod, - paymentNo, - paidAt: new Date(), + const payResult = await this.wechatPayService.createJsapiOrder({ + orderNo: order.orderNo, + description: `${order.room?.name || '房间'}预订`, + amount: order.payAmount, + openid: user.openid, + notifyUrl, }); - // 2. 记录系统总账户收入(用户实付金额) - const transactionNo = `TXN${Date.now()}${Math.floor(Math.random() * 10000)}`; - await this.accountService.addSystemIncome( - order.payAmount, - transactionNo, - 'order_payment', - order.id, - order.orderNo, - `用户支付订单:${order.orderNo}`, - ); + // 更新订单支付方式 + await this.orderRepo.update(order.id, { + paymentMethod: 'wechat', + }); - // 3. 扣减房态库存 - const checkIn = new Date(order.checkInDate); - const checkOut = new Date(order.checkOutDate); - for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) { - const dateStr = d.toISOString().split('T')[0]; - const calendar = await queryRunner.manager.findOne(RoomCalendar, { - where: { roomId: order.roomId, date: dateStr }, - }); - if (!calendar) { - throw new BadRequestException(`房态日历数据异常:${dateStr}`); - } - await queryRunner.manager.update(RoomCalendar, calendar.id, { - sold: calendar.sold + order.roomCount, - }); - } - - // 提交事务 - await queryRunner.commitTransaction(); - return { message: '支付成功', paymentNo }; + // 返回小程序支付参数 + return { + orderNo: order.orderNo, + payAmount: order.payAmount, + payParams: payResult, + }; } catch (error) { - // 回滚事务 - await queryRunner.rollbackTransaction(); - throw error; - } finally { - // 释放连接 - await queryRunner.release(); + throw new BadRequestException(`发起支付失败: ${error.message}`); } } diff --git a/apps/server/src/modules/app/payment/payment.controller.ts b/apps/server/src/modules/app/payment/payment.controller.ts new file mode 100644 index 0000000..f0119e7 --- /dev/null +++ b/apps/server/src/modules/app/payment/payment.controller.ts @@ -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, + @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, + @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 }; + } + } +} diff --git a/apps/server/src/modules/app/payment/payment.module.ts b/apps/server/src/modules/app/payment/payment.module.ts new file mode 100644 index 0000000..5403199 --- /dev/null +++ b/apps/server/src/modules/app/payment/payment.module.ts @@ -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 {} diff --git a/apps/server/src/modules/app/payment/payment.service.ts b/apps/server/src/modules/app/payment/payment.service.ts new file mode 100644 index 0000000..5c623fa --- /dev/null +++ b/apps/server/src/modules/app/payment/payment.service.ts @@ -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, + @InjectRepository(RoomCalendar) + private calendarRepo: Repository, + private readonly wechatPayService: WechatPayService, + private readonly accountService: AccountService, + ) {} + + /** + * 验证微信支付签名 + */ + async verifyWechatSignature(params: { + timestamp: string; + nonce: string; + body: string; + signature: string; + serial: string; + }): Promise { + 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}`); + } + } +} diff --git a/apps/server/src/modules/shared/config/config.service.ts b/apps/server/src/modules/shared/config/config.service.ts index 0f7d9cb..4316398 100644 --- a/apps/server/src/modules/shared/config/config.service.ts +++ b/apps/server/src/modules/shared/config/config.service.ts @@ -54,4 +54,50 @@ export class ConfigService { } await this.setConfig('service_fee_rate', rate.toString(), '软件服务费比例'); } + + async getAllConfigs() { + return this.configRepo.find({ order: { createdAt: 'DESC' } }); + } + + async deleteConfig(key: string): Promise { + await this.configRepo.delete({ configKey: key }); + } + + /** + * 获取商家提现最低金额 + */ + async getMerchantMinWithdrawAmount(): Promise { + const value = await this.getConfig('merchant_min_withdraw_amount'); + const amount = parseFloat(value || '100'); + return isNaN(amount) ? 100 : amount; + } + + /** + * 获取平台提现最低金额 + */ + async getPlatformMinWithdrawAmount(): Promise { + const value = await this.getConfig('platform_min_withdraw_amount'); + const amount = parseFloat(value || '10'); + return isNaN(amount) ? 10 : amount; + } + + /** + * 设置商家提现最低金额 + */ + async setMerchantMinWithdrawAmount(amount: number): Promise { + if (amount < 0) { + throw new Error('提现最低金额不能为负数'); + } + await this.setConfig('merchant_min_withdraw_amount', amount.toString(), '商家提现最低金额(元)'); + } + + /** + * 设置平台提现最低金额 + */ + async setPlatformMinWithdrawAmount(amount: number): Promise { + if (amount < 0) { + throw new Error('提现最低金额不能为负数'); + } + await this.setConfig('platform_min_withdraw_amount', amount.toString(), '平台提现最低金额(元)'); + } } \ No newline at end of file diff --git a/apps/server/src/modules/shared/finance/finance.module.ts b/apps/server/src/modules/shared/finance/finance.module.ts index 203d1ab..7cfb4ee 100644 --- a/apps/server/src/modules/shared/finance/finance.module.ts +++ b/apps/server/src/modules/shared/finance/finance.module.ts @@ -27,6 +27,7 @@ import { ReportService } from './report.service'; import { RefundService } from './refund.service'; import { BankCardService } from './bank-card.service'; import { MerchantModule } from '@/modules/merchant/merchant.module'; +import { ConfigModule } from '../config/config.module'; @Module({ imports: [ @@ -51,6 +52,7 @@ import { MerchantModule } from '@/modules/merchant/merchant.module'; Order, ]), forwardRef(() => MerchantModule), + ConfigModule, ], providers: [ SettlementService, diff --git a/apps/server/src/modules/shared/finance/withdrawal.service.ts b/apps/server/src/modules/shared/finance/withdrawal.service.ts index 73990fd..74a72c3 100644 --- a/apps/server/src/modules/shared/finance/withdrawal.service.ts +++ b/apps/server/src/modules/shared/finance/withdrawal.service.ts @@ -9,6 +9,7 @@ import { PlatformAccount } from '@/entities/platform-account.entity'; import { MerchantTransaction } from '@/entities/merchant-transaction.entity'; import { AccountService } from './account.service'; import { TransactionService } from './transaction.service'; +import { ConfigService } from '../config/config.service'; @Injectable() export class WithdrawalService { @@ -23,6 +24,7 @@ export class WithdrawalService { private merchantTransactionRepo: Repository, private accountService: AccountService, private transactionService: TransactionService, + private configService: ConfigService, private dataSource: DataSource, ) {} @@ -35,9 +37,8 @@ export class WithdrawalService { }) { const { amount, paymentChannel } = dto; - if (amount < 10) { - throw new BadRequestException('最低提现金额为10元'); - } + // 用户提现最低金额由邀请活动配置中的 withdrawThreshold 控制 + // 这里不再检查最低金额,由调用方(邀请活动模块)负责验证 const account = await this.accountService.getUserAccount(userId); @@ -86,8 +87,9 @@ export class WithdrawalService { }) { const { amount, bankName, bankAccount, accountName } = dto; - if (amount < 100) { - throw new BadRequestException('最低提现金额为100元'); + const minAmount = await this.configService.getMerchantMinWithdrawAmount(); + if (amount < minAmount) { + throw new BadRequestException(`最低提现金额为${minAmount}元`); } const account = await this.accountService.getMerchantAccount(merchantId); @@ -152,8 +154,9 @@ export class WithdrawalService { }) { const { amount, bankName, bankAccount, accountName } = dto; - if (amount < 10) { - throw new BadRequestException('最低提现金额为10元'); + const minAmount = await this.configService.getPlatformMinWithdrawAmount(); + if (amount < minAmount) { + throw new BadRequestException(`最低提现金额为${minAmount}元`); } const account = await this.accountService.getPlatformAccount(); diff --git a/database/seeds/001_init_data.sql b/database/seeds/001_init_data.sql index eb163fc..12c9670 100644 --- a/database/seeds/001_init_data.sql +++ b/database/seeds/001_init_data.sql @@ -64,7 +64,9 @@ INSERT INTO `platform_configs` (`config_key`, `config_value`, `description`) VAL ('auto_complete_hours', '24', '入住后自动完成订单时间(小时)'), ('sms_enabled', 'true', '是否启用短信通知'), ('max_images_per_room', '20', '每个房源最大图片数'), -('max_images_per_review', '9', '每条评价最大图片数'); +('max_images_per_review', '9', '每条评价最大图片数'), +('merchant_min_withdraw_amount', '100', '商家提现最低金额(元)'), +('platform_min_withdraw_amount', '10', '平台提现最低金额(元)'); -- ============================================================ -- 7. 营销活动 - 邀请返现活动 diff --git a/docs/WECHAT_PAY_SETUP.md b/docs/WECHAT_PAY_SETUP.md new file mode 100644 index 0000000..4931eef --- /dev/null +++ b/docs/WECHAT_PAY_SETUP.md @@ -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` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 757bf41..c12fc1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13031,8 +13031,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.6 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) @@ -13058,7 +13058,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -13069,21 +13069,21 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13094,7 +13094,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3