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
@@ -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,