diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a4fc851..cc75bd3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -74,7 +74,18 @@ "Read(//c/Users/admin/AppData/Local/Temp/1/claude/d--project-company-rent/949fb73a-e05f-4a74-a5a8-ad2ee982a5f4/tasks/**)", "Bash(powershell -Command 'Get-NetTCPConnection -LocalPort 3000 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique | Where-Object { $_ -ne 0 } | ForEach-Object { Stop-Process -Id $_ -Force }')", "Bash(npx tsc *)", - "Bash(curl -s http://localhost:3000/api/admin/finance/platform-accounts)" + "Bash(curl -s http://localhost:3000/api/admin/finance/platform-accounts)", + "Bash(cd /d/project/company/rent && ls -la database/seeds/)", + "Read(//d/d/project/company/rent/database/**)", + "Bash(mysql -u root -p123456 -D rent_platform -e \"SELECT id, account_name, balance, total_income, total_refund, total_withdrawn FROM system_accounts;\")", + "Bash(mysql -h localhost -P 3306 -u root -pquan131735 rent_platform)", + "Bash(npx ts-node *)", + "Bash(mysql -uroot -proot rent_platform -e \"SELECT id, order_no, status, merchant_id, checkout_at, check_out_date FROM orders WHERE status = 'completed' LIMIT 5;\")", + "Bash(taskkill /F /PID 3664)", + "Bash(taskkill //F //PID 1716)", + "Bash(mysql -u root -p123456 rent_platform -e \"SELECT id, name, type, enabled, config FROM mkt_activities WHERE type='invite_cashback' LIMIT 1\")", + "Bash(node check-config.js)", + "Bash(node verify-fix.js)" ] } } diff --git a/apps/merchant-admin/src/App.tsx b/apps/merchant-admin/src/App.tsx index f758942..00cdf3b 100644 --- a/apps/merchant-admin/src/App.tsx +++ b/apps/merchant-admin/src/App.tsx @@ -16,6 +16,7 @@ import FinanceSettlementDetail from '@/pages/finance/SettlementDetail'; import FinanceWithdrawals from '@/pages/finance/Withdrawals'; import FinanceWallet from '@/pages/finance/Wallet'; import FinanceTransactions from '@/pages/finance/Transactions'; +import FinancePendingSettlement from '@/pages/finance/PendingSettlement'; import Settings from '@/pages/Settings'; const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -42,7 +43,8 @@ const App: React.FC = () => ( } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/apps/merchant-admin/src/api/finance.ts b/apps/merchant-admin/src/api/finance.ts index 901db77..e06b10b 100644 --- a/apps/merchant-admin/src/api/finance.ts +++ b/apps/merchant-admin/src/api/finance.ts @@ -43,3 +43,12 @@ export function getSettlementDetail(id: number) { export function getSettlementItems(id: number, params: any) { return request.get(`/api/merchant/finance/settlements/${id}/items`, { params }); } + +// 待结算订单相关 +export function getPendingSettlementOrders(params: any) { + return request.get('/api/merchant/finance/settlements/pending/orders', { params }); +} + +export function getPendingSettlementSummary() { + return request.get('/api/merchant/finance/settlements/pending/summary'); +} diff --git a/apps/merchant-admin/src/layouts/MainLayout.tsx b/apps/merchant-admin/src/layouts/MainLayout.tsx index 9347d6f..e505450 100644 --- a/apps/merchant-admin/src/layouts/MainLayout.tsx +++ b/apps/merchant-admin/src/layouts/MainLayout.tsx @@ -14,6 +14,7 @@ import { AuditOutlined, PayCircleOutlined, AccountBookOutlined, + ClockCircleOutlined, } from '@ant-design/icons'; import { useAuthStore } from '@/store/auth'; @@ -30,6 +31,7 @@ const menuItems = [ icon: , label: '财务管理', children: [ + { key: '/finance/pending-settlement', icon: , label: '待结算订单' }, { key: '/finance/settlements', icon: , label: '结算对账' }, { key: '/finance/withdrawals', icon: , label: '收款记录' }, { key: '/finance/wallet', icon: , label: '我的钱包' }, diff --git a/apps/merchant-admin/src/pages/finance/PendingSettlement.tsx b/apps/merchant-admin/src/pages/finance/PendingSettlement.tsx new file mode 100644 index 0000000..b2d0073 --- /dev/null +++ b/apps/merchant-admin/src/pages/finance/PendingSettlement.tsx @@ -0,0 +1,242 @@ +import React, { useEffect, useState } from 'react'; +import { Card, Table, Statistic, Row, Col, Space, Tag } from 'antd'; +import { ClockCircleOutlined, DollarOutlined, FileTextOutlined, CalendarOutlined } from '@ant-design/icons'; +import { getPendingSettlementOrders, getPendingSettlementSummary } from '@/api/finance'; +import type { ColumnsType } from 'antd/es/table'; +import { formatMoney, formatDate, formatDateTime } from '@rent/shared-utils'; +import { useTableData } from '@/hooks/useTableData'; + +interface PendingOrder { + id: number; + orderNo: string; + roomName: string; + checkInDate: string; + checkOutDate: string; + nights: number; + payAmount: number; + serviceFee: number; + merchantIncome: number; + checkoutAt: string; + contactName: string; + contactPhone: string; +} + +interface Summary { + orderCount: number; + totalPayAmount: number; + totalServiceFee: number; + totalMerchantIncome: number; + startDate: string; + lastSettlementDate: string | null; +} + +const PendingSettlement: React.FC = () => { + const [summary, setSummary] = useState(null); + const [summaryLoading, setSummaryLoading] = useState(false); + + const { + data: orders, + loading, + total, + page, + pageSize, + setPage, + setPageSize, + refresh, + } = useTableData({ + fetchFn: async (params) => { + const res = await getPendingSettlementOrders(params); + return { + list: res.data.list || [], + total: res.data.total, + page: params.page || 1, + pageSize: params.pageSize || 20, + totalPages: Math.ceil(res.data.total / (params.pageSize || 20)), + }; + }, + initialParams: { pageSize: 20 }, + }); + + useEffect(() => { + fetchSummary(); + }, []); + + const fetchSummary = async () => { + setSummaryLoading(true); + try { + const res = await getPendingSettlementSummary(); + setSummary(res.data); + } catch (error) { + console.error('获取待结算统计失败', error); + } finally { + setSummaryLoading(false); + } + }; + + const columns: ColumnsType = [ + { + title: '订单号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 180, + fixed: 'left', + }, + { + title: '房型名称', + dataIndex: 'roomName', + key: 'roomName', + width: 150, + }, + { + title: '入住日期', + dataIndex: 'checkInDate', + key: 'checkInDate', + width: 120, + render: (date: string) => formatDate(date), + }, + { + title: '离店日期', + dataIndex: 'checkOutDate', + key: 'checkOutDate', + width: 120, + render: (date: string) => formatDate(date), + }, + { + title: '入住晚数', + dataIndex: 'nights', + key: 'nights', + width: 100, + render: (nights: number) => `${nights}晚`, + }, + { + title: '订单金额', + dataIndex: 'payAmount', + key: 'payAmount', + width: 120, + render: (amount: number) => formatMoney(amount), + }, + { + title: '服务费', + dataIndex: 'serviceFee', + key: 'serviceFee', + width: 120, + render: (fee: number) => -{formatMoney(fee, '')}, + }, + { + title: '预计收入', + dataIndex: 'merchantIncome', + key: 'merchantIncome', + width: 120, + render: (income: number) => ( + {formatMoney(income)} + ), + }, + { + title: '完成时间', + dataIndex: 'checkoutAt', + key: 'checkoutAt', + width: 180, + render: (date: string) => formatDateTime(date), + }, + { + title: '联系人', + key: 'contact', + width: 150, + render: (_, record) => ( +
+
{record.contactName}
+
{record.contactPhone}
+
+ ), + }, + ]; + + return ( +
+

待结算订单

+ + + + + } + suffix="笔" + /> + + + + + } + suffix="元" + /> + + + + + } + suffix="元" + valueStyle={{ color: '#ff4d4f' }} + /> + + + + + } + suffix="元" + valueStyle={{ color: '#52c41a' }} + /> + + + + + + {summary?.lastSettlementDate && ( + } color="blue"> + 上次结算: {formatDate(summary.lastSettlementDate)} + + )} + } color="orange"> + 统计起始: {summary?.startDate ? formatDate(summary.startDate) : '-'} + + + } + > + `共 ${total} 条`, + onChange: setPage, + onShowSizeChange: (_, size) => setPageSize(size), + }} + /> + + + ); +}; + +export default PendingSettlement; diff --git a/apps/miniapp/src/api/user/wallet.ts b/apps/miniapp/src/api/user/wallet.ts index e334b9a..bcbc66b 100644 --- a/apps/miniapp/src/api/user/wallet.ts +++ b/apps/miniapp/src/api/user/wallet.ts @@ -7,21 +7,6 @@ export interface WalletInfo { availableBalance: number; } -// 交易流水接口 -export interface Transaction { - id: number; - accountId: number; - direction: 'in' | 'out'; - transactionType: string; - amount: number; - balanceBefore: number; - balanceAfter: number; - description: string; - relatedId?: number; - relatedType?: string; - createdAt: string; -} - // 提现记录接口 export interface Withdrawal { id: number; @@ -46,28 +31,10 @@ export const walletApi = { }); }, - // 获取交易流水 - getTransactions(params?: { - direction?: 'in' | 'out'; - transactionType?: string; - startDate?: string; - endDate?: string; - page?: number; - pageSize?: number; - }) { - return request<{ items: Transaction[]; total: number }>({ - url: '/api/app/finance/transactions', - method: 'GET', - data: params, - }); - }, - // 申请提现 withdraw(data: { amount: number; - accountType: 'alipay' | 'wechat'; - accountName: string; - accountNumber: string; + paymentChannel: 'wechat' | 'alipay'; }) { return request({ url: '/api/app/finance/withdraw', diff --git a/apps/miniapp/src/pages.json b/apps/miniapp/src/pages.json index 70c436d..18345c7 100644 --- a/apps/miniapp/src/pages.json +++ b/apps/miniapp/src/pages.json @@ -197,12 +197,6 @@ "navigationStyle": "custom" } }, - { - "path": "pages/wallet/transactions", - "style": { - "navigationBarTitleText": "交易流水" - } - }, { "path": "pages/wallet/withdraw", "style": { diff --git a/apps/miniapp/src/pages/index/index.vue b/apps/miniapp/src/pages/index/index.vue index 04d4b40..0b0ff41 100644 --- a/apps/miniapp/src/pages/index/index.vue +++ b/apps/miniapp/src/pages/index/index.vue @@ -32,9 +32,7 @@ 入住 - {{ nightCount }}晚 - {{ checkOutLabel }} @@ -633,11 +631,6 @@ onActivated(() => { padding: 0 32rpx; } -.sep-line { - width: 40rpx; - height: 2rpx; - background: #ddd; -} .sep-nights { font-size: 20rpx; diff --git a/apps/miniapp/src/pages/invite/index.vue b/apps/miniapp/src/pages/invite/index.vue index fcff094..11d5f28 100644 --- a/apps/miniapp/src/pages/invite/index.vue +++ b/apps/miniapp/src/pages/invite/index.vue @@ -18,37 +18,29 @@ - - - + + + + 我的收益 + + + ¥{{ wallet.availableBalance || '0.00' }} + + 满{{ config.config.withdrawThreshold }}元可提现 + - 我的收益 - - - 可提现余额(元) - - ¥ - {{ wallet.availableBalance || '0.00' }} - - - - 满10元可提现 - + + + 提现 - - - - 立即提现 - - - 提现记录 - + + 记录 @@ -119,38 +111,18 @@ 活动规则 - - - 1 + + + {{ index + 1 }} - 首单高返 - 好友首次下单,您可获得订单金额 5% 返现 + {{ rule.title }} + + - - 2 - - 持续收益 - 好友再次下单,您仍可获得 0.5% 返现奖励 - - - - - 3 - - 返现限额 - 单笔返现最低 0.01元,最高 50元 - - - - - 4 - - 提现门槛 - 余额满 10元 即可提现,实时到账 - - + + 加载中... @@ -181,7 +153,7 @@ - - diff --git a/apps/miniapp/src/pages/wallet/withdraw.vue b/apps/miniapp/src/pages/wallet/withdraw.vue index c8dafa8..ce6ada9 100644 --- a/apps/miniapp/src/pages/wallet/withdraw.vue +++ b/apps/miniapp/src/pages/wallet/withdraw.vue @@ -44,34 +44,21 @@ - + {{ type.label }} - + - 真实姓名 - - - - - {{ form.accountType === 'alipay' ? '支付宝账号' : '微信账号' }} - + + + 提现将直接到账当前登录的微信账号 + @@ -112,13 +99,10 @@ const submitting = ref(false); const form = ref({ amount: '', - accountType: 'alipay', - accountName: '', - accountNumber: '', + paymentChannel: 'wechat', }); const accountTypes = [ - { label: '支付宝', value: 'alipay', icon: 'pay-circle' }, { label: '微信', value: 'wechat', icon: 'weixin' }, ]; @@ -168,20 +152,10 @@ async function handleSubmit() { return; } - if (!form.value.accountName) { - uni.showToast({ title: '请输入真实姓名', icon: 'none' }); - return; - } - - if (!form.value.accountNumber) { - uni.showToast({ title: '请输入账号', icon: 'none' }); - return; - } - // 确认提现 uni.showModal({ title: '确认提现', - content: `确认提现 ¥${formatMoney(form.value.amount)} 到${getAccountTypeLabel(form.value.accountType)}?`, + content: `确认提现 ¥${formatMoney(form.value.amount)} 到${getAccountTypeLabel(form.value.paymentChannel)}?`, success: async (res) => { if (res.confirm) { await submitWithdraw(); @@ -196,9 +170,7 @@ async function submitWithdraw() { try { await walletApi.withdraw({ amount: Number(form.value.amount), - accountType: form.value.accountType as 'alipay' | 'wechat', - accountName: form.value.accountName, - accountNumber: form.value.accountNumber, + paymentChannel: form.value.paymentChannel as 'wechat', }); uni.showToast({ title: '提现申请已提交', icon: 'success' }); @@ -216,7 +188,6 @@ async function submitWithdraw() { function getAccountTypeLabel(type: string): string { const typeMap: Record = { - alipay: '支付宝', wechat: '微信', }; return typeMap[type] || type; @@ -233,7 +204,9 @@ function goBack() { background: #F5F7FA; display: flex; flex-direction: column; - padding-bottom: 120rpx; + padding-bottom: calc(120rpx + env(safe-area-inset-bottom)); + box-sizing: border-box; + overflow-x: hidden; } /* 导航栏 */ @@ -379,14 +352,16 @@ function goBack() { } .type-item { - flex: 1; - padding: 24rpx 16rpx; + width: 160rpx; + height: 160rpx; + padding: 24rpx; background: #F5F7FA; border-radius: 16rpx; display: flex; flex-direction: column; align-items: center; - gap: 8rpx; + justify-content: center; + gap: 12rpx; transition: all 0.3s ease; border: 2rpx solid transparent; @@ -401,7 +376,7 @@ function goBack() { } .type-text { - font-size: 24rpx; + font-size: 26rpx; color: #666; .active & { @@ -419,6 +394,22 @@ function goBack() { color: #1A1A1A; } +.wechat-tip { + display: flex; + align-items: center; + gap: 8rpx; + padding: 20rpx; + background: #FFF7E6; + border-radius: 12rpx; +} + +.wechat-tip-text { + flex: 1; + font-size: 24rpx; + color: #FF6B35; + line-height: 1.5; +} + /* 提示卡片 */ .tips-card { margin: 0 24rpx 16rpx; @@ -463,10 +454,11 @@ function goBack() { bottom: 0; left: 0; right: 0; - padding: 16rpx 24rpx; + padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #ffffff; border-top: 1rpx solid #F0F0F0; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); + box-sizing: border-box; } .submit-btn { @@ -476,6 +468,7 @@ function goBack() { border-radius: 48rpx; text-align: center; transition: all 0.3s ease; + box-sizing: border-box; &:active:not(.disabled) { transform: scale(0.98); diff --git a/apps/platform-admin/src/api/finance.ts b/apps/platform-admin/src/api/finance.ts index b29632f..9abcb42 100644 --- a/apps/platform-admin/src/api/finance.ts +++ b/apps/platform-admin/src/api/finance.ts @@ -186,3 +186,12 @@ export function getMerchantReport(merchantId: number, params: any) { export function exportReport(type: string, params: any) { return request.get(`/api/admin/finance/reports/export/${type}`, { params, responseType: 'blob' }); } + +// 服务费管理 +export function getServiceFeeStatistics() { + return request.get('/api/admin/finance/reports/service-fees/statistics'); +} + +export function getServiceFeeList(params: any) { + return request.get('/api/admin/finance/reports/service-fees/list', { params }); +} diff --git a/apps/platform-admin/src/pages/InviteManage.tsx b/apps/platform-admin/src/pages/InviteManage.tsx index bbe2db2..bc4da93 100644 --- a/apps/platform-admin/src/pages/InviteManage.tsx +++ b/apps/platform-admin/src/pages/InviteManage.tsx @@ -238,7 +238,7 @@ const InviteManage: React.FC = () => { - + diff --git a/apps/platform-admin/src/pages/finance/BankCards.tsx b/apps/platform-admin/src/pages/finance/BankCards.tsx index 1d7c3dc..d6b2380 100644 --- a/apps/platform-admin/src/pages/finance/BankCards.tsx +++ b/apps/platform-admin/src/pages/finance/BankCards.tsx @@ -240,7 +240,7 @@ const BankCards: React.FC = () => { label="卡片名称" rules={[{ required: true, message: '请输入卡片名称' }]} > - + { {/* 系统总账户金额 - 突出显示 */} 系统总账户金额} + title={系统总账户金额(资金守恒)} value={Number(overview.systemTotalAmount || 0)} prefix={} - suffix={} + suffix={元 ({getAccountDisplayName('SYSTEM_MAIN')})} precision={2} valueStyle={{ color: '#fff', fontSize: 36, fontWeight: 'bold' }} /> -
- 用户实付(¥{Number(overview.totalUserPaid || 0).toFixed(2)})- 退款(¥{Number(overview.totalRefund || 0).toFixed(2)})- 提现(¥{Number(overview.totalWithdrawn || 0).toFixed(2)}) + {/*
+ 商家账户余额(¥{Number(overview.totalMerchantBalance || 0).toFixed(2)})+ 用户账户余额(¥{Number(overview.totalUserBalance || 0).toFixed(2)})+ 平台净收益(¥{Number(overview.platformBalance || 0).toFixed(2)})
-
- = 商家账户余额(¥{Number(overview.totalMerchantBalance || 0).toFixed(2)})+ 用户账户余额(¥{Number(overview.totalUserBalance || 0).toFixed(2)})+ 平台净收益(¥{Number(overview.platformBalance || 0).toFixed(2)}) + */} +
+ 系统总账户 balance = 用户实付(¥{Number(overview.totalUserPaid || 0).toFixed(2)})- 退款(¥{Number(overview.totalRefund || 0).toFixed(2)})- 提现(¥{Number(overview.totalWithdrawn || 0).toFixed(2)})= ¥{(Number(overview.totalUserPaid || 0) - Number(overview.totalRefund || 0) - Number(overview.totalWithdrawn || 0)).toFixed(2)}
@@ -180,11 +182,16 @@ const FinanceDashboard: React.FC = () => {
} + suffix="元" + valueStyle={{ color: '#ff9800' }} /> +
+ 待结算订单金额 +
diff --git a/apps/platform-admin/src/pages/finance/ServiceFees.tsx b/apps/platform-admin/src/pages/finance/ServiceFees.tsx index fea426c..b680af7 100644 --- a/apps/platform-admin/src/pages/finance/ServiceFees.tsx +++ b/apps/platform-admin/src/pages/finance/ServiceFees.tsx @@ -3,7 +3,7 @@ import { Card, Table, DatePicker, Space, Tag, Button, Statistic, Row, Col, messa import { DownloadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { formatMoney, formatDateTime } from '@rent/shared-utils'; -import { getFinancialOverview, getMerchantTransactions } from '@/api/finance'; +import { getServiceFeeStatistics, getServiceFeeList } from '@/api/finance'; import type { Dayjs } from 'dayjs'; const { RangePicker } = DatePicker; @@ -45,7 +45,7 @@ const ServiceFees: React.FC = () => { const fetchStatistics = async () => { try { - const res = await getFinancialOverview({}); + const res = await getServiceFeeStatistics(); if (res?.data) { setStatistics({ todayCommission: res.data?.todayCommission || 0, @@ -72,7 +72,7 @@ const ServiceFees: React.FC = () => { params.endDate = dateRange[1].format('YYYY-MM-DD'); } - const res = await getMerchantTransactions(params); + const res = await getServiceFeeList(params); if (res?.data) { setDataSource(res.data?.list || []); setPagination({ diff --git a/apps/platform-admin/src/utils/accountNameMap.ts b/apps/platform-admin/src/utils/accountNameMap.ts new file mode 100644 index 0000000..a7d3961 --- /dev/null +++ b/apps/platform-admin/src/utils/accountNameMap.ts @@ -0,0 +1,31 @@ +/** + * 账户名称映射工具 + * 数据库存储英文常量,前端显示中文名称 + */ + +export const ACCOUNT_NAMES = { + SYSTEM_MAIN: 'SYSTEM_MAIN', + PLATFORM_MAIN: 'PLATFORM_MAIN', + PLATFORM_BACKUP: 'PLATFORM_BACKUP', +} as const; + +export const ACCOUNT_NAME_MAP: Record = { + SYSTEM_MAIN: '系统总账户', + PLATFORM_MAIN: '平台主账户', + PLATFORM_BACKUP: '平台备用账户', +}; + +/** + * 将英文账户名称转换为中文显示名称 + */ +export function getAccountDisplayName(accountName: string): string { + return ACCOUNT_NAME_MAP[accountName] || accountName; +} + +/** + * 将中文显示名称转换为英文账户名称 + */ +export function getAccountConstName(displayName: string): string { + const entry = Object.entries(ACCOUNT_NAME_MAP).find(([_, value]) => value === displayName); + return entry ? entry[0] : displayName; +} diff --git a/apps/server/src/constants/account.constant.ts b/apps/server/src/constants/account.constant.ts new file mode 100644 index 0000000..ae9993d --- /dev/null +++ b/apps/server/src/constants/account.constant.ts @@ -0,0 +1,15 @@ +/** + * 账户名称常量 + * 数据库存储使用这些英文常量 + */ + +export const ACCOUNT_NAMES = { + /** 系统总账户 */ + SYSTEM_MAIN: 'SYSTEM_MAIN', + /** 平台主账户 */ + PLATFORM_MAIN: 'PLATFORM_MAIN', + /** 平台备用账户 */ + PLATFORM_BACKUP: 'PLATFORM_BACKUP', +} as const; + +export type AccountName = typeof ACCOUNT_NAMES[keyof typeof ACCOUNT_NAMES]; diff --git a/apps/server/src/entities/platform-account.entity.ts b/apps/server/src/entities/platform-account.entity.ts index 755ca5d..8c557cb 100644 --- a/apps/server/src/entities/platform-account.entity.ts +++ b/apps/server/src/entities/platform-account.entity.ts @@ -5,7 +5,7 @@ export class PlatformAccount { @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true }) id: number; - @Column({ type: 'varchar', length: 50, comment: '账户名称(如:主账户、备用账户)' }) + @Column({ type: 'varchar', length: 50, comment: '账户名称(如:PLATFORM_MAIN、PLATFORM_BACKUP)' }) account_name: string; @Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额(平台净收益 = total_income - total_expense)' }) diff --git a/apps/server/src/entities/system-account.entity.ts b/apps/server/src/entities/system-account.entity.ts index 15dbece..c255e7b 100644 --- a/apps/server/src/entities/system-account.entity.ts +++ b/apps/server/src/entities/system-account.entity.ts @@ -5,7 +5,7 @@ export class SystemAccount { @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true }) id: number; - @Column({ type: 'varchar', length: 50, comment: '账户名称(如:主账户)' }) + @Column({ type: 'varchar', length: 50, comment: '账户名称(如:SYSTEM_MAIN)' }) account_name: string; @Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额(total_income - total_refund - total_withdrawn)' }) diff --git a/apps/server/src/modules/admin/activity/activity.service.ts b/apps/server/src/modules/admin/activity/activity.service.ts index 178cdec..d20d3c0 100644 --- a/apps/server/src/modules/admin/activity/activity.service.ts +++ b/apps/server/src/modules/admin/activity/activity.service.ts @@ -102,7 +102,7 @@ export class ActivityService { firstOrderRate: 0.05, secondOrderRate: 0.005, minCashback: 0.01, - maxCashback: 50, + maxCashback: 1000, withdrawThreshold: 10, maxOrderIndex: 2, }, diff --git a/apps/server/src/modules/admin/finance/report-admin.controller.ts b/apps/server/src/modules/admin/finance/report-admin.controller.ts index 2dd735c..7a0ed35 100644 --- a/apps/server/src/modules/admin/finance/report-admin.controller.ts +++ b/apps/server/src/modules/admin/finance/report-admin.controller.ts @@ -67,4 +67,26 @@ export class ReportAdminController { // TODO: 实现导出功能 return { message: '导出功能开发中' }; } + + @Get('service-fees/statistics') + @ApiOperation({ summary: '服务费统计' }) + async getServiceFeeStatistics() { + return this.reportService.getServiceFeeStatistics(); + } + + @Get('service-fees/list') + @ApiOperation({ summary: '服务费明细列表' }) + async getServiceFeeList( + @Query('page') page: number = 1, + @Query('pageSize') pageSize: number = 20, + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + return this.reportService.getServiceFeeList({ + page: Number(page), + pageSize: Number(pageSize), + startDate, + endDate, + }); + } } diff --git a/apps/server/src/modules/app/activity/activity.module.ts b/apps/server/src/modules/app/activity/activity.module.ts index 58e7e7e..f4738b0 100644 --- a/apps/server/src/modules/app/activity/activity.module.ts +++ b/apps/server/src/modules/app/activity/activity.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ActivityController } from './activity.controller'; import { ActivityService } from './activity.service'; @@ -22,7 +22,7 @@ import { FinanceModule } from '@/modules/shared/finance/finance.module'; Order, User, ]), - FinanceModule, + forwardRef(() => FinanceModule), ], controllers: [ActivityController], providers: [ActivityService], diff --git a/apps/server/src/modules/app/finance/finance-user.controller.ts b/apps/server/src/modules/app/finance/finance-user.controller.ts index 4560838..6d67ef3 100644 --- a/apps/server/src/modules/app/finance/finance-user.controller.ts +++ b/apps/server/src/modules/app/finance/finance-user.controller.ts @@ -10,12 +10,10 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '@/common'; import { CurrentUser } from '@/common/decorators/current-user.decorator'; import { WithdrawalService } from '@/modules/shared/finance/withdrawal.service'; -import { TransactionService } from '@/modules/shared/finance/transaction.service'; import { AccountService } from '@/modules/shared/finance/account.service'; import { CreateUserWithdrawalDto, QueryUserWithdrawalDto, - QueryTransactionDto, } from '@/modules/shared/finance/dto/finance.dto'; @ApiTags('财务管理(用户)') @@ -25,7 +23,6 @@ import { export class FinanceUserController { constructor( private readonly withdrawalService: WithdrawalService, - private readonly transactionService: TransactionService, private readonly accountService: AccountService, ) {} @@ -63,23 +60,4 @@ export class FinanceUserController { pageSize: dto.pageSize, }); } - - @Get('transactions') - @ApiOperation({ summary: '交易流水列表' }) - async getTransactions( - @CurrentUser('sub') userId: number, - @Query() dto: QueryTransactionDto, - ) { - const account = await this.accountService.getUserAccount(userId); - - return this.transactionService.getUserTransactions({ - accountId: account.id, - direction: dto.direction, - transactionType: dto.transactionType, - startDate: dto.startDate, - endDate: dto.endDate, - page: dto.page, - pageSize: dto.pageSize, - }); - } } diff --git a/apps/server/src/modules/app/order/order.service.ts b/apps/server/src/modules/app/order/order.service.ts index 214872b..bab1af0 100644 --- a/apps/server/src/modules/app/order/order.service.ts +++ b/apps/server/src/modules/app/order/order.service.ts @@ -320,42 +320,59 @@ export class OrderService { throw new BadRequestException('当前订单状态不可支付'); } - // 模拟支付成功 - const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`; - await this.orderRepo.update(order.id, { - status: 'pending_confirm', - paymentMethod, - paymentNo, - paidAt: new Date(), - }); + // 使用事务确保所有操作原子性 + const queryRunner = this.orderRepo.manager.connection.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - // 记录系统总账户收入(用户实付金额) - 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}`, - ); - - // 扣减房态库存 - 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 this.calendarRepo.findOne({ - where: { roomId: order.roomId, date: dateStr }, + 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(), }); - if (calendar) { - await this.calendarRepo.update(calendar.id, { + + // 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. 扣减房态库存 + 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, }); } - } - return { message: '支付成功', paymentNo }; + // 提交事务 + await queryRunner.commitTransaction(); + return { message: '支付成功', paymentNo }; + } catch (error) { + // 回滚事务 + await queryRunner.rollbackTransaction(); + throw error; + } finally { + // 释放连接 + await queryRunner.release(); + } } /** @@ -368,42 +385,59 @@ export class OrderService { throw new BadRequestException('当前订单状态不可支付'); } - // 模拟支付成功 - const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`; - await this.orderRepo.update(id, { - status: 'pending_confirm', - paymentMethod, - paymentNo, - paidAt: new Date(), - }); + // 使用事务确保所有操作原子性 + const queryRunner = this.orderRepo.manager.connection.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - // 记录系统总账户收入(用户实付金额) - 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}`, - ); - - // 扣减房态库存 - 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 this.calendarRepo.findOne({ - where: { roomId: order.roomId, date: dateStr }, + try { + // 1. 更新订单状态为已支付 + const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`; + await queryRunner.manager.update(Order, id, { + status: 'pending_confirm', + paymentMethod, + paymentNo, + paidAt: new Date(), }); - if (calendar) { - await this.calendarRepo.update(calendar.id, { + + // 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. 扣减房态库存 + 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, }); } - } - return { message: '支付成功', paymentNo }; + // 提交事务 + await queryRunner.commitTransaction(); + return { message: '支付成功', paymentNo }; + } catch (error) { + // 回滚事务 + await queryRunner.rollbackTransaction(); + throw error; + } finally { + // 释放连接 + await queryRunner.release(); + } } /** diff --git a/apps/server/src/modules/merchant/finance/finance.service.ts b/apps/server/src/modules/merchant/finance/finance.service.ts index 0bd5136..cf4e0f3 100644 --- a/apps/server/src/modules/merchant/finance/finance.service.ts +++ b/apps/server/src/modules/merchant/finance/finance.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { MerchantAccount } from '@/entities/merchant-account.entity'; import { MerchantTransaction } from '@/entities/merchant-transaction.entity'; +import { SettlementService } from '@/modules/shared/finance/settlement.service'; @Injectable() export class MerchantFinanceService { @@ -11,6 +12,7 @@ export class MerchantFinanceService { private merchantAccountRepo: Repository, @InjectRepository(MerchantTransaction) private merchantTransactionRepo: Repository, + private settlementService: SettlementService, ) {} async getMerchantAccount(merchantId: number) { @@ -33,6 +35,9 @@ export class MerchantFinanceService { await this.merchantAccountRepo.save(account); } + // 动态计算待结算金额 + const pendingSettlement = await this.settlementService.getPendingSettlementAmount(merchantId); + // 计算可用余额并返回格式化的数据 return { id: account.id, @@ -43,7 +48,7 @@ export class MerchantFinanceService { totalExpense: Number(account.total_expense), totalSettlement: Number(account.total_settlement), totalWithdraw: Number(account.total_withdraw), - pendingSettlement: Number(account.pending_settlement), + pendingSettlement: Number(pendingSettlement), debtAmount: Number(account.debt_amount), status: account.status, lastSettlementAt: account.last_settlement_at, diff --git a/apps/server/src/modules/merchant/finance/settlement-merchant.controller.ts b/apps/server/src/modules/merchant/finance/settlement-merchant.controller.ts index 539bc59..0f049bd 100644 --- a/apps/server/src/modules/merchant/finance/settlement-merchant.controller.ts +++ b/apps/server/src/modules/merchant/finance/settlement-merchant.controller.ts @@ -93,4 +93,24 @@ export class SettlementMerchantController { pageSize }; } + + @Get('pending/orders') + @ApiOperation({ summary: '查询待结算订单列表' }) + async getPendingSettlementOrders( + @CurrentSeller('sub') sellerId: number, + @Query('page') page: number = 1, + @Query('pageSize') pageSize: number = 20, + ) { + const merchantId = await this.getMerchantId(sellerId); + return this.settlementService.getPendingSettlementOrders(merchantId, page, pageSize); + } + + @Get('pending/summary') + @ApiOperation({ summary: '查询待结算订单统计' }) + async getPendingSettlementSummary( + @CurrentSeller('sub') sellerId: number, + ) { + const merchantId = await this.getMerchantId(sellerId); + return this.settlementService.getPendingSettlementSummary(merchantId); + } } diff --git a/apps/server/src/modules/merchant/order/order.module.ts b/apps/server/src/modules/merchant/order/order.module.ts index 5782c96..eb03d9a 100644 --- a/apps/server/src/modules/merchant/order/order.module.ts +++ b/apps/server/src/modules/merchant/order/order.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MerchantOrderController } from './order.controller'; import { MerchantOrderService } from './order.service'; @@ -6,11 +6,15 @@ import { Order } from '@/entities/order.entity'; import { Room } from '@/entities/room.entity'; import { RoomCalendar } from '@/entities/room-calendar.entity'; import { MerchantProfileModule } from '../profile/profile.module'; +import { UserActivityModule } from '@/modules/app/activity/activity.module'; +import { FinanceModule } from '@/modules/shared/finance/finance.module'; @Module({ imports: [ TypeOrmModule.forFeature([Order, Room, RoomCalendar]), MerchantProfileModule, + forwardRef(() => UserActivityModule), + forwardRef(() => FinanceModule), ], controllers: [MerchantOrderController], providers: [MerchantOrderService], diff --git a/apps/server/src/modules/merchant/order/order.service.ts b/apps/server/src/modules/merchant/order/order.service.ts index cb8edea..55ccd94 100644 --- a/apps/server/src/modules/merchant/order/order.service.ts +++ b/apps/server/src/modules/merchant/order/order.service.ts @@ -1,13 +1,18 @@ -import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, ForbiddenException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Order } from '@/entities/order.entity'; import { Room } from '@/entities/room.entity'; import { RoomCalendar } from '@/entities/room-calendar.entity'; import { QueryOrderDto } from './dto/order.dto'; +import { ActivityService } from '@/modules/app/activity/activity.service'; +import { AccountService } from '@/modules/shared/finance/account.service'; +import { TransactionService } from '@/modules/shared/finance/transaction.service'; @Injectable() export class MerchantOrderService { + private readonly logger = new Logger(MerchantOrderService.name); + constructor( @InjectRepository(Order) private orderRepo: Repository, @@ -15,6 +20,9 @@ export class MerchantOrderService { private roomRepo: Repository, @InjectRepository(RoomCalendar) private calendarRepo: Repository, + private readonly activityService: ActivityService, + private readonly accountService: AccountService, + private readonly transactionService: TransactionService, ) {} async findByMerchant(merchantId: number, query: QueryOrderDto) { @@ -111,6 +119,51 @@ export class MerchantOrderService { checkoutAt: new Date(), }); + // 订单完成后立即结算服务费给平台账户 + try { + const transactionNo = this.transactionService.generateTransactionNo(); + const serviceFee = Number(order.serviceFee || 0); + + if (serviceFee > 0) { + await this.accountService.addPlatformServiceFee( + serviceFee, + transactionNo, + 'order_complete', + order.id, + order.orderNo, + `订单完成服务费 - ${order.orderNo}`, + ); + this.logger.log(`订单 ${orderNo} 完成,平台服务费 ${serviceFee} 元已入账`); + } + } catch (error) { + this.logger.error(`订单 ${orderNo} 服务费结算失败: ${error.message}`); + // 不影响订单完成流程,只记录错误 + } + + // 增加商家待结算金额 + try { + const merchantIncome = Number(order.merchantIncome || 0); + if (merchantIncome > 0) { + await this.accountService.addMerchantPendingSettlement( + order.merchantId, + merchantIncome, + ); + this.logger.log(`订单 ${orderNo} 完成,商家待结算金额 ${merchantIncome} 元已增加`); + } + } catch (error) { + this.logger.error(`订单 ${orderNo} 商家待结算金额更新失败: ${error.message}`); + // 不影响订单完成流程,只记录错误 + } + + // 触发邀请返现 + try { + await this.activityService.handleOrderCompleted(order.id); + this.logger.log(`订单 ${orderNo} 完成,已触发邀请返现处理`); + } catch (error) { + this.logger.error(`订单 ${orderNo} 邀请返现处理失败: ${error.message}`); + // 不影响订单完成流程,只记录错误 + } + return { message: '已确认离店,订单已完成' }; } } diff --git a/apps/server/src/modules/shared/finance/account.service.ts b/apps/server/src/modules/shared/finance/account.service.ts index 4b153b7..9c409e7 100644 --- a/apps/server/src/modules/shared/finance/account.service.ts +++ b/apps/server/src/modules/shared/finance/account.service.ts @@ -10,6 +10,7 @@ import { MerchantTransaction } from '@/entities/merchant-transaction.entity'; import { PlatformTransaction } from '@/entities/platform-transaction.entity'; import { SystemTransaction } from '@/entities/system-transaction.entity'; import { QueryUserAccountsDto, QueryMerchantAccountsDto } from './dto/account.dto'; +import { ACCOUNT_NAMES } from '@/constants/account.constant'; @Injectable() export class AccountService { @@ -82,7 +83,7 @@ export class AccountService { /** * 获取平台账户 */ - async getPlatformAccount(accountName: string = '主账户'): Promise { + async getPlatformAccount(accountName: string = ACCOUNT_NAMES.PLATFORM_MAIN): Promise { const account = await this.platformAccountRepo.findOne({ where: { account_name: accountName } }); if (!account) { @@ -246,12 +247,15 @@ export class AccountService { } const balance = Number(account.balance); - if (balance < amount) { - throw new BadRequestException('账户余额不足'); + const frozenBalance = Number(account.frozen_balance); + const availableBalance = balance - frozenBalance; + + if (availableBalance < amount) { + throw new BadRequestException('可用余额不足'); } - account.balance = balance - amount; - account.frozen_balance = Number(account.frozen_balance) + amount; + // 只增加冻结金额,不减少余额 + account.frozen_balance = frozenBalance + amount; account.version += 1; await queryRunner.manager.save(account); @@ -287,7 +291,7 @@ export class AccountService { throw new BadRequestException('冻结余额不足'); } - account.balance = Number(account.balance) + amount; + // 只减少冻结金额,不增加余额(因为余额本来就没减少) account.frozen_balance = frozenBalance - amount; account.version += 1; @@ -303,6 +307,7 @@ export class AccountService { /** * 商家账户增加余额(结算) + * 同时减少待结算金额 */ async addMerchantBalance( merchantId: number, @@ -336,6 +341,8 @@ export class AccountService { account.balance = balanceAfter; account.total_income = Number(account.total_income) + amount; + // 结算时减少待结算金额 + account.pending_settlement = Math.max(0, Number(account.pending_settlement) - amount); account.version += 1; await queryRunner.manager.save(account); @@ -366,6 +373,40 @@ export class AccountService { } } + /** + * 商家账户增加待结算金额(订单完成时) + */ + async addMerchantPendingSettlement( + merchantId: number, + amount: number, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const account = await queryRunner.manager.findOne(MerchantAccount, { + where: { merchant_id: merchantId }, + lock: { mode: 'pessimistic_write' }, + }); + + if (!account) { + throw new NotFoundException('商家账户不存在'); + } + + account.pending_settlement = Number(account.pending_settlement) + amount; + account.version += 1; + + await queryRunner.manager.save(account); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + /** * 商家账户扣减余额(提现) */ @@ -451,7 +492,7 @@ export class AccountService { try { const account = await queryRunner.manager.findOne(PlatformAccount, { - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN }, lock: { mode: 'pessimistic_write' }, }); @@ -510,7 +551,7 @@ export class AccountService { try { const account = await queryRunner.manager.findOne(PlatformAccount, { - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN }, lock: { mode: 'pessimistic_write' }, }); @@ -569,7 +610,7 @@ export class AccountService { try { const account = await queryRunner.manager.findOne(PlatformAccount, { - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN }, lock: { mode: 'pessimistic_write' }, }); @@ -773,7 +814,7 @@ export class AccountService { /** * 获取系统总账户 */ - async getSystemAccount(accountName: string = '主账户'): Promise { + async getSystemAccount(accountName: string = ACCOUNT_NAMES.SYSTEM_MAIN): Promise { const account = await this.systemAccountRepo.findOne({ where: { account_name: accountName } }); if (!account) { @@ -802,7 +843,7 @@ export class AccountService { try { const account = await queryRunner.manager.findOne(SystemAccount, { - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN }, lock: { mode: 'pessimistic_write' }, }); @@ -861,7 +902,7 @@ export class AccountService { try { const account = await queryRunner.manager.findOne(SystemAccount, { - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN }, lock: { mode: 'pessimistic_write' }, }); @@ -920,7 +961,7 @@ export class AccountService { try { const account = await queryRunner.manager.findOne(SystemAccount, { - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN }, lock: { mode: 'pessimistic_write' }, }); diff --git a/apps/server/src/modules/shared/finance/finance.module.ts b/apps/server/src/modules/shared/finance/finance.module.ts index d6f608e..203d1ab 100644 --- a/apps/server/src/modules/shared/finance/finance.module.ts +++ b/apps/server/src/modules/shared/finance/finance.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Settlement } from '@/entities/settlement.entity'; import { SettlementItem } from '@/entities/settlement-item.entity'; @@ -50,7 +50,7 @@ import { MerchantModule } from '@/modules/merchant/merchant.module'; Merchant, Order, ]), - MerchantModule, + forwardRef(() => MerchantModule), ], providers: [ SettlementService, diff --git a/apps/server/src/modules/shared/finance/reconciliation.service.ts b/apps/server/src/modules/shared/finance/reconciliation.service.ts index ed54bc6..b2112db 100644 --- a/apps/server/src/modules/shared/finance/reconciliation.service.ts +++ b/apps/server/src/modules/shared/finance/reconciliation.service.ts @@ -14,6 +14,7 @@ import { Settlement } from '@/entities/settlement.entity'; import { UserWithdrawal } from '@/entities/user-withdrawal.entity'; import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity'; import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity'; +import { ACCOUNT_NAMES } from '@/constants/account.constant'; import dayjs from 'dayjs'; @Injectable() @@ -68,7 +69,7 @@ export class ReconciliationService { */ async performReconciliation(date: string) { const platformAccount = await this.platformAccountRepo.findOne({ - where: { account_name: '主账户' } + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN } }); const platformBalanceStart = Number(platformAccount?.balance || 0); @@ -221,7 +222,7 @@ export class ReconciliationService { */ async getAccountSummary() { const platformAccount = await this.platformAccountRepo.findOne({ - where: { account_name: '主账户' } + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN } }); const merchantStats = await this.merchantAccountRepo diff --git a/apps/server/src/modules/shared/finance/report.service.ts b/apps/server/src/modules/shared/finance/report.service.ts index bf25952..8ea2574 100644 --- a/apps/server/src/modules/shared/finance/report.service.ts +++ b/apps/server/src/modules/shared/finance/report.service.ts @@ -12,6 +12,7 @@ import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity'; import { UserWithdrawal } from '@/entities/user-withdrawal.entity'; import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity'; import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity'; +import { ACCOUNT_NAMES } from '@/constants/account.constant'; import dayjs from 'dayjs'; @Injectable() @@ -47,17 +48,18 @@ export class ReportService { async getOverview() { // 获取系统总账户 const systemAccount = await this.systemAccountRepo.findOne({ - where: { account_name: '系统总账户' }, + where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN }, }); // 获取平台账户 const platformAccount = await this.platformAccountRepo.findOne({ - where: { account_name: '主账户' }, + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN }, }); const merchantStats = await this.merchantAccountRepo .createQueryBuilder('a') .select('SUM(a.balance)', 'totalBalance') + .addSelect('SUM(a.pending_settlement)', 'totalPendingSettlement') .addSelect('COUNT(*)', 'count') .getRawOne(); @@ -88,24 +90,34 @@ export class ReportService { .andWhere('t.direction = :direction', { direction: 'expense' }) .getRawOne(); + // 系统总账户金额(直接从数据库读取) + const systemTotalAmount = Number(systemAccount?.balance || 0); + + // 各层级账户余额 + const totalMerchantBalance = Number(merchantStats?.totalBalance || 0); + const totalMerchantPendingSettlement = Number(merchantStats?.totalPendingSettlement || 0); + const totalUserBalance = Number(userStats?.totalBalance || 0); + const platformBalance = Number(platformAccount?.balance || 0); + return { - // 系统总账户金额(用户实付 - 退款 - 提现) - systemTotalAmount: Number(systemAccount?.balance || 0), + // 系统总账户金额(从 system_accounts 表读取) + systemTotalAmount, totalUserPaid: Number(systemAccount?.total_income || 0), totalRefund: Number(systemAccount?.total_refund || 0), totalWithdrawn: Number(systemAccount?.total_withdrawn || 0), // 平台净收益(服务费 - 邀请返现) - platformBalance: Number(platformAccount?.balance || 0), + platformBalance, platformTotalIncome: Number(platformAccount?.total_income || 0), platformTotalExpense: Number(platformAccount?.total_expense || 0), // 商家账户统计 - totalMerchantBalance: Number(merchantStats?.totalBalance || 0), + totalMerchantBalance, + totalMerchantPendingSettlement, merchantCount: Number(merchantStats?.count || 0), // 用户账户统计 - totalUserBalance: Number(userStats?.totalBalance || 0), + totalUserBalance, userCount: Number(userStats?.count || 0), // 今日统计 @@ -314,4 +326,123 @@ export class ReportService { netAmount: Number(income?.sum || 0) - Number(expense?.sum || 0), }; } + + /** + * 服务费统计 + */ + async getServiceFeeStatistics() { + const today = dayjs().format('YYYY-MM-DD'); + const monthStart = dayjs().startOf('month').format('YYYY-MM-DD'); + const monthEnd = dayjs().endOf('month').format('YYYY-MM-DD'); + + // 今日服务费 + const todayFee = await this.orderRepo + .createQueryBuilder('o') + .select('SUM(o.service_fee)', 'sum') + .where('DATE(o.created_at) = :date', { date: today }) + .andWhere('o.status != :status', { status: 'cancelled' }) + .getRawOne(); + + // 本月服务费 + const monthFee = await this.orderRepo + .createQueryBuilder('o') + .select('SUM(o.service_fee)', 'sum') + .where('DATE(o.created_at) BETWEEN :start AND :end', { + start: monthStart, + end: monthEnd, + }) + .andWhere('o.status != :status', { status: 'cancelled' }) + .getRawOne(); + + // 累计服务费 + const totalFee = await this.orderRepo + .createQueryBuilder('o') + .select('SUM(o.service_fee)', 'sum') + .where('o.status != :status', { status: 'cancelled' }) + .getRawOne(); + + // 待结算服务费(已完成但未结算的订单) + const pendingFee = await this.orderRepo + .createQueryBuilder('o') + .leftJoin('settlement_items', 'si', 'si.order_id = o.id') + .select('SUM(o.service_fee)', 'sum') + .where('o.status = :status', { status: 'completed' }) + .andWhere('si.id IS NULL') + .getRawOne(); + + return { + todayCommission: Number(todayFee?.sum || 0), + monthCommission: Number(monthFee?.sum || 0), + totalCommission: Number(totalFee?.sum || 0), + pendingCommission: Number(pendingFee?.sum || 0), + }; + } + + /** + * 服务费明细列表 + */ + async getServiceFeeList(params: { + page: number; + pageSize: number; + startDate?: string; + endDate?: string; + }) { + const { page, pageSize, startDate, endDate } = params; + + const query = this.orderRepo + .createQueryBuilder('o') + .leftJoinAndSelect('o.merchant', 'm') + .leftJoinAndSelect('o.user', 'u') + .leftJoin('settlement_items', 'si', 'si.order_id = o.id') + .leftJoin('settlements', 's', 's.id = si.settlement_id') + .select([ + 'o.id', + 'o.orderNo', + 'o.totalAmount', + 'o.serviceFee', + 'o.status', + 'o.createdAt', + 'm.id', + 'm.name', + 'u.id', + 'u.nickname', + 's.settledAt', + ]) + .where('o.status != :status', { status: 'cancelled' }) + .andWhere('o.service_fee > 0'); + + if (startDate && endDate) { + query.andWhere('DATE(o.created_at) BETWEEN :startDate AND :endDate', { + startDate, + endDate, + }); + } + + const [list, total] = await query + .orderBy('o.created_at', 'DESC') + .skip((page - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + + const result = list.map((order) => ({ + id: order.id, + orderId: order.id, + orderNo: order.orderNo, + merchantName: order.merchant?.shopName || '-', + userName: order.user?.nickname || '-', + orderAmount: Number(order.totalAmount), + commissionRate: 0.05, // 固定5%,如果需要动态获取可以从配置读取 + commissionAmount: Number(order.serviceFee), + status: order['s_settledAt'] ? 'settled' : 'pending', + settledAt: order['s_settledAt'] || null, + createdAt: order.createdAt, + })); + + return { + list: result, + total, + page, + pageSize, + }; + } } diff --git a/apps/server/src/modules/shared/finance/settlement.service.ts b/apps/server/src/modules/shared/finance/settlement.service.ts index 3ec6e38..27425ae 100644 --- a/apps/server/src/modules/shared/finance/settlement.service.ts +++ b/apps/server/src/modules/shared/finance/settlement.service.ts @@ -51,33 +51,66 @@ export class SettlementService { throw new BadRequestException(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,无法重复结算`); } - const orders = await this.orderRepo + // 查询所有已完成且截止到上周末的订单 + this.logger.log(`查询条件: status=completed, checkout_at <= ${lastWeekEnd} 23:59:59`); + + const allOrders = await this.orderRepo .createQueryBuilder('o') .where('o.status = :status', { status: 'completed' }) .andWhere('o.merchant_id IS NOT NULL') - .andWhere('o.checkout_at BETWEEN :start AND :end', { - start: `${lastWeekStart} 00:00:00`, + .andWhere('o.checkout_at IS NOT NULL') + .andWhere('o.checkout_at <= :end', { end: `${lastWeekEnd} 23:59:59` }) .getMany(); - if (orders.length === 0) { + this.logger.log(`查询到 ${allOrders.length} 个已完成订单`); + + if (allOrders.length === 0) { this.logger.log('没有需要结算的订单'); throw new BadRequestException('该周期内没有需要结算的订单'); } - const ordersByMerchant = orders.reduce((acc, order) => { + // 批量查询已结算的订单ID + const orderIds = allOrders.map(o => o.id); + const settledItems = await this.settlementItemRepo + .createQueryBuilder('si') + .select('si.order_id') + .where('si.order_id IN (:...orderIds)', { orderIds }) + .getRawMany(); + + const settledOrderIds = new Set(settledItems.map(item => item.order_id)); + + // 过滤出未结算的订单,并按商家分组 + const ordersByMerchant: Record = {}; + let skippedCount = 0; + + for (const order of allOrders) { const merchantId = order.merchantId; + if (!merchantId || isNaN(merchantId)) { this.logger.warn(`订单 ${order.id} 的 merchantId 无效: ${merchantId}`); - return acc; + continue; } - if (!acc[merchantId]) { - acc[merchantId] = []; + + // 检查该订单是否已经被结算过 + if (settledOrderIds.has(order.id)) { + skippedCount++; + continue; } - acc[merchantId].push(order); - return acc; - }, {} as Record); + + if (!ordersByMerchant[merchantId]) { + ordersByMerchant[merchantId] = []; + } + ordersByMerchant[merchantId].push(order); + } + + this.logger.log(`共查询到 ${allOrders.length} 个订单,已结算 ${skippedCount} 个,待结算 ${allOrders.length - skippedCount} 个`); + + if (Object.keys(ordersByMerchant).length === 0) { + this.logger.log('没有需要结算的订单(所有订单都已结算)'); + throw new BadRequestException('没有需要结算的订单'); + } let successCount = 0; let failCount = 0; @@ -96,7 +129,7 @@ export class SettlementService { } this.logger.log(`周结算任务执行完成,成功:${successCount},失败:${failCount}`); - return { successCount, failCount, totalOrders: orders.length }; + return { successCount, failCount, totalOrders: allOrders.length - skippedCount }; } catch (error) { this.logger.error(`周结算任务执行失败:${error.message}`); throw error; @@ -333,6 +366,93 @@ export class SettlementService { return orderAmount - serviceFee; } + /** + * 获取商家待结算订单列表 + */ + async getPendingSettlementOrders(merchantId: number, page: number = 1, pageSize: number = 20) { + const lastSettlement = await this.settlementRepo.findOne({ + where: { merchantId }, + order: { settledAt: 'DESC' } + }); + + const startDate = lastSettlement + ? dayjs(lastSettlement.periodEnd).add(1, 'day').format('YYYY-MM-DD') + : '2020-01-01'; + + const queryBuilder = this.orderRepo.createQueryBuilder('order') + .leftJoinAndSelect('order.room', 'room') + .leftJoinAndSelect('order.user', 'user') + .where('order.merchantId = :merchantId', { merchantId }) + .andWhere('order.status = :status', { status: 'completed' }) + .andWhere('order.checkoutAt IS NOT NULL') + .andWhere('order.checkoutAt >= :startDate', { startDate: `${startDate} 00:00:00` }) + .andWhere('order.checkoutAt <= :endDate', { endDate: new Date() }) + .orderBy('order.checkoutAt', 'DESC'); + + const skip = (page - 1) * pageSize; + queryBuilder.skip(skip).take(pageSize); + + const [orders, total] = await queryBuilder.getManyAndCount(); + + return { + list: orders.map(order => ({ + id: order.id, + orderNo: order.orderNo, + roomName: order.room?.name || '', + checkInDate: order.checkInDate, + checkOutDate: order.checkOutDate, + nights: order.nights, + payAmount: Number(order.payAmount), + serviceFee: Number(order.serviceFee || 0), + merchantIncome: Number(order.merchantIncome || 0), + checkoutAt: order.checkoutAt, + contactName: order.contactName, + contactPhone: order.contactPhone, + })), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize) + }; + } + + /** + * 获取商家待结算订单统计 + */ + async getPendingSettlementSummary(merchantId: number) { + const lastSettlement = await this.settlementRepo.findOne({ + where: { merchantId }, + order: { settledAt: 'DESC' } + }); + + const startDate = lastSettlement + ? dayjs(lastSettlement.periodEnd).add(1, 'day').format('YYYY-MM-DD') + : '2020-01-01'; + + const orders = await this.orderRepo + .createQueryBuilder('o') + .where('o.merchant_id = :merchantId', { merchantId }) + .andWhere('o.status = :status', { status: 'completed' }) + .andWhere('o.checkout_at IS NOT NULL') + .andWhere('o.checkout_at >= :startDate', { startDate: `${startDate} 00:00:00` }) + .andWhere('o.checkout_at <= :endDate', { endDate: new Date() }) + .getMany(); + + const orderCount = orders.length; + const totalPayAmount = orders.reduce((sum, o) => sum + Number(o.payAmount), 0); + const totalServiceFee = orders.reduce((sum, o) => sum + Number(o.serviceFee || 0), 0); + const totalMerchantIncome = orders.reduce((sum, o) => sum + Number(o.merchantIncome || 0), 0); + + return { + orderCount, + totalPayAmount, + totalServiceFee, + totalMerchantIncome, + startDate, + lastSettlementDate: lastSettlement?.settledAt || null, + }; + } + /** * 预览周结算数据(不实际执行) */ diff --git a/database/migrations/001_init_schema.sql b/database/migrations/001_init_schema.sql index 2059d06..e9c719b 100644 --- a/database/migrations/001_init_schema.sql +++ b/database/migrations/001_init_schema.sql @@ -648,7 +648,7 @@ CREATE TABLE IF NOT EXISTS `merchant_accounts` ( -- 3. 系统总账户表(记录整个系统的资金流入流出) CREATE TABLE IF NOT EXISTS `system_accounts` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '账户ID', - `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:主账户)', + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:SYSTEM_MAIN)', `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额(total_income - total_refund - total_withdrawn)', `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(用户实付总额)', `total_refund` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计退款', @@ -665,7 +665,7 @@ CREATE TABLE IF NOT EXISTS `system_accounts` ( -- 4. 平台账户表(记录平台净收益) CREATE TABLE IF NOT EXISTS `platform_accounts` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '账户ID', - `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:主账户、备用账户)', + `account_name` VARCHAR(50) NOT NULL COMMENT '账户名称(如:PLATFORM_MAIN、PLATFORM_BACKUP)', `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额(平台净收益 = total_income - total_expense)', `frozen_balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额(提现中)', `total_income` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '累计收入(服务费收入)', diff --git a/database/seeds/001_init_data.sql b/database/seeds/001_init_data.sql index 8a1d688..eb163fc 100644 --- a/database/seeds/001_init_data.sql +++ b/database/seeds/001_init_data.sql @@ -4,12 +4,22 @@ USE `rent_platform`; -- ============================================================ --- 1. 平台账户(必须首先创建) +-- 1. 系统总账户(必须首先创建,用于资金守恒验证) +-- ============================================================ +-- 注意:初始余额为0,如果有历史订单数据,请执行相应脚本进行余额修复 +-- account_name: SYSTEM_MAIN (系统主账户) +INSERT INTO `system_accounts` (`account_name`, `balance`, `total_income`, `total_refund`, `total_withdrawn`, `status`) +VALUES ('SYSTEM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active') +ON DUPLICATE KEY UPDATE `account_name` = 'SYSTEM_MAIN'; + +-- ============================================================ +-- 2. 平台账户(记录平台净收益) -- ============================================================ -- 注意:初始余额为0,如果有历史订单数据,请执行 scripts/init-platform-account.sql 脚本进行余额修复 +-- account_name: PLATFORM_MAIN (平台主账户) INSERT INTO `platform_accounts` (`account_name`, `balance`, `frozen_balance`, `total_income`, `total_expense`, `status`) -VALUES ('主账户', 0.00, 0.00, 0.00, 0.00, 'active') -ON DUPLICATE KEY UPDATE `account_name` = '主账户'; +VALUES ('PLATFORM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active') +ON DUPLICATE KEY UPDATE `account_name` = 'PLATFORM_MAIN'; -- ============================================================ -- 2. 平台管理员账号 @@ -55,3 +65,12 @@ INSERT INTO `platform_configs` (`config_key`, `config_value`, `description`) VAL ('sms_enabled', 'true', '是否启用短信通知'), ('max_images_per_room', '20', '每个房源最大图片数'), ('max_images_per_review', '9', '每条评价最大图片数'); + +-- ============================================================ +-- 7. 营销活动 - 邀请返现活动 +-- ============================================================ +INSERT INTO `mkt_activities` (`name`, `type`, `enabled`, `config`, `start_time`, `end_time`) VALUES +('邀请好友返现活动', 'invite_cashback', 1, +'{"firstOrderRate": 0.05, "secondOrderRate": 0.005, "minCashback": 0.01, "maxCashback": 1000, "withdrawThreshold": 10, "maxOrderIndex": 2}', +NOW(), NULL) +ON DUPLICATE KEY UPDATE `enabled` = 1; diff --git a/docs/README.md b/docs/README.md index 993fd36..3f2fb58 100644 --- a/docs/README.md +++ b/docs/README.md @@ -159,11 +159,11 @@ source database/migrations/001_init_schema.sql; # 方式3:初始化系统总账户和平台账户 USE your_database_name; -INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn) -VALUES ('系统总账户', 0.00, 0.00, 0.00, 0.00); +INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn, status) +VALUES ('SYSTEM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active'); -INSERT INTO platform_accounts (account_name, balance, total_income, total_expense) -VALUES ('主账户', 0.00, 0.00, 0.00); +INSERT INTO platform_accounts (account_name, balance, frozen_balance, total_income, total_expense, status) +VALUES ('PLATFORM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active'); ``` --- diff --git a/docs/database/finance-database.md b/docs/database/finance-database.md index 46d533f..4884ac2 100644 --- a/docs/database/finance-database.md +++ b/docs/database/finance-database.md @@ -434,7 +434,7 @@ try { ```typescript const account = await queryRunner.manager.findOne(SystemAccount, { - where: { account_name: '系统总账户' }, + where: { account_name: 'SYSTEM_MAIN' }, lock: { mode: 'pessimistic_write' }, }); ``` @@ -497,12 +497,12 @@ mysql -u root -p < database/migrations/001_init_schema.sql # 2. 初始化系统总账户 mysql -u root -p USE your_database_name; -INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn) -VALUES ('系统总账户', 0.00, 0.00, 0.00, 0.00); +INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn, status) +VALUES ('SYSTEM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active'); # 3. 初始化平台账户 -INSERT INTO platform_accounts (account_name, balance, total_income, total_expense) -VALUES ('主账户', 0.00, 0.00, 0.00); +INSERT INTO platform_accounts (account_name, balance, frozen_balance, total_income, total_expense, status) +VALUES ('PLATFORM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active'); ``` ### 3. 自动创建的数据 diff --git a/docs/features/finance-system.md b/docs/features/finance-system.md index 69de904..dafd3b7 100644 --- a/docs/features/finance-system.md +++ b/docs/features/finance-system.md @@ -44,6 +44,43 @@ ## 账户体系 +### 账户名称常量说明 + +为了提高代码可维护性和国际化支持,系统账户名称采用英文常量存储,前端显示时映射为中文。 + +**账户名称映射表:** + +| 英文常量 (数据库存储) | 中文显示名称 | 说明 | +|---------------------|------------|------| +| `SYSTEM_MAIN` | 系统总账户 | 记录整个系统的资金流入流出,用于资金守恒验证 | +| `PLATFORM_MAIN` | 平台主账户 | 平台的主要收益账户,记录服务费收入和邀请返现支出 | +| `PLATFORM_BACKUP` | 平台备用账户 | 平台的备用账户(可选) | + +**后端使用:** +```typescript +// 导入常量 +import { ACCOUNT_NAMES } from '@/constants/account.constant'; + +// 使用常量查询 +const systemAccount = await this.systemAccountRepo.findOne({ + where: { account_name: ACCOUNT_NAMES.SYSTEM_MAIN } +}); + +const platformAccount = await this.platformAccountRepo.findOne({ + where: { account_name: ACCOUNT_NAMES.PLATFORM_MAIN } +}); +``` + +**前端使用:** +```typescript +// 导入映射工具 +import { getAccountDisplayName } from '@/utils/accountNameMap'; + +// 将英文常量转换为中文显示 +const displayName = getAccountDisplayName('SYSTEM_MAIN'); // 返回:系统总账户 +const displayName2 = getAccountDisplayName('PLATFORM_MAIN'); // 返回:平台主账户 +``` + ### 四层账户结构 系统采用四层账户结构,确保资金流转清晰、职责明确: @@ -955,14 +992,19 @@ mysql -u root -p < database/migrations/001_init_schema.sql 2. 初始化系统总账户: ```sql -INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn) -VALUES ('系统总账户', 0.00, 0.00, 0.00, 0.00); +INSERT INTO system_accounts (account_name, balance, total_income, total_refund, total_withdrawn, status) +VALUES ('SYSTEM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active'); ``` 3. 初始化平台账户: ```sql -INSERT INTO platform_accounts (account_name, balance, total_income, total_expense) -VALUES ('主账户', 0.00, 0.00, 0.00); +INSERT INTO platform_accounts (account_name, balance, frozen_balance, total_income, total_expense, status) +VALUES ('PLATFORM_MAIN', 0.00, 0.00, 0.00, 0.00, 'active'); +``` + +或者直接执行种子数据脚本: +```bash +mysql -u root -p rent_platform < database/seeds/001_init_data.sql ``` ### 服务启动