feat: 迭代

This commit is contained in:
2026-05-28 19:47:45 +08:00
parent 9baf5f29f7
commit e8bce5e924
50 changed files with 891 additions and 873 deletions
+1
View File
@@ -29,6 +29,7 @@
"@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
"@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
"china-division": "^2.7.0",
"pinia": "^2.2.0",
"uview-plus": "^3.8.18",
"vue": "^3.5.32",
+2 -2
View File
@@ -29,11 +29,11 @@ export function refreshToken(refreshToken: string) {
}
export function getUserProfile() {
return get('/api/app/profile/profile');
return get('/api/app/profile');
}
export function updateUserProfile(data: { nickname?: string; avatar?: string }) {
return post('/api/app/profile/profile', data);
return post('/api/app/profile', data);
}
export function uploadAvatar(filePath: string) {
+25 -152
View File
@@ -2,7 +2,7 @@
<view class="region-selector" @tap="openSelector">
<text v-if="displayText" class="region-text">{{ displayText }}</text>
<text v-else class="region-placeholder">{{ placeholder }}</text>
<text class="region-arrow"></text>
<u-icon name="arrow-down" :size="20" color="#999" />
</view>
<!-- 遮罩层 -->
@@ -12,7 +12,9 @@
<view :class="['popup-panel', { show: visible }]">
<view class="panel-header">
<text class="panel-title">选择地区</text>
<text class="panel-close" @tap="closeSelector"></text>
<view class="panel-close" @tap="closeSelector">
<u-icon name="close" :size="32" color="#999" />
</view>
</view>
<view class="panel-tabs">
<view
@@ -25,7 +27,7 @@
<view v-if="activeTab === index" class="tab-line"></view>
</view>
</view>
<scroll-view class="panel-content" scroll-y :scroll-top="0">
<scroll-view class="panel-content" scroll-y>
<view class="list-wrapper">
<view
v-for="item in currentList"
@@ -34,7 +36,7 @@
@tap="selectItem(item)"
>
<text class="item-name">{{ item.name }}</text>
<text v-if="isItemSelected(item)" class="item-check"></text>
<u-icon v-if="isItemSelected(item)" name="checkmark" :size="28" color="#667eea" />
</view>
</view>
</scroll-view>
@@ -43,6 +45,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { regionData } from '@/data/region';
interface RegionItem {
code: string;
@@ -76,9 +79,9 @@ const selectedCity = ref<RegionItem | null>(null);
const selectedDistrict = ref<RegionItem | null>(null);
const tabs = computed(() => [
{ label: selectedProvince.value?.name || '请选择' },
{ label: selectedCity.value?.name || '请选择' },
{ label: selectedDistrict.value?.name || '请选择' },
{ label: selectedProvince.value?.name || '' },
{ label: selectedCity.value?.name || '' },
{ label: selectedDistrict.value?.name || '' },
]);
const displayText = computed(() => {
@@ -89,137 +92,15 @@ const displayText = computed(() => {
return parts.join(' ');
});
const provinceList = ref<RegionItem[]>([
{ code: '11', name: '北京市' },
{ code: '12', name: '天津市' },
{ code: '13', name: '河北省' },
{ code: '14', name: '山西省' },
{ code: '15', name: '内蒙古自治区' },
{ code: '21', name: '辽宁省' },
{ code: '22', name: '吉林省' },
{ code: '23', name: '黑龙江省' },
{ code: '31', name: '上海市' },
{ code: '32', name: '江苏省' },
{ code: '33', name: '浙江省' },
{ code: '34', name: '安徽省' },
{ code: '35', name: '福建省' },
{ code: '36', name: '江西省' },
{ code: '37', name: '山东省' },
{ code: '41', name: '河南省' },
{ code: '42', name: '湖北省' },
{ code: '43', name: '湖南省' },
{ code: '44', name: '广东省' },
{ code: '45', name: '广西壮族自治区' },
{ code: '46', name: '海南省' },
{ code: '50', name: '重庆市' },
{ code: '51', name: '四川省' },
{ code: '52', name: '贵州省' },
{ code: '53', name: '云南省' },
{ code: '54', name: '西藏自治区' },
{ code: '61', name: '陕西省' },
{ code: '62', name: '甘肃省' },
{ code: '63', name: '青海省' },
{ code: '64', name: '宁夏回族自治区' },
{ code: '65', name: '新疆维吾尔自治区' },
{ code: '71', name: '台湾省' },
{ code: '81', name: '香港特别行政区' },
{ code: '82', name: '澳门特别行政区' },
]);
const cityData: Record<string, RegionItem[]> = {
'11': [{ code: '1101', name: '北京市' }],
'12': [{ code: '1201', name: '天津市' }],
'31': [{ code: '3101', name: '上海市' }],
'50': [{ code: '5001', name: '重庆市' }],
'32': [
{ code: '3201', name: '南京市' },
{ code: '3202', name: '无锡市' },
{ code: '3203', name: '徐州市' },
{ code: '3204', name: '常州市' },
{ code: '3205', name: '苏州市' },
{ code: '3206', name: '南通市' },
{ code: '3207', name: '连云港市' },
],
'33': [
{ code: '3301', name: '杭州市' },
{ code: '3302', name: '宁波市' },
{ code: '3303', name: '温州市' },
{ code: '3304', name: '嘉兴市' },
{ code: '3305', name: '湖州市' },
{ code: '3306', name: '绍兴市' },
{ code: '3307', name: '金华市' },
],
'44': [
{ code: '4401', name: '广州市' },
{ code: '4402', name: '韶关市' },
{ code: '4403', name: '深圳市' },
{ code: '4404', name: '珠海市' },
{ code: '4405', name: '汕头市' },
{ code: '4406', name: '佛山市' },
{ code: '4407', name: '江门市' },
],
};
const districtData: Record<string, RegionItem[]> = {
'1101': [
{ code: '110101', name: '东城区' },
{ code: '110102', name: '西城区' },
{ code: '110105', name: '朝阳区' },
{ code: '110106', name: '丰台区' },
{ code: '110107', name: '石景山区' },
{ code: '110108', name: '海淀区' },
],
'3101': [
{ code: '310101', name: '黄浦区' },
{ code: '310104', name: '徐汇区' },
{ code: '310105', name: '长宁区' },
{ code: '310106', name: '静安区' },
{ code: '310107', name: '普陀区' },
{ code: '310112', name: '闵行区' },
],
'3201': [
{ code: '320102', name: '玄武区' },
{ code: '320104', name: '秦淮区' },
{ code: '320105', name: '建邺区' },
{ code: '320106', name: '鼓楼区' },
{ code: '320111', name: '浦口区' },
],
'3301': [
{ code: '330102', name: '上城区' },
{ code: '330103', name: '下城区' },
{ code: '330104', name: '江干区' },
{ code: '330105', name: '拱墅区' },
{ code: '330106', name: '西湖区' },
],
'4401': [
{ code: '440103', name: '荔湾区' },
{ code: '440104', name: '越秀区' },
{ code: '440105', name: '海珠区' },
{ code: '440106', name: '天河区' },
{ code: '440111', name: '白云区' },
],
'4403': [
{ code: '440303', name: '罗湖区' },
{ code: '440304', name: '福田区' },
{ code: '440305', name: '南山区' },
{ code: '440306', name: '宝安区' },
{ code: '440307', name: '龙岗区' },
],
};
const currentList = computed(() => {
if (activeTab.value === 0) {
return provinceList.value;
return regionData.provinces;
} else if (activeTab.value === 1) {
if (!selectedProvince.value) return [];
return cityData[selectedProvince.value.code] || [
{ code: selectedProvince.value.code + '01', name: selectedProvince.value.name.replace(/省$|自治区$|特别行政区$|壮族|维吾尔|回族/g, '') + '市辖区' },
];
return regionData.cities[selectedProvince.value.code] || [];
} else {
if (!selectedCity.value) return [];
return districtData[selectedCity.value.code] || [
{ code: selectedCity.value.code + '01', name: '市辖区' },
];
return regionData.districts[selectedCity.value.code] || [];
}
});
@@ -277,16 +158,16 @@ watch(
() => [props.province, props.city, props.district],
() => {
if (props.province) {
const province = provinceList.value.find((p) => p.name === props.province);
const province = regionData.provinces.find((p) => p.name === props.province);
if (province) {
selectedProvince.value = province;
if (props.city) {
const cities = cityData[province.code] || [];
const cities = regionData.cities[province.code] || [];
const city = cities.find((c) => c.name === props.city);
if (city) {
selectedCity.value = city;
if (props.district) {
const districts = districtData[city.code] || [];
const districts = regionData.districts[city.code] || [];
const district = districts.find((d) => d.name === props.district);
if (district) {
selectedDistrict.value = district;
@@ -305,10 +186,12 @@ watch(
.region-selector {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
cursor: pointer;
}
.region-text {
@@ -323,12 +206,6 @@ watch(
color: #999;
}
.region-arrow {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
/* 遮罩 */
.mask {
position: fixed;
@@ -374,8 +251,9 @@ watch(
}
.panel-close {
font-size: 32rpx;
color: #999;
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx;
}
@@ -396,7 +274,7 @@ watch(
}
.tab-item.active .tab-text {
color: #FF6B35;
color: #667eea;
font-weight: 600;
}
@@ -407,7 +285,7 @@ watch(
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: #FF6B35;
background: #667eea;
border-radius: 2rpx;
}
@@ -427,7 +305,7 @@ watch(
border-bottom: 1rpx solid #f5f5f5;
&.selected {
background: #fff8f5;
background: rgba(102, 126, 234, 0.05);
}
}
@@ -437,12 +315,7 @@ watch(
}
.list-item.selected .item-name {
color: #FF6B35;
color: #667eea;
font-weight: 600;
}
.item-check {
font-size: 28rpx;
color: #FF6B35;
}
</style>
+47
View File
@@ -0,0 +1,47 @@
// 省市区数据
import provinces from 'china-division/dist/provinces.json';
import cities from 'china-division/dist/cities.json';
import areas from 'china-division/dist/areas.json';
interface RegionItem {
code: string;
name: string;
}
// 转换省份数据
const provinceList: RegionItem[] = provinces.map((p: any) => ({
code: p.code,
name: p.name,
}));
// 转换城市数据 - 按省份code分组
const cityData: Record<string, RegionItem[]> = {};
cities.forEach((c: any) => {
const provinceCode = c.provinceCode;
if (!cityData[provinceCode]) {
cityData[provinceCode] = [];
}
cityData[provinceCode].push({
code: c.code,
name: c.name,
});
});
// 转换区县数据 - 按城市code分组
const districtData: Record<string, RegionItem[]> = {};
areas.forEach((a: any) => {
const cityCode = a.cityCode;
if (!districtData[cityCode]) {
districtData[cityCode] = [];
}
districtData[cityCode].push({
code: a.code,
name: a.name,
});
});
export const regionData = {
provinces: provinceList,
cities: cityData,
districts: districtData,
};
+13 -15
View File
@@ -2,7 +2,9 @@
<view class="guest-page">
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="list.length === 0" class="empty">
<view class="empty-icon">👤</view>
<view class="empty-icon">
<u-icon name="account" :size="120" color="#ccc" />
</view>
<view class="empty-text">暂无常住人</view>
<view class="empty-tip">添加常住人下次预订更便捷</view>
</view>
@@ -33,7 +35,7 @@
</view>
<view class="add-btn" @tap="handleAdd">
<text class="add-icon">+</text>
<u-icon name="plus" :size="20" color="#fff" />
<text class="add-text">添加常住人</text>
</view>
@@ -42,7 +44,9 @@
<view class="form-popup">
<view class="form-header">
<text class="form-title">{{ formData.id ? '编辑常住人' : '添加常住人' }}</text>
<text class="form-close" @tap="closeForm"></text>
<view class="form-close" @tap="closeForm">
<u-icon name="close" :size="40" color="#999" />
</view>
</view>
<view class="form-body">
@@ -100,7 +104,7 @@
<view class="form-item">
<view class="form-checkbox" @tap="formData.isDefault = !formData.isDefault">
<view class="checkbox" :class="{ checked: formData.isDefault }">
<text v-if="formData.isDefault" class="checkbox-icon"></text>
<u-icon v-if="formData.isDefault" name="checkbox-mark" :size="24" color="#fff" />
</view>
<text class="checkbox-label">设为默认常住人</text>
</view>
@@ -272,8 +276,9 @@ const handleDelete = (item: any) => {
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 24rpx;
display: flex;
justify-content: center;
}
.empty-text {
@@ -369,9 +374,6 @@ const handleDelete = (item: any) => {
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
}
.add-icon {
font-size: 40rpx;
}
.form-popup {
background: #fff;
@@ -396,8 +398,9 @@ const handleDelete = (item: any) => {
}
.form-close {
font-size: 40rpx;
color: #999;
display: flex;
align-items: center;
justify-content: center;
}
.form-body {
@@ -473,11 +476,6 @@ const handleDelete = (item: any) => {
}
}
.checkbox-icon {
color: #fff;
font-size: 24rpx;
font-weight: bold;
}
.checkbox-label {
font-size: 28rpx;
+437 -115
View File
@@ -2,58 +2,118 @@
<view class="page-merchant">
<!-- 未登录商家账号 -->
<view v-if="!sellerStore.isSellerLoggedIn()" class="empty-state">
<view class="empty-icon">
<u-icon name="shop" :size="120" color="#667eea" />
<view class="empty-bg">
<view class="bg-circle circle-1"></view>
<view class="bg-circle circle-2"></view>
<view class="bg-circle circle-3"></view>
</view>
<view class="empty-content">
<text class="empty-title">开启您的事业</text>
<text class="empty-desc">注册成为商家轻松管理房源和订单</text>
<view class="feature-list">
<view class="feature-item">
<view class="feature-icon">
<u-icon name="checkmark" :size="20" color="#52c41a" />
</view>
<text class="feature-text">零门槛入驻快速开店</text>
</view>
<view class="feature-item">
<view class="feature-icon">
<u-icon name="checkmark" :size="20" color="#52c41a" />
</view>
<text class="feature-text">智能管理高效运营</text>
</view>
<view class="feature-item">
<view class="feature-icon">
<u-icon name="checkmark" :size="20" color="#52c41a" />
</view>
<text class="feature-text">平台流量订单无忧</text>
</view>
</view>
<button class="primary-btn" @tap="goSellerRegister">
<text class="btn-text">立即注册/登录</text>
<u-icon name="arrow-right" :size="20" color="#fff" />
</button>
</view>
<text class="empty-title">欢迎成为商家</text>
<text class="empty-desc">注册商家账号开启您的民宿事业</text>
<button class="primary-btn" @tap="goSellerRegister">
<text class="btn-text">立即注册/登录</text>
</button>
</view>
<!-- 已登录但未申请店铺 -->
<view v-else-if="!sellerStore.hasMerchant()" class="create-shop-state">
<view class="welcome-card">
<view class="welcome-icon">
<u-icon name="account-fill" :size="80" color="#fff" />
</view>
<text class="welcome-title">你好{{ sellerStore.sellerInfo?.contactName }}</text>
<text class="welcome-phone">{{ sellerStore.sellerInfo?.phone }}</text>
</view>
<view class="create-card">
<view class="create-icon">
<u-icon name="home" :size="80" color="#667eea" />
</view>
<text class="create-title">创建您的店铺</text>
<text class="create-desc">完成店铺信息填写提交审核后即可开始营业</text>
<view class="create-steps">
<view class="step-item">
<view class="step-number">1</view>
<text class="step-text">填写店铺信息</text>
</view>
<view class="step-divider"></view>
<view class="step-item">
<view class="step-number">2</view>
<text class="step-text">等待平台审核</text>
</view>
<view class="step-divider"></view>
<view class="step-item">
<view class="step-number">3</view>
<text class="step-text">开始营业</text>
<view class="create-content">
<view class="welcome-section">
<view class="avatar-wrapper">
<view class="avatar-circle">
<u-icon name="account-fill" :size="60" color="#667eea" />
</view>
<view class="avatar-badge">
<u-icon name="checkmark" :size="16" color="#fff" />
</view>
</view>
<text class="welcome-name">{{ sellerStore.sellerInfo?.contactName }}</text>
<text class="welcome-phone">{{ sellerStore.sellerInfo?.phone }}</text>
</view>
<button class="primary-btn large" @tap="goCreateShop">
<text class="btn-text">立即创建店铺</text>
<view class="create-main-card">
<view class="card-header">
<view class="header-icon-box">
<u-icon name="home-fill" :size="48" color="#667eea" />
</view>
<view class="header-text">
<text class="card-title">创建您的店铺</text>
<text class="card-subtitle">开启民宿经营之旅</text>
</view>
</view>
<view class="progress-steps">
<view class="progress-line"></view>
<view class="step-item">
<view class="step-circle">
<text class="step-num">1</text>
</view>
<text class="step-label">填写信息</text>
</view>
<view class="step-item">
<view class="step-circle">
<text class="step-num">2</text>
</view>
<text class="step-label">等待审核</text>
</view>
<view class="step-item">
<view class="step-circle">
<text class="step-num">3</text>
</view>
<text class="step-label">开始营业</text>
</view>
</view>
<view class="benefits-list">
<view class="benefit-item">
<u-icon name="checkmark-circle" :size="20" color="#52c41a" />
<text class="benefit-text">免费入驻无需押金</text>
</view>
<view class="benefit-item">
<u-icon name="checkmark-circle" :size="20" color="#52c41a" />
<text class="benefit-text">专业工具轻松管理</text>
</view>
<view class="benefit-item">
<u-icon name="checkmark-circle" :size="20" color="#52c41a" />
<text class="benefit-text">平台推广客源无忧</text>
</view>
</view>
<button class="create-shop-btn" @tap="goCreateShop">
<text class="btn-text">立即创建店铺</text>
<u-icon name="arrow-right" :size="20" color="#fff" />
</button>
</view>
<button class="logout-btn" @tap="handleLogoutSeller">
<text class="logout-text">退出商家账号</text>
</button>
</view>
<button class="text-btn" @tap="handleLogoutSeller">
<text class="text-btn-label">退出商家账号</text>
</button>
</view>
<!-- 已有店铺加载中 -->
@@ -339,174 +399,438 @@ function navigateTo(url: string) {
/* ========== 空状态 ========== */
.empty-state {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 0 48rpx;
overflow: hidden;
}
.empty-icon {
.empty-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.bg-circle {
position: absolute;
border-radius: 50%;
opacity: 0.1;
animation: float 6s ease-in-out infinite;
&.circle-1 {
width: 400rpx;
height: 400rpx;
background: linear-gradient(135deg, #667eea, #764ba2);
top: -100rpx;
right: -100rpx;
animation-delay: 0s;
}
&.circle-2 {
width: 300rpx;
height: 300rpx;
background: linear-gradient(135deg, #f093fb, #f5576c);
bottom: 100rpx;
left: -80rpx;
animation-delay: 2s;
}
&.circle-3 {
width: 200rpx;
height: 200rpx;
background: linear-gradient(135deg, #4facfe, #00f2fe);
top: 50%;
right: -50rpx;
animation-delay: 4s;
}
}
@keyframes float {
0%, 100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-30rpx) scale(1.05);
}
}
.empty-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: $spacing-2xl;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20rpx); }
width: 100%;
}
.empty-title {
font-size: $font-2xl;
font-size: 48rpx;
font-weight: $font-bold;
color: $text-primary;
margin-bottom: $spacing-md;
margin-bottom: 24rpx;
text-align: center;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.empty-desc {
font-size: $font-base;
font-size: 28rpx;
color: $text-secondary;
margin-bottom: $spacing-3xl;
margin-bottom: 64rpx;
text-align: center;
line-height: 1.6;
}
.feature-list {
width: 100%;
margin-bottom: 64rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.feature-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10rpx);
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
transform: translateX(8rpx);
}
}
.feature-icon {
width: 48rpx;
height: 48rpx;
background: rgba(82, 196, 26, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.feature-text {
font-size: 28rpx;
color: $text-primary;
font-weight: $font-medium;
}
/* ========== 创建店铺状态 ========== */
.create-shop-state {
padding: $spacing-2xl;
position: relative;
min-height: 100vh;
padding: 48rpx 32rpx;
}
.welcome-card {
background: linear-gradient(135deg, $primary-color, $primary-400);
border-radius: $radius-xl;
padding: $spacing-3xl $spacing-2xl;
.create-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.welcome-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: $spacing-2xl;
box-shadow: $shadow-lg;
padding: 48rpx 32rpx;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
border-radius: 24rpx;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, #667eea, #764ba2);
}
}
.welcome-icon {
.avatar-wrapper {
position: relative;
margin-bottom: 24rpx;
}
.avatar-circle {
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-lg;
border: 4rpx solid #fff;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.2);
}
.welcome-title {
font-size: $font-xl;
.avatar-badge {
position: absolute;
bottom: 0;
right: 0;
width: 36rpx;
height: 36rpx;
background: linear-gradient(135deg, #52c41a, #73d13d);
border-radius: 50%;
border: 3rpx solid #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
.welcome-name {
font-size: 36rpx;
font-weight: $font-bold;
color: #fff;
margin-bottom: $spacing-xs;
color: $text-primary;
margin-bottom: 8rpx;
}
.welcome-phone {
font-size: $font-base;
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
color: $text-secondary;
}
.create-card {
background: $bg-card;
border-radius: $radius-xl;
padding: $spacing-3xl $spacing-2xl;
margin-bottom: $spacing-xl;
box-shadow: $shadow-sm;
.create-main-card {
background: #fff;
border-radius: 24rpx;
padding: 40rpx 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.create-icon {
.card-header {
display: flex;
align-items: center;
margin-bottom: 40rpx;
padding-bottom: 32rpx;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
}
.header-icon-box {
width: 96rpx;
height: 96rpx;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-lg;
margin-right: 24rpx;
flex-shrink: 0;
}
.create-title {
font-size: $font-xl;
.header-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.card-title {
font-size: 32rpx;
font-weight: $font-bold;
color: $text-primary;
text-align: center;
display: block;
margin-bottom: $spacing-sm;
}
.create-desc {
font-size: $font-base;
.card-subtitle {
font-size: 24rpx;
color: $text-secondary;
text-align: center;
line-height: 1.6;
display: block;
margin-bottom: $spacing-3xl;
}
.create-steps {
.progress-steps {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $spacing-3xl;
padding: 0 $spacing-md;
margin-bottom: 40rpx;
padding: 0 20rpx;
}
.progress-line {
position: absolute;
top: 28rpx;
left: 20%;
right: 20%;
height: 2rpx;
background: linear-gradient(90deg, #e8e8e8, #e8e8e8);
z-index: 0;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
flex: 1;
position: relative;
z-index: 1;
}
.step-number {
.step-circle {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, $primary-color, $primary-400);
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
color: #fff;
font-size: $font-lg;
font-weight: $font-bold;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-sm;
box-shadow: 0 4rpx 12rpx rgba($primary-color, 0.3);
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.step-text {
font-size: $font-xs;
.step-num {
font-size: 24rpx;
font-weight: $font-bold;
color: #fff;
}
.step-label {
font-size: 22rpx;
color: $text-secondary;
text-align: center;
}
.step-divider {
width: 60rpx;
height: 2rpx;
background: $border-base;
margin: 0 $spacing-xs;
.benefits-list {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 40rpx;
padding: 32rpx;
background: linear-gradient(135deg, rgba(82, 196, 26, 0.03), rgba(115, 209, 61, 0.03));
border-radius: 16rpx;
border: 1rpx solid rgba(82, 196, 26, 0.1);
}
.benefit-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.benefit-text {
font-size: 26rpx;
color: $text-primary;
font-weight: $font-medium;
}
.create-shop-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
box-shadow: 0 12rpx 40rpx rgba(102, 126, 234, 0.4);
border: none;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
&:active {
transform: scale(0.98);
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
&::before {
left: 100%;
}
}
}
.logout-btn {
width: 100%;
height: 88rpx;
background: transparent;
border: 1rpx solid rgba(0, 0, 0, 0.1);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
transition: all 0.3s ease;
&::after {
border: none;
}
&:active {
background: rgba(0, 0, 0, 0.02);
transform: scale(0.98);
}
}
.logout-text {
font-size: 28rpx;
color: $text-secondary;
}
/* ========== 按钮样式 ========== */
.primary-btn {
width: 100%;
height: 88rpx;
height: 96rpx;
background: linear-gradient(135deg, $primary-color, $primary-400);
border-radius: $radius-round;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba($primary-color, 0.3);
gap: 12rpx;
box-shadow: 0 12rpx 40rpx rgba($primary-color, 0.4);
border: none;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&.large {
height: 96rpx;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s ease;
}
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba($primary-color, 0.2);
box-shadow: 0 8rpx 24rpx rgba($primary-color, 0.3);
&::before {
left: 100%;
}
}
&.large {
height: 96rpx;
}
}
@@ -859,9 +1183,7 @@ function navigateTo(url: string) {
gap: $spacing-lg;
.menu-card:nth-child(5) {
grid-column: 1 / -1;
max-width: 50%;
margin: 0 auto;
grid-column: 1;
}
}
@@ -47,7 +47,6 @@
:class="['type-item', { active: form.paymentChannel === type.value }]"
@tap="form.paymentChannel = type.value"
>
<u-icon :name="type.icon" :size="28" :color="form.paymentChannel === type.value ? '#FF6B35' : '#999'" />
<text class="type-text">{{ type.label }}</text>
</view>
</view>
+6 -6
View File
@@ -68,27 +68,27 @@ export function confirmPlatformWithdrawal(id: number, transactionNo: string) {
}
export function approveUserWithdrawal(id: number) {
return request.put(`/api/admin/finance/withdrawals/users/${id}/approve`);
return request.put(`/api/admin/finance/withdrawals/${id}/approve?type=user`);
}
export function rejectUserWithdrawal(id: number, reason: string) {
return request.put(`/api/admin/finance/withdrawals/users/${id}/reject`, { rejectReason: reason });
return request.put(`/api/admin/finance/withdrawals/${id}/reject?type=user`, { rejectReason: reason });
}
export function confirmUserWithdrawal(id: number, transactionNo: string) {
return request.put(`/api/admin/finance/withdrawals/users/${id}/confirm`, { paymentNo: transactionNo });
return request.put(`/api/admin/finance/withdrawals/${id}/confirm?type=user`, { paymentNo: transactionNo });
}
export function approveMerchantWithdrawal(id: number) {
return request.put(`/api/admin/finance/withdrawals/merchants/${id}/approve`);
return request.put(`/api/admin/finance/withdrawals/${id}/approve?type=merchant`);
}
export function rejectMerchantWithdrawal(id: number, reason: string) {
return request.put(`/api/admin/finance/withdrawals/merchants/${id}/reject`, { rejectReason: reason });
return request.put(`/api/admin/finance/withdrawals/${id}/reject?type=merchant`, { rejectReason: reason });
}
export function confirmMerchantWithdrawal(id: number, transactionNo: string) {
return request.put(`/api/admin/finance/withdrawals/merchants/${id}/confirm`, { paymentNo: transactionNo });
return request.put(`/api/admin/finance/withdrawals/${id}/confirm?type=merchant`, { paymentNo: transactionNo });
}
// 平台银行卡管理
@@ -4,6 +4,7 @@ import { Card, Descriptions, Tag, Button, Space, Tabs, Table, Statistic, Row, Co
import { ArrowLeftOutlined, ShopOutlined, PhoneOutlined, EnvironmentOutlined, StarOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { getMerchantDetail, approveMerchant, rejectMerchant, freezeMerchant, unfreezeMerchant } from '@/api/admin';
import dayjs from 'dayjs';
const statusMap: Record<string, { color: string; label: string }> = {
pending: { color: 'gold', label: '待审核' },
@@ -74,7 +75,7 @@ const MerchantDetail: React.FC = () => {
{ title: '价格', dataIndex: 'price', width: 100, render: (v) => `¥${v}` },
{ title: '面积', dataIndex: 'area', width: 100, render: (v) => `${v}` },
{ title: '状态', dataIndex: 'status', width: 100, render: (s) => <Tag color={s === 'available' ? 'green' : 'default'}>{s === 'available' ? '可租' : '已租'}</Tag> },
{ title: '发布时间', dataIndex: 'createdAt', width: 180 },
{ title: '发布时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
const orderColumns: ColumnsType<any> = [
@@ -83,7 +84,7 @@ const MerchantDetail: React.FC = () => {
{ title: '租客', dataIndex: 'userName', width: 120 },
{ title: '金额', dataIndex: 'totalAmount', width: 100, render: (v) => `¥${v}` },
{ title: '状态', dataIndex: 'status', width: 120, render: (s) => <Tag>{s}</Tag> },
{ title: '下单时间', dataIndex: 'createdAt', width: 180 },
{ title: '下单时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
const reviewColumns: ColumnsType<any> = [
@@ -91,7 +92,7 @@ const MerchantDetail: React.FC = () => {
{ title: '评价人', dataIndex: 'userName', width: 120 },
{ title: '评分', dataIndex: 'rating', width: 100, render: (v) => <><StarOutlined style={{ color: '#faad14' }} /> {v}</> },
{ title: '评价内容', dataIndex: 'content', ellipsis: true },
{ title: '评价时间', dataIndex: 'createdAt', width: 180 },
{ title: '评价时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
return (
@@ -137,8 +138,8 @@ const MerchantDetail: React.FC = () => {
<Descriptions.Item label="详细地址">{merchant.address || '-'}</Descriptions.Item>
<Descriptions.Item label="营业时间">{merchant.businessHours || '-'}</Descriptions.Item>
<Descriptions.Item label="店铺简介" span={2}>{merchant.description || '-'}</Descriptions.Item>
<Descriptions.Item label="入驻时间">{merchant.createdAt}</Descriptions.Item>
<Descriptions.Item label="更新时间">{merchant.updatedAt}</Descriptions.Item>
<Descriptions.Item label="入驻时间">{merchant.createdAt ? dayjs(merchant.createdAt).format('YYYY-MM-DD HH:mm') : '-'}</Descriptions.Item>
<Descriptions.Item label="更新时间">{merchant.updatedAt ? dayjs(merchant.updatedAt).format('YYYY-MM-DD HH:mm') : '-'}</Descriptions.Item>
{merchant.status === 'rejected' && (
<Descriptions.Item label="拒绝原因" span={2}>
<span style={{ color: '#ff4d4f' }}>{merchant.rejectReason}</span>
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { Table, Tag, Button, Space, Select, Modal, Input, message, Popconfirm } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getMerchantList, approveMerchant, rejectMerchant, freezeMerchant, unfreezeMerchant } from '@/api/admin';
import dayjs from 'dayjs';
const { Option } = Select;
@@ -80,7 +81,7 @@ const MerchantList: React.FC = () => {
title: '状态', dataIndex: 'status', width: 100,
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
},
{ title: '入驻时间', dataIndex: 'createdAt', width: 180 },
{ title: '入驻时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{
title: '操作', width: 300, fixed: 'right',
render: (_, r) => (
+2 -1
View File
@@ -3,6 +3,7 @@ import { Table, Tag, Select, Space, Input, Button } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useNavigate } from 'react-router-dom';
import { getOrderList } from '@/api/admin';
import dayjs from 'dayjs';
const { Option } = Select;
@@ -50,7 +51,7 @@ const OrderList: React.FC = () => {
title: '状态', dataIndex: 'status', width: 100,
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
},
{ title: '下单时间', dataIndex: 'createdAt', width: 180 },
{ title: '下单时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{
title: '操作', key: 'action', width: 100, fixed: 'right',
render: (_, record) => (
+2 -1
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Table, Button, Space, Tag, Modal, Input, Select, message, Image } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getAdminRoomList, approveRoom, rejectRoom } from '@/api/room';
import dayjs from 'dayjs';
const typeLabels: Record<string, string> = { hotel: '酒店', homestay: '民宿', apartment: '公寓', hostel: '青旅' };
const auditStatusMap: Record<string, { label: string; color: string }> = {
@@ -74,7 +75,7 @@ const RoomAudit: React.FC = () => {
title: '拒绝原因', dataIndex: 'auditRejectReason', width: 160, ellipsis: true,
render: (v) => v || '-',
},
{ title: '提交时间', dataIndex: 'createdAt', width: 170 },
{ title: '提交时间', dataIndex: 'createdAt', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{
title: '操作', width: 180, fixed: 'right',
render: (_, r) => (
+2 -1
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Table, Tag, Button, Space, Select, Input, Popconfirm, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getUserList, freezeUser, unfreezeUser } from '@/api/admin';
import dayjs from 'dayjs';
const { Option } = Select;
const { Search } = Input;
@@ -63,7 +64,7 @@ const UserList: React.FC = () => {
title: '状态', dataIndex: 'status', width: 100,
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
},
{ title: '注册时间', dataIndex: 'createdAt', width: 180 },
{ title: '注册时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{
title: '操作', width: 150, fixed: 'right',
render: (_, r) => (
@@ -12,7 +12,7 @@ interface PlatformAccount {
frozen_balance: number;
total_income: number;
total_expense: number;
total_service_fee: number;
total_service_fee?: number;
status: string;
created_at: string;
updated_at: string;
@@ -45,6 +45,7 @@ const PlatformWallet: React.FC = () => {
setLoading(true);
try {
const res = await getPlatformAccounts({});
console.log('Platform account data:', res.data);
if (res.data && res.data.length > 0) {
setAccount(res.data[0]);
}
@@ -112,6 +113,9 @@ const PlatformWallet: React.FC = () => {
// 可提现金额 = 钱包余额 - 冻结余额
const withdrawableAmount = account ? Number(account.balance) - Number(account.frozen_balance) : 0;
// 服务费收入 = 总收入 - 总支出(如果后端没有返回 total_service_fee
const serviceFeeIncome = account ? (account.total_service_fee ?? (Number(account.total_income) - Number(account.total_expense))) : 0;
return (
<div>
<h2 style={{ marginBottom: 24 }}></h2>
@@ -167,7 +171,7 @@ const PlatformWallet: React.FC = () => {
<Card>
<Statistic
title="服务费收入"
value={account.total_service_fee}
value={serviceFeeIncome}
precision={2}
suffix="元"
valueStyle={{ color: '#722ed1', fontSize: 32 }}
@@ -251,7 +255,7 @@ const PlatformWallet: React.FC = () => {
</span>
</Descriptions.Item>
<Descriptions.Item label="服务费收入">
<span style={{ color: '#722ed1', fontSize: 16, fontWeight: 'bold' }}>{formatMoney(account.total_service_fee)}</span>
<span style={{ color: '#722ed1', fontSize: 16, fontWeight: 'bold' }}>{formatMoney(serviceFeeIncome)}</span>
</Descriptions.Item>
<Descriptions.Item label="其他收入">
<span style={{ color: '#13c2c2' }}>{formatMoney(0)}</span>
@@ -81,7 +81,7 @@ const PlatformWithdrawals: React.FC = () => {
const handleApprove = async (record: PlatformWithdrawal) => {
Modal.confirm({
title: '确认审核通过',
content: `确定要审核通过提现申请 ${record.withdrawal_no} 吗?`,
content: `确定要审核通过提现申请 ${record.withdrawNo} 吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
@@ -83,16 +83,9 @@ const Withdrawals: React.FC = () => {
}
};
const handleViewDetail = async (id: number) => {
try {
// TODO: 实现提现详情接口
message.info('提现详情功能待实现');
// const res = await getWithdrawalDetail(id);
// setCurrentDetail(res.data);
// setDetailVisible(true);
} catch (error) {
message.error('获取提现详情失败');
}
const handleViewDetail = (record: Withdrawal) => {
setCurrentDetail(record);
setDetailVisible(true);
};
const handleApprove = async (id: number) => {
@@ -240,7 +233,7 @@ const Withdrawals: React.FC = () => {
fixed: 'right',
render: (_, record: Withdrawal) => (
<Space>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record.id)}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
</Button>
{record.status === 'pending' && (
@@ -331,19 +324,25 @@ const Withdrawals: React.FC = () => {
<Descriptions.Item label="申请人">
{currentDetail.userName || currentDetail.merchantName} (ID: {currentDetail.userId || currentDetail.merchantId})
</Descriptions.Item>
<Descriptions.Item label="申请时间">{currentDetail.createdAt}</Descriptions.Item>
<Descriptions.Item label="提现金额">¥{currentDetail.amount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="手续费">¥{currentDetail.fee.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="提现金额">
<span style={{ fontSize: 16, fontWeight: 'bold' }}>¥{Number(currentDetail.amount).toFixed(2)}</span>
</Descriptions.Item>
<Descriptions.Item label="手续费">¥{Number(currentDetail.fee || 0).toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="到账金额">
<span style={{ fontWeight: 'bold', color: '#52c41a' }}>
¥{currentDetail.actualAmount.toFixed(2)}
<span style={{ fontWeight: 'bold', color: '#52c41a', fontSize: 16 }}>
¥{Number(currentDetail.actualAmount).toFixed(2)}
</span>
</Descriptions.Item>
<Descriptions.Item label="开户银行">{currentDetail.bankName}</Descriptions.Item>
<Descriptions.Item label="银行账号">{currentDetail.bankAccount}</Descriptions.Item>
<Descriptions.Item label="账户名称">{currentDetail.accountName}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={currentDetail.status === 'completed' ? 'green' : 'orange'}>
<Tag color={
currentDetail.status === 'completed' ? 'green' :
currentDetail.status === 'approved' ? 'blue' :
currentDetail.status === 'rejected' ? 'red' :
currentDetail.status === 'pending' ? 'orange' : 'default'
}>
{currentDetail.status === 'pending' && '待审核'}
{currentDetail.status === 'approved' && '已通过'}
{currentDetail.status === 'rejected' && '已拒绝'}
@@ -351,11 +350,12 @@ const Withdrawals: React.FC = () => {
{currentDetail.status === 'cancelled' && '已取消'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="申请时间">{formatDateTime(currentDetail.createdAt)}</Descriptions.Item>
{currentDetail.processedAt && (
<Descriptions.Item label="处理时间">{currentDetail.processedAt}</Descriptions.Item>
<Descriptions.Item label="处理时间">{formatDateTime(currentDetail.processedAt)}</Descriptions.Item>
)}
{currentDetail.paidAt && (
<Descriptions.Item label="打款时间">{currentDetail.paidAt}</Descriptions.Item>
<Descriptions.Item label="打款时间">{formatDateTime(currentDetail.paidAt)}</Descriptions.Item>
)}
{currentDetail.transactionNo && (
<Descriptions.Item label="交易单号">{currentDetail.transactionNo}</Descriptions.Item>
@@ -0,0 +1,147 @@
# 周结算接口修复说明
## 问题描述
调用 `/api/admin/finance/settlements/execute-weekly` 接口时,即使没有实际执行结算,也会返回成功消息 `"周结算任务已执行完成"`,导致用户误以为结算成功。
## 问题原因
1. **Controller 层**:没有捕获 Service 层抛出的异常,也没有返回详细的执行结果
2. **Service 层**:在以下情况会抛出 `BadRequestException`
- 该周期已经结算过
- 没有需要结算的订单
- 所有订单都已结算
这些异常导致接口返回 400 错误,但前端可能没有正确处理,或者异常被中间件捕获后返回了成功状态。
## 修复方案
### 1. Controller 层改进
**修改前:**
```typescript
@Post('execute-weekly')
async executeWeeklySettlement() {
await this.settlementService.handleWeeklySettlement();
return { message: '周结算任务已执行完成' };
}
```
**修改后:**
```typescript
@Post('execute-weekly')
async executeWeeklySettlement() {
const result = await this.settlementService.handleWeeklySettlement();
return {
message: '周结算任务已执行完成',
...result
};
}
```
### 2. Service 层改进
将抛出异常改为返回明确的结果对象:
**修改前:**
```typescript
if (existingSettlements > 0) {
throw new BadRequestException(`该周期已经结算过,无法重复结算`);
}
```
**修改后:**
```typescript
if (existingSettlements > 0) {
this.logger.warn(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,跳过`);
return {
successCount: 0,
failCount: 0,
totalOrders: 0,
skipped: true,
reason: `该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过`
};
}
```
## 返回值结构
### 成功执行结算
```json
{
"code": 200,
"message": "success",
"data": {
"message": "周结算任务已执行完成",
"successCount": 5,
"failCount": 0,
"totalOrders": 120,
"skipped": false
}
}
```
### 跳过结算(已结算过)
```json
{
"code": 200,
"message": "success",
"data": {
"message": "周结算任务已执行完成",
"successCount": 0,
"failCount": 0,
"totalOrders": 0,
"skipped": true,
"reason": "该周期 2026-05-19 ~ 2026-05-25 已经结算过"
}
}
```
### 跳过结算(无订单)
```json
{
"code": 200,
"message": "success",
"data": {
"message": "周结算任务已执行完成",
"successCount": 0,
"failCount": 0,
"totalOrders": 0,
"skipped": true,
"reason": "该周期内没有需要结算的订单"
}
}
```
## 前端处理建议
前端应该根据返回的 `skipped` 字段判断是否真正执行了结算:
```typescript
const response = await executeWeeklySettlement();
if (response.skipped) {
// 显示警告信息
message.warning(response.reason);
} else if (response.successCount > 0) {
// 显示成功信息
message.success(`结算成功:${response.successCount} 个商家,共 ${response.totalOrders} 个订单`);
if (response.failCount > 0) {
message.warning(`${response.failCount} 个商家结算失败,请查看日志`);
}
} else {
message.info('没有需要结算的数据');
}
```
## 测试步骤
1. **首次执行**:调用接口,应该返回 `skipped: false` 和实际的结算数据
2. **重复执行**:再次调用接口,应该返回 `skipped: true` 和原因说明
3. **无订单场景**:在没有已完成订单的情况下调用,应该返回相应的提示
## 相关文件
- [settlement-admin.controller.ts](../src/modules/admin/finance/settlement-admin.controller.ts)
- [settlement.service.ts](../src/modules/shared/finance/settlement.service.ts)
+18 -5
View File
@@ -9,8 +9,14 @@
```
finance/
├── entities/
│ ├── account.entity.ts # 账户实体
│ ├── transaction.entity.ts # 交易流水实体
│ ├── user-account.entity.ts # 用户账户实体
│ ├── merchant-account.entity.ts # 商家账户实体
│ ├── platform-account.entity.ts # 平台账户实体
│ ├── system-account.entity.ts # 系统总账户实体
│ ├── user-transaction.entity.ts # 用户交易流水实体
│ ├── merchant-transaction.entity.ts # 商家交易流水实体
│ ├── platform-transaction.entity.ts # 平台交易流水实体
│ ├── system-transaction.entity.ts # 系统总账户交易流水实体
│ ├── settlement.entity.ts # 结算单实体
│ ├── settlement-item.entity.ts # 结算明细实体
│ ├── user-withdrawal.entity.ts # 用户提现实体
@@ -203,8 +209,14 @@ finance/
| 表名 | 说明 |
|------|------|
| `accounts` | 账户表(用户/商家/平台) |
| `transactions` | 交易流水表(复式记账) |
| `system_accounts` | 系统总账户表 |
| `system_transactions` | 系统总账户交易流水表 |
| `platform_accounts` | 平台账户表 |
| `platform_transactions` | 平台交易流水表 |
| `merchant_accounts` | 商家账户表 |
| `merchant_transactions` | 商家交易流水表 |
| `user_accounts` | 用户账户表 |
| `user_transactions` | 用户交易流水表 |
| `settlements` | 结算单表 |
| `settlement_items` | 结算明细表 |
| `user_withdrawals` | 用户提现表 |
@@ -216,12 +228,13 @@ finance/
## 技术特性
1. **复式记账**: 每笔转账生成两条交易流水(支出+收入
1. **分表设计**: 账户和交易流水按角色分表(用户/商家/平台/系统
2. **乐观锁**: 账户余额更新使用版本号防止并发问题
3. **事务保证**: 所有涉及金额变动的操作都在事务中执行
4. **冻结机制**: 提现时先冻结余额,审核通过后扣减
5. **自动对账**: 每日自动检查账户余额和交易流水一致性
6. **定时结算**: 每周自动生成商家结算单
7. **资金守恒**: 系统总账户 = 商家账户 + 用户账户 + 平台账户
---
@@ -1,49 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, VersionColumn } from 'typeorm';
@Entity('accounts')
@Index(['account_type', 'owner_id'], { unique: true })
export class Account {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@Column({
type: 'enum',
enum: ['user', 'merchant', 'platform'],
comment: '账户类型'
})
@Index()
account_type: 'user' | 'merchant' | 'platform';
@Column({ type: 'bigint', unsigned: true, comment: '所有者IDuser_id/merchant_id/platform固定为0' })
owner_id: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额' })
balance: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '冻结余额(提现中、退款中)' })
frozen_balance: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计收入' })
total_income: number;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '累计支出' })
total_expense: number;
@VersionColumn({ comment: '乐观锁版本号' })
version: number;
@Column({
type: 'enum',
enum: ['active', 'frozen', 'closed'],
default: 'active',
comment: '状态'
})
@Index()
status: 'active' | 'frozen' | 'closed';
@CreateDateColumn({ comment: '创建时间' })
created_at: Date;
@UpdateDateColumn({ comment: '更新时间' })
updated_at: Date;
}
@@ -1,2 +0,0 @@
// 别名映射到 mkt-activity.entity.ts
export { MktActivity as InviteActivity } from './mkt-activity.entity';
@@ -1,2 +0,0 @@
// 别名映射到 mkt-cashback.entity.ts
export { MktCashback as InviteCashback } from './mkt-cashback.entity';
@@ -1,2 +0,0 @@
// 别名映射到 mkt-invitation.entity.ts
export { MktInvitation as InviteRecord } from './mkt-invitation.entity';
@@ -1,2 +0,0 @@
// 别名映射到 platform-config.entity.ts
export { PlatformConfig as SystemConfig } from './platform-config.entity';
@@ -1,66 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
@Entity('transactions')
export class Transaction {
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
id: number;
@Column({ type: 'varchar', length: 32, unique: true, comment: '交易流水号(全局唯一)' })
transaction_no: string;
@Column({ type: 'bigint', unsigned: true, comment: '账户ID' })
@Index()
account_id: number;
@Column({
type: 'enum',
enum: ['user', 'merchant', 'platform'],
comment: '账户类型'
})
account_type: 'user' | 'merchant' | 'platform';
@Column({ type: 'bigint', unsigned: true, comment: '账户所有者ID' })
owner_id: number;
@Column({
type: 'enum',
enum: ['income', 'expense'],
comment: '方向:income-收入/expense-支出'
})
direction: 'income' | 'expense';
@Column({ type: 'decimal', precision: 12, scale: 2, comment: '金额(正数)' })
amount: number;
@Column({ type: 'decimal', precision: 12, scale: 2, comment: '交易前余额' })
balance_before: number;
@Column({ type: 'decimal', precision: 12, scale: 2, comment: '交易后余额' })
balance_after: number;
@Column({ type: 'varchar', length: 50, comment: '交易类型' })
@Index()
transaction_type: string;
@Column({ type: 'varchar', length: 50, comment: '业务类型:order/refund/settlement/cashback/withdraw' })
business_type: string;
@Column({ type: 'bigint', unsigned: true, nullable: true, comment: '业务ID(订单ID/提现ID等)' })
business_id: number;
@Column({ type: 'varchar', length: 32, nullable: true, comment: '业务单号(订单号/提现单号等)' })
business_no: string;
@Column({ type: 'bigint', unsigned: true, nullable: true, comment: '对方账户ID(复式记账关联)' })
related_account_id: number;
@Column({ type: 'varchar', length: 500, nullable: true, comment: '备注' })
remark: string;
@CreateDateColumn({ comment: '创建时间' })
@Index()
created_at: Date;
}
@Index(['business_type', 'business_id'])
export class TransactionIndex {}
@@ -1,57 +0,0 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { CouponService } from '@/modules/shared/coupon/coupon.service';
import { JwtAuthGuard, RolesGuard } from '@/common';
import { Roles } from '@/common/decorators/roles.decorator';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { CreateCouponDto, UpdateCouponDto, QueryCouponDto } from './dto/coupon.dto';
@ApiTags('优惠券管理(管理员)')
@Controller('admin/coupons')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth()
export class CouponController {
constructor(private readonly couponService: CouponService) {}
@Post()
@ApiOperation({ summary: '创建优惠券' })
async create(@Body() dto: CreateCouponDto, @CurrentUser() user: any) {
return this.couponService.create(dto, user.id);
}
@Put(':id')
@ApiOperation({ summary: '更新优惠券' })
async update(@Param('id') id: number, @Body() dto: UpdateCouponDto) {
return this.couponService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: '删除优惠券' })
async delete(@Param('id') id: number) {
await this.couponService.delete(id);
return { message: '删除成功' };
}
@Get()
@ApiOperation({ summary: '查询优惠券列表' })
async findAll(@Query() dto: QueryCouponDto) {
return this.couponService.findAll(dto);
}
@Get(':id')
@ApiOperation({ summary: '获取优惠券详情' })
async findOne(@Param('id') id: number) {
return this.couponService.findOne(id);
}
}
@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { CouponModule } from '@/modules/shared/coupon/coupon.module';
import { CouponController } from './coupon.controller';
import { CouponAdminController } from './coupon-admin.controller';
@Module({
imports: [CouponModule],
controllers: [CouponController],
controllers: [CouponAdminController],
})
export class AdminCouponModule {}
@@ -33,12 +33,6 @@ export class AccountAdminController {
return this.accountService.getPlatformAccountDetail(id);
}
@Get('platform/:id/balance')
@ApiOperation({ summary: '查询平台账户余额' })
async getPlatformAccountBalance(@Param('id') id: number) {
return this.accountService.getPlatformAccountBalance(id);
}
// ==================== 用户账户管理 ====================
@Get('users')
@@ -47,12 +41,6 @@ export class AccountAdminController {
return this.accountService.getUserAccounts(dto);
}
@Get('users/:userId')
@ApiOperation({ summary: '查询用户账户详情' })
async getUserAccountDetail(@Param('userId') userId: number) {
return this.accountService.getUserAccountDetail(userId);
}
@Get('users/summary')
@ApiOperation({ summary: '用户账户汇总统计' })
async getUserAccountsSummary() {
@@ -67,12 +55,6 @@ export class AccountAdminController {
return this.accountService.getMerchantAccounts(dto);
}
@Get('merchants/:merchantId')
@ApiOperation({ summary: '查询商家账户详情' })
async getMerchantAccountDetail(@Param('merchantId') merchantId: number) {
return this.accountService.getMerchantAccountDetail(merchantId);
}
@Get('merchants/summary')
@ApiOperation({ summary: '商家账户汇总统计' })
async getMerchantAccountsSummary() {
@@ -89,8 +89,11 @@ export class SettlementAdminController {
@Post('execute-weekly')
@ApiOperation({ summary: '手动执行周结算(所有商家)' })
async executeWeeklySettlement() {
await this.settlementService.handleWeeklySettlement();
return { message: '周结算任务已执行完成' };
const result = await this.settlementService.handleWeeklySettlement();
return {
message: '周结算任务已执行完成',
...result
};
}
@Put(':id/approve')
@@ -40,13 +40,6 @@ export class TransactionAdminController {
});
}
@Get('platform/:id')
@ApiOperation({ summary: '查询平台交易详情' })
async getPlatformTransactionDetail(@Param('id') id: number) {
// TODO: 实现根据ID查询交易详情
return { message: '功能开发中' };
}
@Get('platform/export')
@ApiOperation({ summary: '导出平台交易流水' })
async exportPlatformTransactions(@Query() dto: QueryPlatformTransactionDto) {
@@ -71,24 +64,6 @@ export class TransactionAdminController {
});
}
@Get('users/:userId')
@ApiOperation({ summary: '查询指定用户交易流水' })
async getUserTransactionsByUserId(
@Param('userId') userId: number,
@Query() dto: QueryUserTransactionDto,
) {
return this.transactionService.getUserTransactions({
userId,
direction: dto.direction,
transactionType: dto.transactionType,
businessType: dto.businessType,
startDate: dto.startDate,
endDate: dto.endDate,
page: dto.page,
pageSize: dto.pageSize,
});
}
// ==================== 商家交易流水 ====================
@Get('merchants')
@@ -105,22 +80,4 @@ export class TransactionAdminController {
pageSize: dto.pageSize,
});
}
@Get('merchants/:merchantId')
@ApiOperation({ summary: '查询指定商家交易流水' })
async getMerchantTransactionsByMerchantId(
@Param('merchantId') merchantId: number,
@Query() dto: QueryMerchantTransactionDto,
) {
return this.transactionService.getMerchantTransactions({
merchantId,
direction: dto.direction,
transactionType: dto.transactionType,
businessType: dto.businessType,
startDate: dto.startDate,
endDate: dto.endDate,
page: dto.page,
pageSize: dto.pageSize,
});
}
}
@@ -1,58 +0,0 @@
import {
Controller,
Get,
Post,
Body,
Query,
UseGuards,
Param,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { CouponService } from './coupon.service';
import { JwtAuthGuard } from '@/common';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { ReceiveCouponDto, QueryUserCouponDto, QueryCouponDto } from './dto/coupon.dto';
@ApiTags('优惠券(用户)')
@Controller('app/coupons')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class CouponController {
constructor(private readonly couponService: CouponService) {}
@Get('available')
@ApiOperation({ summary: '查询可领取的优惠券' })
async findAvailable(@Query() dto: QueryCouponDto) {
// 只返回active状态的优惠券
return this.couponService.findAll({ ...dto, status: 'active' });
}
@Post('receive')
@ApiOperation({ summary: '领取优惠券' })
async receive(@Body() dto: ReceiveCouponDto, @CurrentUser() user: any) {
return this.couponService.receive(user.sub, dto.couponId);
}
@Get('my')
@ApiOperation({ summary: '查询我的优惠券' })
async findMyCoupons(@Query() dto: QueryUserCouponDto, @CurrentUser() user: any) {
return this.couponService.findUserCoupons(user.id, dto);
}
@Get('usable/:orderId')
@ApiOperation({ summary: '查询订单可用优惠券' })
async findUsableCoupons(
@Param('orderId') orderId: number,
@Query('orderAmount') orderAmount: number,
@Query('merchantId') merchantId: number,
@Query('roomId') roomId: number,
@CurrentUser() user: any,
) {
return this.couponService.findAvailableCoupons(
user.id,
orderAmount,
merchantId,
roomId,
);
}
}
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CouponController } from './coupon.controller';
import { CouponUserController } from './coupon-user.controller';
import { CouponService } from './coupon.service';
import { Coupon } from '@/entities/coupon.entity';
import { UserCoupon } from '@/entities/user-coupon.entity';
@@ -9,7 +9,7 @@ import { UserCoupon } from '@/entities/user-coupon.entity';
imports: [
TypeOrmModule.forFeature([Coupon, UserCoupon]),
],
controllers: [CouponController],
controllers: [CouponUserController],
providers: [CouponService],
exports: [CouponService],
})
@@ -1,86 +0,0 @@
import {
Controller,
Get,
Post,
Put,
Body,
UseGuards,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { UserService } from './user.service';
import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import {
UpdateProfileDto,
ChangePasswordDto,
VerifyIdentityDto,
} from './dto/user.dto';
@ApiTags('用户')
@Controller('app')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class UserUserController {
constructor(private readonly userService: UserService) {}
@Get('profile')
@ApiOperation({ summary: '获取个人信息' })
async getProfile(@CurrentUser('sub') userId: number) {
return this.userService.findById(userId);
}
@Post('profile')
@ApiOperation({ summary: '更新个人信息' })
async updateProfile(
@CurrentUser('sub') userId: number,
@Body() dto: UpdateProfileDto,
) {
return this.userService.updateProfile(userId, dto);
}
@Post('avatar')
@ApiOperation({ summary: '上传头像' })
@UseInterceptors(FileInterceptor('file'))
async uploadAvatar(
@CurrentUser('sub') userId: number,
@UploadedFile() file: Express.Multer.File,
) {
return this.userService.uploadAvatar(userId, file);
}
@Put('profile')
@ApiOperation({ summary: '更新个人信息(旧接口,保留兼容)' })
async updateProfilePut(
@CurrentUser('sub') userId: number,
@Body() dto: UpdateProfileDto,
) {
return this.userService.updateProfile(userId, dto);
}
@Put('password')
@ApiOperation({ summary: '修改密码' })
async changePassword(
@CurrentUser('sub') userId: number,
@Body() dto: ChangePasswordDto,
) {
return this.userService.changePassword(userId, dto);
}
@Post('verify')
@ApiOperation({ summary: '实名认证' })
async verifyIdentity(
@CurrentUser('sub') userId: number,
@Body() dto: VerifyIdentityDto,
) {
return this.userService.verifyIdentity(userId, dto);
}
@Get('verify/status')
@ApiOperation({ summary: '获取实名认证状态' })
async getVerifyStatus(@CurrentUser('sub') userId: number) {
return this.userService.getVerifyStatus(userId);
}
}
@@ -9,7 +9,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantFinanceService } from './finance.service';
import { MerchantProfileService } from '../profile/profile.service';
import { MerchantService } from '../merchant.service';
import { QueryTransactionDto } from './dto/finance.dto';
@ApiTags('商家财务管理')
@@ -19,11 +19,11 @@ import { QueryTransactionDto } from './dto/finance.dto';
export class MerchantFinanceController {
constructor(
private readonly financeService: MerchantFinanceService,
private readonly profileService: MerchantProfileService,
private readonly merchantService: MerchantService,
) {}
private async getMerchantId(sellerId: number): Promise<number> {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return merchant.id;
}
@@ -7,14 +7,12 @@ import { WithdrawalMerchantController } from './withdrawal-merchant.controller';
import { TransactionSellerController } from './transaction-seller.controller';
import { MerchantAccount } from '@/entities/merchant-account.entity';
import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
import { MerchantProfileModule } from '../profile/profile.module';
import { MerchantModule } from '../merchant.module';
import { FinanceModule } from '@/modules/shared/finance/finance.module';
@Module({
imports: [
TypeOrmModule.forFeature([MerchantAccount, MerchantTransaction]),
MerchantProfileModule,
forwardRef(() => MerchantModule),
forwardRef(() => FinanceModule),
],
@@ -1,59 +0,0 @@
import {
Controller,
Get,
Put,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MerchantService } from './merchant.service';
import { JwtAuthGuard, RolesGuard } from '@/common';
import { Roles } from '@/common/decorators/roles.decorator';
import { QueryMerchantDto } from './dto/merchant.dto';
@ApiTags('商家管理(管理员)')
@Controller('admin/merchants')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth()
export class MerchantAdminController {
constructor(private readonly merchantService: MerchantService) {}
@Get()
@ApiOperation({ summary: '获取商家列表' })
async findAll(@Query() query: QueryMerchantDto) {
return this.merchantService.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: '获取商家详情' })
async findById(@Param('id') id: number) {
return this.merchantService.findById(id);
}
@Put(':id/approve')
@ApiOperation({ summary: '审核通过' })
async approve(@Param('id') id: number) {
return this.merchantService.approve(id);
}
@Put(':id/reject')
@ApiOperation({ summary: '审核拒绝' })
async reject(@Param('id') id: number, @Body('reason') reason: string) {
return this.merchantService.reject(id, reason);
}
@Put(':id/freeze')
@ApiOperation({ summary: '冻结店铺' })
async freeze(@Param('id') id: number) {
return this.merchantService.freeze(id);
}
@Put(':id/unfreeze')
@ApiOperation({ summary: '解冻店铺' })
async unfreeze(@Param('id') id: number) {
return this.merchantService.unfreeze(id);
}
}
@@ -9,15 +9,14 @@ import { Review } from '@/entities/review.entity';
import { MerchantService } from './merchant.service';
import { StatisticsService } from './statistics.service';
import { MerchantSellerController } from './merchant-seller.controller';
import { MerchantAdminController } from './merchant-admin.controller';
import { MerchantAuthModule } from './auth/auth.module';
import { MerchantProfileModule } from './profile/profile.module';
import { MerchantRoomModule } from './room/room.module';
import { MerchantRoomCalendarModule } from './room-calendar/room-calendar.module';
import { MerchantOrderModule } from './order/order.module';
import { MerchantReviewModule } from './review/review.module';
import { MerchantFinanceModule } from './finance/finance.module';
import { MerchantStatisticsModule } from './statistics/statistics.module';
import { MerchantProfileModule } from './profile/profile.module';
@Module({
imports: [
@@ -33,7 +32,6 @@ import { MerchantStatisticsModule } from './statistics/statistics.module';
],
controllers: [
MerchantSellerController,
MerchantAdminController,
],
providers: [MerchantService, StatisticsService],
exports: [MerchantService, StatisticsService],
@@ -13,7 +13,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MerchantOrderService } from './order.service';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantProfileService } from '../profile/profile.service';
import { MerchantService } from '../merchant.service';
import { QueryOrderDto } from './dto/order.dto';
@ApiTags('订单管理(商家)')
@@ -23,7 +23,7 @@ import { QueryOrderDto } from './dto/order.dto';
export class MerchantOrderController {
constructor(
private readonly orderService: MerchantOrderService,
private readonly profileService: MerchantProfileService,
private readonly merchantService: MerchantService,
) {}
@Get()
@@ -32,7 +32,7 @@ export class MerchantOrderController {
@CurrentSeller('sub') sellerId: number,
@Query() query: QueryOrderDto,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.findByMerchant(merchant.id, query);
}
@@ -43,7 +43,7 @@ export class MerchantOrderController {
@CurrentSeller('sub') sellerId: number,
@Param('orderNo') orderNo: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
const order = await this.orderService.findOne(orderNo);
if (order.merchantId !== merchant.id) {
@@ -58,7 +58,7 @@ export class MerchantOrderController {
@CurrentSeller('sub') sellerId: number,
@Body('orderNo') orderNo: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
const order = await this.orderService.findOne(orderNo);
if (order.merchantId !== merchant.id) {
@@ -73,7 +73,7 @@ export class MerchantOrderController {
@CurrentSeller('sub') sellerId: number,
@Param('orderNo') orderNo: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.confirm(merchant.id, orderNo);
}
@@ -85,7 +85,7 @@ export class MerchantOrderController {
@Param('orderNo') orderNo: string,
@Body('reason') reason: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.reject(merchant.id, orderNo, reason);
}
@@ -96,7 +96,7 @@ export class MerchantOrderController {
@CurrentSeller('sub') sellerId: number,
@Param('orderNo') orderNo: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.checkin(merchant.id, orderNo);
}
@@ -107,7 +107,7 @@ export class MerchantOrderController {
@CurrentSeller('sub') sellerId: number,
@Param('orderNo') orderNo: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.checkout(merchant.id, orderNo);
}
@@ -5,14 +5,14 @@ import { MerchantOrderService } from './order.service';
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 { MerchantModule } from '../merchant.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(() => MerchantModule),
forwardRef(() => UserActivityModule),
forwardRef(() => FinanceModule),
],
@@ -9,7 +9,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MerchantReviewService } from './review.service';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantProfileService } from '../profile/profile.service';
import { MerchantService } from '../merchant.service';
@ApiTags('评价管理(商家)')
@Controller('merchant/reviews')
@@ -18,7 +18,7 @@ import { MerchantProfileService } from '../profile/profile.service';
export class MerchantReviewController {
constructor(
private readonly reviewService: MerchantReviewService,
private readonly profileService: MerchantProfileService,
private readonly merchantService: MerchantService,
) {}
@Get()
@@ -29,7 +29,7 @@ export class MerchantReviewController {
@Query('limit') limit: string = '10',
@Query('roomId') roomId?: string,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.reviewService.getSellerReviews(
@@ -1,14 +1,14 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MerchantReviewController } from './review.controller';
import { MerchantReviewService } from './review.service';
import { Review } from '@/entities/review.entity';
import { MerchantProfileModule } from '../profile/profile.module';
import { MerchantModule } from '../merchant.module';
@Module({
imports: [
TypeOrmModule.forFeature([Review]),
MerchantProfileModule,
forwardRef(() => MerchantModule),
],
controllers: [MerchantReviewController],
providers: [MerchantReviewService],
@@ -14,7 +14,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MerchantRoomService } from './room.service';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantProfileService } from '../profile/profile.service';
import { MerchantService } from '../merchant.service';
import { CreateRoomDto, UpdateRoomDto, QueryRoomDto } from './dto/room.dto';
@ApiTags('房源管理(商家)')
@@ -24,7 +24,7 @@ import { CreateRoomDto, UpdateRoomDto, QueryRoomDto } from './dto/room.dto';
export class MerchantRoomController {
constructor(
private readonly roomService: MerchantRoomService,
private readonly profileService: MerchantProfileService,
private readonly merchantService: MerchantService,
) {}
@Get()
@@ -33,7 +33,7 @@ export class MerchantRoomController {
@CurrentSeller('sub') sellerId: number,
@Query() query: QueryRoomDto,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.findByMerchant(Number(merchant.id), query);
}
@@ -44,7 +44,7 @@ export class MerchantRoomController {
@CurrentSeller('sub') sellerId: number,
@Param('id') id: number,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.findByIdAndMerchant(
Number(id),
@@ -58,7 +58,7 @@ export class MerchantRoomController {
@CurrentSeller('sub') sellerId: number,
@Body() dto: CreateRoomDto,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.create(Number(merchant.id), dto);
}
@@ -70,7 +70,7 @@ export class MerchantRoomController {
@Param('id') id: number,
@Body() dto: UpdateRoomDto,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.update(Number(id), Number(merchant.id), dto);
}
@@ -81,7 +81,7 @@ export class MerchantRoomController {
@CurrentSeller('sub') sellerId: number,
@Param('id') id: number,
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.roomService.remove(Number(id), Number(merchant.id));
}
@@ -1,15 +1,15 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MerchantRoomController } from './room.controller';
import { MerchantRoomService } from './room.service';
import { Room } from '@/entities/room.entity';
import { RoomCalendar } from '@/entities/room-calendar.entity';
import { MerchantProfileModule } from '../profile/profile.module';
import { MerchantModule } from '../merchant.module';
@Module({
imports: [
TypeOrmModule.forFeature([Room, RoomCalendar]),
MerchantProfileModule,
forwardRef(() => MerchantModule),
],
controllers: [MerchantRoomController],
providers: [MerchantRoomService],
@@ -1,31 +0,0 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard';
import { RolesGuard } from '@/common/guards/roles.guard';
import { Roles } from '@/common/decorators/roles.decorator';
import { CurrentUser } from '@/common/decorators/current-user.decorator';
import { StatisticsService } from './statistics.service';
@ApiTags('商家统计')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('merchant')
@Controller('merchant/statistics')
export class StatisticsSellerController {
constructor(private readonly statisticsService: StatisticsService) {}
@Get('overview')
@ApiOperation({ summary: '获取数据概览' })
async getOverview(@CurrentUser() user: any) {
return this.statisticsService.getOverview(user.merchantId);
}
@Get('income-trend')
@ApiOperation({ summary: '获取收入趋势' })
async getIncomeTrend(
@CurrentUser() user: any,
@Query('type') type: 'day' | 'week' | 'month' = 'day',
) {
return this.statisticsService.getIncomeTrend(user.merchantId, type);
}
}
@@ -3,7 +3,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantStatisticsService } from './statistics.service';
import { MerchantProfileService } from '../profile/profile.service';
import { MerchantService } from '../merchant.service';
@ApiTags('商家统计')
@ApiBearerAuth()
@@ -12,13 +12,13 @@ import { MerchantProfileService } from '../profile/profile.service';
export class MerchantStatisticsController {
constructor(
private readonly statisticsService: MerchantStatisticsService,
private readonly profileService: MerchantProfileService,
private readonly merchantService: MerchantService,
) {}
@Get('overview')
@ApiOperation({ summary: '获取数据概览' })
async getOverview(@CurrentSeller('sub') sellerId: number) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.statisticsService.getOverview(merchant.id);
}
@@ -29,7 +29,7 @@ export class MerchantStatisticsController {
@CurrentSeller('sub') sellerId: number,
@Query('type') type: 'day' | 'week' | 'month' = 'day',
) {
const merchant = await this.profileService.findBySellerId(sellerId);
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.statisticsService.getIncomeTrend(merchant.id, type);
}
@@ -1,14 +1,14 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MerchantStatisticsController } from './statistics.controller';
import { MerchantStatisticsService } from './statistics.service';
import { Order } from '@/entities/order.entity';
import { MerchantProfileModule } from '../profile/profile.module';
import { MerchantModule } from '../merchant.module';
@Module({
imports: [
TypeOrmModule.forFeature([Order]),
MerchantProfileModule,
forwardRef(() => MerchantModule),
],
controllers: [MerchantStatisticsController],
providers: [MerchantStatisticsService],
@@ -318,6 +318,9 @@ export class AccountService {
businessNo: string,
remark: string,
): Promise<void> {
// 先确保商家账户存在(如果不存在会自动创建)
await this.getMerchantAccount(merchantId);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
@@ -48,7 +48,13 @@ export class SettlementService {
if (existingSettlements > 0) {
this.logger.warn(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,跳过`);
throw new BadRequestException(`该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过,无法重复结算`);
return {
successCount: 0,
failCount: 0,
totalOrders: 0,
skipped: true,
reason: `该周期 ${lastWeekStart} ~ ${lastWeekEnd} 已经结算过`
};
}
// 查询所有已完成且截止到上周末的订单
@@ -68,7 +74,13 @@ export class SettlementService {
if (allOrders.length === 0) {
this.logger.log('没有需要结算的订单');
throw new BadRequestException('该周期内没有需要结算的订单');
return {
successCount: 0,
failCount: 0,
totalOrders: 0,
skipped: true,
reason: '该周期内没有需要结算的订单'
};
}
// 批量查询已结算的订单ID
@@ -109,11 +121,18 @@ export class SettlementService {
if (Object.keys(ordersByMerchant).length === 0) {
this.logger.log('没有需要结算的订单(所有订单都已结算)');
throw new BadRequestException('没有需要结算的订单');
return {
successCount: 0,
failCount: 0,
totalOrders: 0,
skipped: true,
reason: '没有需要结算的订单(所有订单都已结算)'
};
}
let successCount = 0;
let failCount = 0;
const errors: Array<{ merchantId: number; error: string }> = [];
for (const [merchantIdStr, merchantOrders] of Object.entries(ordersByMerchant)) {
const merchantId = Number(merchantIdStr);
@@ -123,13 +142,26 @@ export class SettlementService {
this.logger.log(`商家 ${merchantId} 结算完成,订单数:${merchantOrders.length}`);
successCount++;
} catch (error) {
this.logger.error(`商家 ${merchantId} 结算失败:${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : '';
this.logger.error(`商家 ${merchantId} 结算失败:${errorMessage}`);
this.logger.error(`错误堆栈:${errorStack}`);
failCount++;
errors.push({
merchantId,
error: errorMessage
});
}
}
this.logger.log(`周结算任务执行完成,成功:${successCount},失败:${failCount}`);
return { successCount, failCount, totalOrders: allOrders.length - skippedCount };
return {
successCount,
failCount,
totalOrders: allOrders.length - skippedCount,
skipped: false,
errors: errors.length > 0 ? errors : undefined
};
} catch (error) {
this.logger.error(`周结算任务执行失败:${error.message}`);
throw error;
@@ -6,6 +6,7 @@ import { MerchantWithdrawal } from '@/entities/merchant-withdrawal.entity';
import { PlatformWithdrawal } from '@/entities/platform-withdrawal.entity';
import { MerchantAccount } from '@/entities/merchant-account.entity';
import { PlatformAccount } from '@/entities/platform-account.entity';
import { MerchantTransaction } from '@/entities/merchant-transaction.entity';
import { AccountService } from './account.service';
import { TransactionService } from './transaction.service';
@@ -18,6 +19,8 @@ export class WithdrawalService {
private merchantWithdrawalRepo: Repository<MerchantWithdrawal>,
@InjectRepository(PlatformWithdrawal)
private platformWithdrawalRepo: Repository<PlatformWithdrawal>,
@InjectRepository(MerchantTransaction)
private merchantTransactionRepo: Repository<MerchantTransaction>,
private accountService: AccountService,
private transactionService: TransactionService,
private dataSource: DataSource,
@@ -423,22 +426,51 @@ export class WithdrawalService {
await queryRunner.startTransaction();
try {
const transactionNo = this.transactionService.generateTransactionNo();
// 扣减商家账户冻结金额
const account = await queryRunner.manager.findOne(MerchantAccount, {
where: { merchant_id: withdrawal.merchantId },
lock: { mode: 'pessimistic_write' }
});
// 扣减商家账户余额
await this.accountService.deductMerchantBalance(
withdrawal.merchantId,
Number(withdrawal.actualAmount),
transactionNo,
'withdraw',
withdrawal.id,
withdrawal.withdrawNo,
`商家提现 - ${withdrawal.bankName}`
);
if (!account) {
throw new NotFoundException('商家账户不存在');
}
const frozenBefore = Number(account.frozen_balance);
const amount = Number(withdrawal.actualAmount);
if (frozenBefore < amount) {
throw new BadRequestException('冻结金额不足');
}
account.frozen_balance = frozenBefore - amount;
account.total_withdraw = Number(account.total_withdraw) + amount;
account.total_expense = Number(account.total_expense) + amount;
account.version += 1;
await queryRunner.manager.save(account);
// 记录交易流水
const transactionNo = this.transactionService.generateTransactionNo();
const transaction = this.merchantTransactionRepo.create({
transaction_no: transactionNo,
merchant_id: withdrawal.merchantId,
account_id: account.id,
direction: 'expense',
amount,
balance_before: Number(account.balance),
balance_after: Number(account.balance),
transaction_type: '提现',
business_type: 'withdraw',
business_id: withdrawal.id,
business_no: withdrawal.withdrawNo,
remark: `商家提现 - ${withdrawal.bankName}`
});
await queryRunner.manager.save(transaction);
// 记录系统总账户提现
await this.accountService.addSystemWithdrawal(
Number(withdrawal.actualAmount),
amount,
transactionNo,
'merchant_withdraw',
withdrawal.id,
@@ -611,7 +643,7 @@ export class WithdrawalService {
const [list, total] = await queryBuilder.getManyAndCount();
return {
list,
items: list,
total,
page,
pageSize,
@@ -648,7 +680,7 @@ export class WithdrawalService {
const [list, total] = await queryBuilder.getManyAndCount();
return {
list,
items: list,
total,
page,
pageSize,
@@ -680,7 +712,7 @@ export class WithdrawalService {
const [list, total] = await queryBuilder.getManyAndCount();
return {
list,
items: list,
total,
page,
pageSize,