feat: 迭代
This commit is contained in:
@@ -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 = () => (
|
||||
<Route path="room-calendar/:roomId" element={<RoomCalendar />} />
|
||||
<Route path="reviews" element={<ReviewList />} />
|
||||
<Route path="finance">
|
||||
<Route index element={<Navigate to="/finance/settlements" replace />} />
|
||||
<Route index element={<Navigate to="/finance/pending-settlement" replace />} />
|
||||
<Route path="pending-settlement" element={<FinancePendingSettlement />} />
|
||||
<Route path="settlements" element={<FinanceSettlements />} />
|
||||
<Route path="settlements/:id" element={<FinanceSettlementDetail />} />
|
||||
<Route path="withdrawals" element={<FinanceWithdrawals />} />
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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: <WalletOutlined />,
|
||||
label: '财务管理',
|
||||
children: [
|
||||
{ key: '/finance/pending-settlement', icon: <ClockCircleOutlined />, label: '待结算订单' },
|
||||
{ key: '/finance/settlements', icon: <AuditOutlined />, label: '结算对账' },
|
||||
{ key: '/finance/withdrawals', icon: <PayCircleOutlined />, label: '收款记录' },
|
||||
{ key: '/finance/wallet', icon: <AccountBookOutlined />, label: '我的钱包' },
|
||||
|
||||
@@ -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<Summary | null>(null);
|
||||
const [summaryLoading, setSummaryLoading] = useState(false);
|
||||
|
||||
const {
|
||||
data: orders,
|
||||
loading,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
setPage,
|
||||
setPageSize,
|
||||
refresh,
|
||||
} = useTableData<PendingOrder>({
|
||||
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<PendingOrder> = [
|
||||
{
|
||||
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) => <span style={{ color: '#ff4d4f' }}>-{formatMoney(fee, '')}</span>,
|
||||
},
|
||||
{
|
||||
title: '预计收入',
|
||||
dataIndex: 'merchantIncome',
|
||||
key: 'merchantIncome',
|
||||
width: 120,
|
||||
render: (income: number) => (
|
||||
<span style={{ fontWeight: 'bold', color: '#52c41a' }}>{formatMoney(income)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '完成时间',
|
||||
dataIndex: 'checkoutAt',
|
||||
key: 'checkoutAt',
|
||||
width: 180,
|
||||
render: (date: string) => formatDateTime(date),
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
key: 'contact',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div>{record.contactName}</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>{record.contactPhone}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>待结算订单</h2>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card loading={summaryLoading}>
|
||||
<Statistic
|
||||
title="待结算订单数"
|
||||
value={summary?.orderCount || 0}
|
||||
prefix={<FileTextOutlined />}
|
||||
suffix="笔"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card loading={summaryLoading}>
|
||||
<Statistic
|
||||
title="订单总金额"
|
||||
value={summary?.totalPayAmount || 0}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined />}
|
||||
suffix="元"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card loading={summaryLoading}>
|
||||
<Statistic
|
||||
title="平台服务费"
|
||||
value={summary?.totalServiceFee || 0}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card loading={summaryLoading}>
|
||||
<Statistic
|
||||
title="预计结算金额"
|
||||
value={summary?.totalMerchantIncome || 0}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
extra={
|
||||
<Space>
|
||||
{summary?.lastSettlementDate && (
|
||||
<Tag icon={<CalendarOutlined />} color="blue">
|
||||
上次结算: {formatDate(summary.lastSettlementDate)}
|
||||
</Tag>
|
||||
)}
|
||||
<Tag icon={<ClockCircleOutlined />} color="orange">
|
||||
统计起始: {summary?.startDate ? formatDate(summary.startDate) : '-'}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1400 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: setPage,
|
||||
onShowSizeChange: (_, size) => setPageSize(size),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PendingSettlement;
|
||||
@@ -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',
|
||||
|
||||
@@ -197,12 +197,6 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/wallet/transactions",
|
||||
"style": {
|
||||
"navigationBarTitleText": "交易流水"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/wallet/withdraw",
|
||||
"style": {
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
<text class="date-tip">入住</text>
|
||||
</view>
|
||||
<view class="date-sep">
|
||||
<view class="sep-line" />
|
||||
<text class="sep-nights">{{ nightCount }}晚</text>
|
||||
<view class="sep-line" />
|
||||
</view>
|
||||
<view class="date-col">
|
||||
<text class="date-day">{{ checkOutLabel }}</text>
|
||||
@@ -633,11 +631,6 @@ onActivated(() => {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.sep-line {
|
||||
width: 40rpx;
|
||||
height: 2rpx;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.sep-nights {
|
||||
font-size: 20rpx;
|
||||
|
||||
@@ -18,37 +18,29 @@
|
||||
|
||||
<!-- 余额卡片 -->
|
||||
<view class="balance-card">
|
||||
<view class="balance-header">
|
||||
<view class="balance-icon-wrapper">
|
||||
<u-icon name="rmb-circle-fill" :size="24" color="#FF6B35" />
|
||||
<view class="balance-left">
|
||||
<view class="balance-header">
|
||||
<u-icon name="rmb-circle-fill" :size="18" color="#FF6B35" />
|
||||
<text class="balance-title">我的收益</text>
|
||||
</view>
|
||||
<view class="balance-amount">
|
||||
<text class="balance-value">¥{{ wallet.availableBalance || '0.00' }}</text>
|
||||
<text v-if="!canWithdraw && config?.config" class="balance-tip">
|
||||
满{{ config.config.withdrawThreshold }}元可提现
|
||||
</text>
|
||||
</view>
|
||||
<text class="balance-title">我的收益</text>
|
||||
</view>
|
||||
|
||||
<view class="balance-main">
|
||||
<view class="balance-amount-section">
|
||||
<text class="balance-label">可提现余额(元)</text>
|
||||
<view class="balance-value-row">
|
||||
<text class="balance-symbol">¥</text>
|
||||
<text class="balance-value">{{ wallet.availableBalance || '0.00' }}</text>
|
||||
</view>
|
||||
<view v-if="!canWithdraw" class="balance-tip">
|
||||
<u-icon name="info-circle" :size="12" color="#FF8C00" />
|
||||
<text class="tip-text">满10元可提现</text>
|
||||
</view>
|
||||
<view class="balance-actions">
|
||||
<view
|
||||
class="action-btn action-btn-primary"
|
||||
:class="{ disabled: !canWithdraw }"
|
||||
@tap="goWithdraw"
|
||||
>
|
||||
<text class="action-btn-text">提现</text>
|
||||
</view>
|
||||
|
||||
<view class="balance-actions">
|
||||
<view
|
||||
class="action-btn action-btn-primary"
|
||||
:class="{ disabled: !canWithdraw }"
|
||||
@tap="goWithdraw"
|
||||
>
|
||||
<text class="action-btn-text">立即提现</text>
|
||||
</view>
|
||||
<view class="action-btn action-btn-secondary" @tap="goWithdrawals">
|
||||
<text class="action-btn-text">提现记录</text>
|
||||
</view>
|
||||
<view class="action-btn action-btn-secondary" @tap="goWithdrawals">
|
||||
<text class="action-btn-text">记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -119,38 +111,18 @@
|
||||
<text class="rules-title">活动规则</text>
|
||||
</view>
|
||||
|
||||
<view class="rules-content">
|
||||
<view class="rule-item">
|
||||
<view class="rule-number">1</view>
|
||||
<view v-if="config?.config" class="rules-content">
|
||||
<view v-for="(rule, index) in rules" :key="index" class="rule-item">
|
||||
<view class="rule-number">{{ index + 1 }}</view>
|
||||
<view class="rule-content">
|
||||
<text class="rule-title-text">首单高返</text>
|
||||
<text class="rule-desc">好友首次下单,您可获得订单金额 <text class="highlight">5%</text> 返现</text>
|
||||
<text class="rule-title-text">{{ rule.title }}</text>
|
||||
<text class="rule-desc" v-html="rule.desc"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rule-item">
|
||||
<view class="rule-number">2</view>
|
||||
<view class="rule-content">
|
||||
<text class="rule-title-text">持续收益</text>
|
||||
<text class="rule-desc">好友再次下单,您仍可获得 <text class="highlight">0.5%</text> 返现奖励</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rule-item">
|
||||
<view class="rule-number">3</view>
|
||||
<view class="rule-content">
|
||||
<text class="rule-title-text">返现限额</text>
|
||||
<text class="rule-desc">单笔返现最低 <text class="highlight">0.01元</text>,最高 <text class="highlight">50元</text></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rule-item">
|
||||
<view class="rule-number">4</view>
|
||||
<view class="rule-content">
|
||||
<text class="rule-title-text">提现门槛</text>
|
||||
<text class="rule-desc">余额满 <text class="highlight">10元</text> 即可提现,实时到账</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="rules-loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -181,7 +153,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { getInviteStats } from '@/api/user/invite';
|
||||
import { getInviteStats, getInviteConfig } from '@/api/user/invite';
|
||||
import { walletApi } from '@/api/user/wallet';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
@@ -195,15 +167,55 @@ const wallet = ref<any>({
|
||||
availableBalance: 0,
|
||||
});
|
||||
|
||||
const config = ref<any>(null);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const canWithdraw = computed(() => {
|
||||
return Number(wallet.value.availableBalance) >= 10;
|
||||
const threshold = config.value?.config?.withdrawThreshold || 10;
|
||||
return Number(wallet.value.availableBalance) >= threshold;
|
||||
});
|
||||
|
||||
// 格式化百分比,去掉多余的0
|
||||
const formatPercent = (rate: number) => {
|
||||
const percent = rate * 100;
|
||||
// 使用parseFloat自动去掉末尾的0
|
||||
return parseFloat(percent.toFixed(2));
|
||||
};
|
||||
|
||||
// 动态生成活动规则
|
||||
const rules = computed(() => {
|
||||
if (!config.value?.config) return [];
|
||||
|
||||
const cfg = config.value.config;
|
||||
const maxOrderIndex = cfg.maxOrderIndex || 2;
|
||||
|
||||
return [
|
||||
{
|
||||
title: '首单高返',
|
||||
desc: `好友首次下单,您可获得订单金额 ${formatPercent(cfg.firstOrderRate)}% 返现`,
|
||||
},
|
||||
{
|
||||
title: maxOrderIndex > 1 ? '持续收益' : '返现次数',
|
||||
desc: maxOrderIndex > 1
|
||||
? `好友再次下单,您仍可获得 ${formatPercent(cfg.secondOrderRate)}% 返现奖励(最多前${maxOrderIndex}单)`
|
||||
: '每位好友仅首单可获得返现',
|
||||
},
|
||||
{
|
||||
title: '返现限额',
|
||||
desc: `单笔返现最低 ${cfg.minCashback}元,最高 ${cfg.maxCashback}元`,
|
||||
},
|
||||
{
|
||||
title: '提现门槛',
|
||||
desc: `余额满 ${cfg.withdrawThreshold}元 即可提现,实时到账`,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchStats();
|
||||
fetchWallet();
|
||||
fetchConfig();
|
||||
});
|
||||
|
||||
async function fetchStats() {
|
||||
@@ -224,13 +236,23 @@ async function fetchWallet() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
const res = await getInviteConfig();
|
||||
config.value = res.data || null;
|
||||
} catch (e) {
|
||||
console.error('获取活动配置失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleShare() {
|
||||
uni.showToast({ title: '请点击右上角分享', icon: 'none' });
|
||||
}
|
||||
|
||||
function goWithdraw() {
|
||||
if (!canWithdraw.value) {
|
||||
uni.showToast({ title: '余额不足10元,暂不可提现', icon: 'none' });
|
||||
const threshold = config.value?.config?.withdrawThreshold || 10;
|
||||
uni.showToast({ title: `余额不足${threshold}元,暂不可提现`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: '/pages/wallet/withdraw' });
|
||||
@@ -348,130 +370,87 @@ function goPoster() {
|
||||
z-index: 1;
|
||||
margin: 0 24rpx 24rpx;
|
||||
background: linear-gradient(135deg, #FF8C5A 0%, #FF6B35 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(255, 107, 53, 0.25);
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(255, 107, 53, 0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.balance-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50rpx;
|
||||
right: -50rpx;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
.balance-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.balance-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.balance-icon-wrapper {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.balance-title {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 600;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.balance-main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.balance-amount-section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.balance-value-row {
|
||||
.balance-amount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.balance-symbol {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 64rpx;
|
||||
font-size: 40rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.balance-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
background: rgba(255, 140, 0, 0.2);
|
||||
border-radius: 20rpx;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 22rpx;
|
||||
color: #FFF5E6;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
|
||||
.balance-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120rpx;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-primary {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
&.disabled {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
opacity: 0.6;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.action-btn-text {
|
||||
color: #FF6B35;
|
||||
font-weight: 600;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,6 +710,16 @@ function goPoster() {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rules-loading {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* ========== 底部记录入口 ========== */
|
||||
.bottom-records {
|
||||
position: relative;
|
||||
|
||||
@@ -94,17 +94,6 @@
|
||||
<text class="order-label">已下单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="item.orderCount > 0" class="record-footer">
|
||||
<view class="earnings-info">
|
||||
<u-icon name="rmb-circle-fill" :size="14" color="#FF6B35" />
|
||||
<text class="earnings-text">已获收益</text>
|
||||
<text class="earnings-amount">¥{{ item.totalCashback || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="arrow-icon">
|
||||
<u-icon name="arrow-right" :size="14" color="#CCCCCC" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
|
||||
@@ -71,7 +71,7 @@ const userInfo = computed(() => userStore.userInfo);
|
||||
|
||||
// 常用工具列表
|
||||
const toolList = [
|
||||
{ key: 'orders', label: '我的订单', icon: 'list', iconColor: '#1A1A1A', bgColor: '#FFFFFF' },
|
||||
{ key: 'orders', label: '我的订单', icon: 'file-text', iconColor: '#1A1A1A', bgColor: '#FFFFFF' },
|
||||
{ key: 'wallet', label: '我的钱包', icon: 'rmb-circle', iconColor: '#1A1A1A', bgColor: '#FFFFFF' },
|
||||
{ key: 'coupon', label: '优惠券', icon: 'coupon', iconColor: '#1A1A1A', bgColor: '#FFFFFF' },
|
||||
{ key: 'invite', label: '邀请返现', icon: 'gift', iconColor: '#1A1A1A', bgColor: '#FFFFFF' },
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<view class="form-item">
|
||||
<view class="form-label-row">
|
||||
<text class="form-label">身份证号</text>
|
||||
<text class="form-optional">选填</text>
|
||||
<text class="form-required">*</text>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
@@ -600,6 +600,16 @@ async function handleSubmit() {
|
||||
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!contactIdCard.value) {
|
||||
uni.showToast({ title: '请输入身份证号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// 验证身份证号格式(15位或18位)
|
||||
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
||||
if (!idCardReg.test(contactIdCard.value)) {
|
||||
uni.showToast({ title: '请输入正确的身份证号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createOrder({
|
||||
@@ -610,7 +620,7 @@ async function handleSubmit() {
|
||||
guestCount: guestCount.value,
|
||||
contactName: contactName.value,
|
||||
contactPhone: contactPhone.value,
|
||||
contactIdCard: contactIdCard.value || undefined,
|
||||
contactIdCard: contactIdCard.value,
|
||||
remark: remark.value,
|
||||
couponId: selectedCoupon.value?.coupon?.id || undefined,
|
||||
});
|
||||
|
||||
@@ -57,12 +57,6 @@
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-item" @tap="goTransactions">
|
||||
<view class="action-icon">
|
||||
<u-icon name="order" :size="24" color="#FF6B35" />
|
||||
</view>
|
||||
<text class="action-text">交易流水</text>
|
||||
</view>
|
||||
<view class="action-item" @tap="goWithdrawals">
|
||||
<view class="action-icon">
|
||||
<u-icon name="list" :size="24" color="#FF6B35" />
|
||||
@@ -146,10 +140,6 @@ function goWithdraw() {
|
||||
uni.navigateTo({ url: '/pages/wallet/withdraw' });
|
||||
}
|
||||
|
||||
function goTransactions() {
|
||||
uni.navigateTo({ url: '/pages/wallet/transactions' });
|
||||
}
|
||||
|
||||
function goWithdrawals() {
|
||||
uni.navigateTo({ url: '/pages/wallet/withdrawals' });
|
||||
}
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
<template>
|
||||
<view class="page-transactions">
|
||||
<!-- 导航栏 -->
|
||||
<view class="navbar">
|
||||
<view class="navbar-left" @tap="goBack">
|
||||
<u-icon name="arrow-left" :size="20" color="#1A1A1A" />
|
||||
</view>
|
||||
<text class="navbar-title">交易流水</text>
|
||||
<view class="navbar-right"></view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<scroll-view scroll-x class="filter-scroll">
|
||||
<view class="filter-items">
|
||||
<view
|
||||
v-for="type in transactionTypes"
|
||||
:key="type.value"
|
||||
:class="['filter-item', { active: filterType === type.value }]"
|
||||
@tap="filterType = type.value; fetchTransactions()"
|
||||
>
|
||||
<text class="filter-text">{{ type.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 交易列表 -->
|
||||
<scroll-view
|
||||
v-if="!loading"
|
||||
scroll-y
|
||||
class="transactions-list"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view v-if="transactions.length > 0" class="list-content">
|
||||
<view
|
||||
v-for="item in transactions"
|
||||
:key="item.id"
|
||||
class="transaction-item"
|
||||
>
|
||||
<view class="item-left">
|
||||
<view class="item-icon" :class="getDirectionClass(item.direction)">
|
||||
<u-icon :name="getDirectionIcon(item.direction)" :size="20" color="#ffffff" />
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="item-title">{{ item.description || getTypeLabel(item.transactionType) }}</text>
|
||||
<text class="item-time">{{ formatTime(item.createdAt) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text :class="['item-amount', getAmountClass(item.direction)]">
|
||||
{{ getAmountPrefix(item.direction) }}{{ formatMoney(item.amount) }}
|
||||
</text>
|
||||
<text class="item-balance">余额: ¥{{ formatMoney(item.balanceAfter) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="hasMore" class="load-more">
|
||||
<text class="load-more-text">加载更多...</text>
|
||||
</view>
|
||||
<view v-else-if="transactions.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="empty-state">
|
||||
<view class="empty-icon">
|
||||
<u-icon name="file-text" :size="80" color="#D8D8D8" />
|
||||
</view>
|
||||
<text class="empty-text">暂无交易记录</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-else class="loading-state">
|
||||
<view class="loading-spinner">
|
||||
<view class="spinner-dot"></view>
|
||||
<view class="spinner-dot"></view>
|
||||
<view class="spinner-dot"></view>
|
||||
</view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { walletApi } from '@/api/user/wallet';
|
||||
|
||||
const loading = ref(true);
|
||||
const transactions = ref<any[]>([]);
|
||||
const filterType = ref('');
|
||||
const page = ref(1);
|
||||
const pageSize = 20;
|
||||
const hasMore = ref(true);
|
||||
|
||||
const transactionTypes = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '收入', value: 'in' },
|
||||
{ label: '支出', value: 'out' },
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
fetchTransactions();
|
||||
});
|
||||
|
||||
async function fetchTransactions(reset = true) {
|
||||
if (reset) {
|
||||
page.value = 1;
|
||||
transactions.value = [];
|
||||
hasMore.value = true;
|
||||
}
|
||||
|
||||
loading.value = reset;
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: page.value,
|
||||
pageSize,
|
||||
};
|
||||
if (filterType.value) {
|
||||
params.direction = filterType.value;
|
||||
}
|
||||
|
||||
const res = await walletApi.getTransactions(params);
|
||||
const newData = res.data.items || [];
|
||||
|
||||
if (reset) {
|
||||
transactions.value = newData;
|
||||
} else {
|
||||
transactions.value = [...transactions.value, ...newData];
|
||||
}
|
||||
|
||||
hasMore.value = newData.length >= pageSize;
|
||||
} catch (e: any) {
|
||||
console.error('获取交易流水失败:', e);
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (!hasMore.value || loading.value) return;
|
||||
page.value++;
|
||||
fetchTransactions(false);
|
||||
}
|
||||
|
||||
function getTypeLabel(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
invite_cashback: '邀请返现',
|
||||
withdrawal: '提现',
|
||||
refund: '退款',
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
function getDirectionIcon(direction: string): string {
|
||||
return direction === 'in' ? 'arrow-down-circle' : 'arrow-up-circle';
|
||||
}
|
||||
|
||||
function getDirectionClass(direction: string): string {
|
||||
return direction === 'in' ? 'type-income' : 'type-expense';
|
||||
}
|
||||
|
||||
function getAmountClass(direction: string): string {
|
||||
return direction === 'in' ? 'amount-income' : 'amount-expense';
|
||||
}
|
||||
|
||||
function getAmountPrefix(direction: string): string {
|
||||
return direction === 'in' ? '+' : '-';
|
||||
}
|
||||
|
||||
function formatMoney(value: number | string): string {
|
||||
const num = Number(value || 0);
|
||||
return num.toFixed(2);
|
||||
}
|
||||
|
||||
function formatTime(time: string): string {
|
||||
if (!time) return '';
|
||||
const date = new Date(time);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const day = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (diff < day && date.getDate() === now.getDate()) {
|
||||
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
} else if (diff < 2 * day && date.getDate() === now.getDate() - 1) {
|
||||
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-transactions {
|
||||
min-height: 100vh;
|
||||
background: #F5F7FA;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.navbar {
|
||||
background: #ffffff;
|
||||
padding: 0 24rpx;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.navbar-left,
|
||||
.navbar-right {
|
||||
width: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx;
|
||||
margin: -12rpx;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background: #ffffff;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-items {
|
||||
display: inline-flex;
|
||||
padding: 16rpx 24rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: inline-block;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #F5F7FA;
|
||||
border-radius: 32rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #FF8C5A 0%, #FF6B35 100%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
|
||||
.active & {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 交易列表 */
|
||||
.transactions-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
margin: 0 24rpx 16rpx;
|
||||
padding: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.type-income {
|
||||
background: linear-gradient(135deg, #52C41A 0%, #389E0D 100%);
|
||||
}
|
||||
|
||||
&.type-expense {
|
||||
background: linear-gradient(135deg, #FF6B35 0%, #FF4D4F 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.item-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
|
||||
&.amount-income {
|
||||
color: #52C41A;
|
||||
}
|
||||
|
||||
&.amount-expense {
|
||||
color: #FF4D4F;
|
||||
}
|
||||
}
|
||||
|
||||
.item-balance {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more,
|
||||
.no-more {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.load-more-text,
|
||||
.no-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.spinner-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #FF6B35;
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s infinite ease-in-out both;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -44,34 +44,21 @@
|
||||
<view
|
||||
v-for="type in accountTypes"
|
||||
:key="type.value"
|
||||
:class="['type-item', { active: form.accountType === type.value }]"
|
||||
@tap="form.accountType = type.value"
|
||||
:class="['type-item', { active: form.paymentChannel === type.value }]"
|
||||
@tap="form.paymentChannel = type.value"
|
||||
>
|
||||
<u-icon :name="type.icon" :size="24" :color="form.accountType === type.value ? '#FF6B35' : '#999'" />
|
||||
<u-icon :name="type.icon" :size="28" :color="form.paymentChannel === type.value ? '#FF6B35' : '#999'" />
|
||||
<text class="type-text">{{ type.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账户信息 -->
|
||||
<!-- 提现说明 -->
|
||||
<view class="form-item">
|
||||
<text class="item-label">真实姓名</text>
|
||||
<input
|
||||
v-model="form.accountName"
|
||||
class="text-input"
|
||||
placeholder="请输入真实姓名"
|
||||
:maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="item-label">{{ form.accountType === 'alipay' ? '支付宝账号' : '微信账号' }}</text>
|
||||
<input
|
||||
v-model="form.accountNumber"
|
||||
class="text-input"
|
||||
:placeholder="form.accountType === 'alipay' ? '请输入支付宝账号' : '请输入微信账号'"
|
||||
:maxlength="30"
|
||||
/>
|
||||
<view class="wechat-tip">
|
||||
<u-icon name="info-circle" :size="16" color="#FF6B35" />
|
||||
<text class="wechat-tip-text">提现将直接到账当前登录的微信账号</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ const InviteManage: React.FC = () => {
|
||||
<InputNumber min={0.01} max={1} step={0.01} prefix="¥" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="最高返现金额" name="maxCashback" rules={[{ required: true }]} extra="单笔返现最高金额">
|
||||
<InputNumber min={10} max={500} step={10} prefix="¥" style={{ width: '100%' }} />
|
||||
<InputNumber min={10} max={5000} step={10} prefix="¥" style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="提现门槛" name="withdrawThreshold" rules={[{ required: true }]} extra="用户余额满多少元可以申请提现">
|
||||
<InputNumber min={1} max={100} step={1} prefix="¥" style={{ width: '100%' }} />
|
||||
|
||||
@@ -240,7 +240,7 @@ const BankCards: React.FC = () => {
|
||||
label="卡片名称"
|
||||
rules={[{ required: true, message: '请输入卡片名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:主账户、备用账户" />
|
||||
<Input placeholder="例如:PLATFORM_MAIN (平台主账户)、PLATFORM_BACKUP (平台备用账户)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bankName"
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, Tag } from 'antd';
|
||||
import { WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, DollarOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import { getFinancialOverview, getPlatformTransactions } from '@/api/finance';
|
||||
import { getAccountDisplayName } from '@/utils/accountNameMap';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
interface FinancialOverview {
|
||||
@@ -14,11 +15,11 @@ interface FinancialOverview {
|
||||
platformTotalExpense: number;
|
||||
totalUserBalance: number;
|
||||
totalMerchantBalance: number;
|
||||
totalMerchantPendingSettlement: number;
|
||||
userCount: number;
|
||||
merchantCount: number;
|
||||
todayIncome: number;
|
||||
todayExpense: number;
|
||||
todayOrders: number;
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
@@ -116,18 +117,19 @@ const FinanceDashboard: React.FC = () => {
|
||||
{/* 系统总账户金额 - 突出显示 */}
|
||||
<Card style={{ marginBottom: 24, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
||||
<Statistic
|
||||
title={<span style={{ color: '#fff', fontSize: 16 }}>系统总账户金额</span>}
|
||||
title={<span style={{ color: '#fff', fontSize: 16 }}>系统总账户金额(资金守恒)</span>}
|
||||
value={Number(overview.systemTotalAmount || 0)}
|
||||
prefix={<WalletOutlined style={{ color: '#fff' }} />}
|
||||
suffix={<span style={{ color: '#fff' }}>元</span>}
|
||||
suffix={<span style={{ color: '#fff' }}>元 ({getAccountDisplayName('SYSTEM_MAIN')})</span>}
|
||||
precision={2}
|
||||
valueStyle={{ color: '#fff', fontSize: 36, fontWeight: 'bold' }}
|
||||
/>
|
||||
<div style={{ color: 'rgba(255,255,255,0.8)', fontSize: 12, marginTop: 8 }}>
|
||||
用户实付(¥{Number(overview.totalUserPaid || 0).toFixed(2)})- 退款(¥{Number(overview.totalRefund || 0).toFixed(2)})- 提现(¥{Number(overview.totalWithdrawn || 0).toFixed(2)})
|
||||
{/* <div style={{ color: 'rgba(255,255,255,0.9)', fontSize: 13, marginTop: 8, fontWeight: 500 }}>
|
||||
商家账户余额(¥{Number(overview.totalMerchantBalance || 0).toFixed(2)})+ 用户账户余额(¥{Number(overview.totalUserBalance || 0).toFixed(2)})+ 平台净收益(¥{Number(overview.platformBalance || 0).toFixed(2)})
|
||||
</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 4 }}>
|
||||
= 商家账户余额(¥{Number(overview.totalMerchantBalance || 0).toFixed(2)})+ 用户账户余额(¥{Number(overview.totalUserBalance || 0).toFixed(2)})+ 平台净收益(¥{Number(overview.platformBalance || 0).toFixed(2)})
|
||||
*/}
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: 11, marginTop: 6 }}>
|
||||
系统总账户 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)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -180,11 +182,16 @@ const FinanceDashboard: React.FC = () => {
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日订单数"
|
||||
value={overview.todayOrders}
|
||||
suffix="单"
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
title="商家待结算金额"
|
||||
value={overview.totalMerchantPendingSettlement}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#ff9800' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 8 }}>
|
||||
待结算订单金额
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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;
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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)' })
|
||||
|
||||
@@ -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)' })
|
||||
|
||||
@@ -102,7 +102,7 @@ export class ActivityService {
|
||||
firstOrderRate: 0.05,
|
||||
secondOrderRate: 0.005,
|
||||
minCashback: 0.01,
|
||||
maxCashback: 50,
|
||||
maxCashback: 1000,
|
||||
withdrawThreshold: 10,
|
||||
maxOrderIndex: 2,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<MerchantAccount>,
|
||||
@InjectRepository(MerchantTransaction)
|
||||
private merchantTransactionRepo: Repository<MerchantTransaction>,
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<Order>,
|
||||
@@ -15,6 +20,9 @@ export class MerchantOrderService {
|
||||
private roomRepo: Repository<Room>,
|
||||
@InjectRepository(RoomCalendar)
|
||||
private calendarRepo: Repository<RoomCalendar>,
|
||||
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: '已确认离店,订单已完成' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PlatformAccount> {
|
||||
async getPlatformAccount(accountName: string = ACCOUNT_NAMES.PLATFORM_MAIN): Promise<PlatformAccount> {
|
||||
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<void> {
|
||||
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<SystemAccount> {
|
||||
async getSystemAccount(accountName: string = ACCOUNT_NAMES.SYSTEM_MAIN): Promise<SystemAccount> {
|
||||
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' },
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<number, Order[]> = {};
|
||||
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<number, Order[]>);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览周结算数据(不实际执行)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user