fix
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/schedule": "^6.1.3",
|
||||
"@nestjs/swagger": "^11.0.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import databaseConfig from './config/database.config';
|
||||
import jwtConfig from './config/jwt.config';
|
||||
import redisConfig from './config/redis.config';
|
||||
@@ -12,9 +13,18 @@ import { RoomModule } from './modules/room/room.module';
|
||||
import { RoomCalendarModule } from './modules/room-calendar/room-calendar.module';
|
||||
import { OrderModule } from './modules/order/order.module';
|
||||
import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
|
||||
import { ReviewModule } from './modules/review/review.module';
|
||||
import { FinanceModule } from './modules/finance/finance.module';
|
||||
import { ActivityModule } from './modules/activity/activity.module';
|
||||
import { ScheduleModule as TaskScheduleModule } from './schedule/schedule.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ScheduleModule.forRoot(),
|
||||
TaskScheduleModule,
|
||||
FinanceModule,
|
||||
ReviewModule,
|
||||
ActivityModule,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [databaseConfig, jwtConfig, redisConfig],
|
||||
|
||||
@@ -62,6 +62,18 @@ export class Merchant {
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '保证金' })
|
||||
deposit: number;
|
||||
|
||||
@Column({ name: 'wallet_balance', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '待提现余额' })
|
||||
walletBalance: number;
|
||||
|
||||
@Column({ name: 'bank_name', length: 100, default: '', comment: '开户银行' })
|
||||
bankName: string;
|
||||
|
||||
@Column({ name: 'bank_account', length: 50, default: '', comment: '银行账号' })
|
||||
bankAccount: string;
|
||||
|
||||
@Column({ name: 'account_name', length: 50, default: '', comment: '账户名' })
|
||||
accountName: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'decimal', precision: 2, scale: 1, unsigned: true, default: 5.0, comment: '评分' })
|
||||
rating: number;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
|
||||
export type ActivityType = 'invite_cashback';
|
||||
|
||||
@Entity('mkt_activities')
|
||||
export class MktActivity {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Column({ length: 100, comment: '活动名称' })
|
||||
name: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['invite_cashback'],
|
||||
default: 'invite_cashback',
|
||||
comment: '活动类型',
|
||||
})
|
||||
type: ActivityType;
|
||||
|
||||
@Column({ type: 'boolean', default: true, comment: '是否启用' })
|
||||
enabled: boolean;
|
||||
|
||||
@Column({ type: 'json', comment: '活动配置' })
|
||||
config: {
|
||||
firstOrderRate: number;
|
||||
secondOrderRate: number;
|
||||
minCashback: number;
|
||||
maxCashback: number;
|
||||
withdrawThreshold: number;
|
||||
};
|
||||
|
||||
@Column({ name: 'start_time', type: 'datetime', nullable: true, comment: '活动开始时间' })
|
||||
startTime: Date;
|
||||
|
||||
@Column({ name: 'end_time', type: 'datetime', nullable: true, comment: '活动结束时间' })
|
||||
endTime: Date;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { Order } from './order.entity';
|
||||
import { MktActivity } from './mkt-activity.entity';
|
||||
|
||||
export type CashbackStatus = 'pending' | 'settled' | 'cancelled';
|
||||
|
||||
@Entity('mkt_cashbacks')
|
||||
export class MktCashback {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'activity_id', type: 'bigint', unsigned: true, comment: '活动ID' })
|
||||
activityId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'inviter_id', type: 'bigint', unsigned: true, comment: '邀请人用户ID' })
|
||||
inviterId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'invitee_id', type: 'bigint', unsigned: true, comment: '被邀请人用户ID' })
|
||||
inviteeId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'order_id', type: 'bigint', unsigned: true, comment: '关联订单ID' })
|
||||
orderId: number;
|
||||
|
||||
@Column({ name: 'order_no', length: 32, comment: '订单号' })
|
||||
orderNo: string;
|
||||
|
||||
@Column({ name: 'order_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '订单金额' })
|
||||
orderAmount: number;
|
||||
|
||||
@Column({ name: 'order_index', type: 'tinyint', unsigned: true, comment: '被邀请人第几单(1/2)' })
|
||||
orderIndex: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 5, scale: 4, unsigned: true, comment: '返现比例' })
|
||||
rate: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '返现金额' })
|
||||
amount: number;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['pending', 'settled', 'cancelled'],
|
||||
default: 'pending',
|
||||
comment: '状态',
|
||||
})
|
||||
status: CashbackStatus;
|
||||
|
||||
@Column({ name: 'settled_at', type: 'datetime', nullable: true, comment: '到账时间' })
|
||||
settledAt: Date;
|
||||
|
||||
@ManyToOne(() => MktActivity)
|
||||
@JoinColumn({ name: 'activity_id' })
|
||||
activity: MktActivity;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'inviter_id' })
|
||||
inviter: User;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'invitee_id' })
|
||||
invitee: User;
|
||||
|
||||
@ManyToOne(() => Order)
|
||||
@JoinColumn({ name: 'order_id' })
|
||||
order: Order;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { MktActivity } from './mkt-activity.entity';
|
||||
|
||||
@Entity('mkt_invitations')
|
||||
export class MktInvitation {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'activity_id', type: 'bigint', unsigned: true, comment: '活动ID' })
|
||||
activityId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'inviter_id', type: 'bigint', unsigned: true, comment: '邀请人用户ID' })
|
||||
inviterId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'invitee_id', type: 'bigint', unsigned: true, unique: true, comment: '被邀请人用户ID' })
|
||||
inviteeId: number;
|
||||
|
||||
@Column({ name: 'invite_code', length: 32, comment: '邀请码' })
|
||||
inviteCode: string;
|
||||
|
||||
@Column({ length: 100, nullable: true, comment: '小程序scene参数' })
|
||||
scene: string;
|
||||
|
||||
@ManyToOne(() => MktActivity)
|
||||
@JoinColumn({ name: 'activity_id' })
|
||||
activity: MktActivity;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'inviter_id' })
|
||||
inviter: User;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'invitee_id' })
|
||||
invitee: User;
|
||||
|
||||
@CreateDateColumn({ comment: '邀请时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { MktActivity } from './mkt-activity.entity';
|
||||
|
||||
export type InviteWithdrawalStatus = 'pending' | 'approved' | 'rejected' | 'paid';
|
||||
|
||||
@Entity('mkt_invite_withdrawals')
|
||||
export class MktInviteWithdrawal {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'activity_id', type: 'bigint', unsigned: true, comment: '活动ID' })
|
||||
activityId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'user_id', type: 'bigint', unsigned: true, comment: '用户ID' })
|
||||
userId: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '提现金额' })
|
||||
amount: number;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['pending', 'approved', 'rejected', 'paid'],
|
||||
default: 'pending',
|
||||
comment: '状态',
|
||||
})
|
||||
status: InviteWithdrawalStatus;
|
||||
|
||||
@Column({ name: 'reject_reason', length: 500, nullable: true, comment: '拒绝原因' })
|
||||
rejectReason: string;
|
||||
|
||||
@Column({ name: 'reviewer_id', type: 'bigint', unsigned: true, nullable: true, comment: '审核人ID' })
|
||||
reviewerId: number;
|
||||
|
||||
@Column({ name: 'reviewed_at', type: 'datetime', nullable: true, comment: '审核时间' })
|
||||
reviewedAt: Date;
|
||||
|
||||
@Column({ name: 'paid_at', type: 'datetime', nullable: true, comment: '打款时间' })
|
||||
paidAt: Date;
|
||||
|
||||
@ManyToOne(() => MktActivity)
|
||||
@JoinColumn({ name: 'activity_id' })
|
||||
activity: MktActivity;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@CreateDateColumn({ comment: '申请时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { MktActivity } from './mkt-activity.entity';
|
||||
|
||||
@Entity('mkt_user_invite_stats')
|
||||
export class MktUserInviteStats {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'activity_id', type: 'bigint', unsigned: true, comment: '活动ID' })
|
||||
activityId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'user_id', type: 'bigint', unsigned: true, unique: true, comment: '用户ID' })
|
||||
userId: number;
|
||||
|
||||
@Column({ name: 'invite_code', length: 32, unique: true, comment: '专属邀请码' })
|
||||
inviteCode: string;
|
||||
|
||||
@Column({ name: 'total_invites', type: 'int', unsigned: true, default: 0, comment: '累计邀请人数' })
|
||||
totalInvites: number;
|
||||
|
||||
@Column({ name: 'total_orders', type: 'int', unsigned: true, default: 0, comment: '累计下单人数' })
|
||||
totalOrders: number;
|
||||
|
||||
@Column({ name: 'total_cashback', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '累计返现金额' })
|
||||
totalCashback: number;
|
||||
|
||||
@Column({ name: 'available_balance', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '可提现余额' })
|
||||
availableBalance: number;
|
||||
|
||||
@Column({ name: 'withdrawn_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '已提现金额' })
|
||||
withdrawnAmount: number;
|
||||
|
||||
@ManyToOne(() => MktActivity)
|
||||
@JoinColumn({ name: 'activity_id' })
|
||||
activity: MktActivity;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -44,9 +44,6 @@ export class Order {
|
||||
@Column({ name: 'room_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '房费总额' })
|
||||
roomAmount: number;
|
||||
|
||||
@Column({ name: 'service_fee', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '服务费' })
|
||||
serviceFee: number;
|
||||
|
||||
@Column({ name: 'coupon_discount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '优惠券抵扣' })
|
||||
couponDiscount: number;
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Order } from './order.entity';
|
||||
import { User } from './user.entity';
|
||||
import { Merchant } from './merchant.entity';
|
||||
import { Room } from './room.entity';
|
||||
|
||||
@Entity('reviews')
|
||||
export class Review {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@ManyToOne(() => Order)
|
||||
@JoinColumn({ name: 'order_id' })
|
||||
order: Order;
|
||||
|
||||
@Column({ name: 'order_id', type: 'bigint', unsigned: true, unique: true, comment: '订单ID' })
|
||||
orderId: number;
|
||||
|
||||
@@ -38,8 +46,11 @@ export class Review {
|
||||
@Column({ name: 'is_anonymous', type: 'boolean', default: false, comment: '是否匿名' })
|
||||
isAnonymous: boolean;
|
||||
|
||||
@Column({ type: 'enum', enum: ['visible', 'hidden'], default: 'visible', comment: '状态' })
|
||||
status: 'visible' | 'hidden';
|
||||
@Column({ name: 'audit_status', type: 'enum', enum: ['pending', 'approved', 'rejected'], default: 'pending', comment: '审核状态' })
|
||||
auditStatus: 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@Column({ name: 'audit_reject_reason', type: 'varchar', length: 500, nullable: true, comment: '审核拒绝原因' })
|
||||
auditRejectReason: string;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
|
||||
import { Settlement } from './settlement.entity';
|
||||
|
||||
@Entity('settlement_items')
|
||||
export class SettlementItem {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(() => Settlement, s => s.items)
|
||||
@JoinColumn({ name: 'settlement_id' })
|
||||
settlement: Settlement;
|
||||
|
||||
@Column({ name: 'settlement_id', type: 'bigint', unsigned: true, comment: '对账单ID' })
|
||||
settlementId: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'order_id', type: 'bigint', unsigned: true, comment: '订单ID' })
|
||||
orderId: number;
|
||||
|
||||
@Column({ name: 'order_no', type: 'varchar', length: 32, comment: '订单号' })
|
||||
orderNo: string;
|
||||
|
||||
@Column({ name: 'order_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '订单金额' })
|
||||
orderAmount: number;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||
import { Merchant } from './merchant.entity';
|
||||
import { SettlementItem } from './settlement-item.entity';
|
||||
|
||||
@Entity('settlements')
|
||||
export class Settlement {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(() => Merchant)
|
||||
@JoinColumn({ name: 'merchant_id' })
|
||||
merchant: Merchant;
|
||||
|
||||
@Column({ name: 'merchant_id', type: 'bigint', unsigned: true, comment: '商家ID' })
|
||||
merchantId: number;
|
||||
|
||||
@Column({ name: 'settlement_no', type: 'varchar', length: 32, unique: true, comment: '对账单号' })
|
||||
settlementNo: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'period_start', type: 'date', comment: '周期开始日期(周日)' })
|
||||
periodStart: string;
|
||||
|
||||
@Column({ name: 'period_end', type: 'date', comment: '周期结束日期(周六)' })
|
||||
periodEnd: string;
|
||||
|
||||
@Column({ name: 'order_count', type: 'int', unsigned: true, default: 0, comment: '订单数量' })
|
||||
orderCount: number;
|
||||
|
||||
@Column({ name: 'order_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '订单总金额' })
|
||||
orderAmount: number;
|
||||
|
||||
@Column({ name: 'commission_rate', type: 'decimal', precision: 5, scale: 4, unsigned: true, default: 0, comment: '佣金比例' })
|
||||
commissionRate: number;
|
||||
|
||||
@Column({ name: 'commission_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '佣金金额' })
|
||||
commissionAmount: number;
|
||||
|
||||
@Column({ name: 'settlement_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '结算金额(订单金额-佣金)' })
|
||||
settlementAmount: number;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'enum', enum: ['pending', 'approved', 'rejected'], default: 'pending', comment: '状态' })
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@Column({ name: 'reject_reason', type: 'varchar', length: 500, nullable: true, comment: '拒绝原因' })
|
||||
rejectReason: string;
|
||||
|
||||
@Column({ name: 'reviewer_id', type: 'bigint', unsigned: true, nullable: true, comment: '审核人ID' })
|
||||
reviewerId: number;
|
||||
|
||||
@Column({ name: 'reviewed_at', type: 'datetime', nullable: true, comment: '审核时间' })
|
||||
reviewedAt: Date;
|
||||
|
||||
@OneToMany(() => SettlementItem, item => item.settlement)
|
||||
items: SettlementItem[];
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Merchant } from './merchant.entity';
|
||||
|
||||
@Entity('withdrawals')
|
||||
export class Withdrawal {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@ManyToOne(() => Merchant)
|
||||
@JoinColumn({ name: 'merchant_id' })
|
||||
merchant: Merchant;
|
||||
|
||||
@Column({ name: 'merchant_id', type: 'bigint', unsigned: true, comment: '商家ID' })
|
||||
merchantId: number;
|
||||
|
||||
@Column({ name: 'settlement_ids', type: 'json', nullable: true, comment: '关联对账单ID列表' })
|
||||
settlementIds: number[];
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '提现金额' })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '手续费' })
|
||||
fee: number;
|
||||
|
||||
@Column({ name: 'commission_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '平台佣金' })
|
||||
commissionAmount: number;
|
||||
|
||||
@Column({ name: 'actual_amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, comment: '实际到账金额' })
|
||||
actualAmount: number;
|
||||
|
||||
@Column({ name: 'bank_name', type: 'varchar', length: 100, comment: '开户银行' })
|
||||
bankName: string;
|
||||
|
||||
@Column({ name: 'bank_account', type: 'varchar', length: 50, comment: '银行账号' })
|
||||
bankAccount: string;
|
||||
|
||||
@Column({ name: 'account_name', type: 'varchar', length: 50, comment: '账户名' })
|
||||
accountName: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'enum', enum: ['pending', 'approved', 'rejected', 'paid'], default: 'pending', comment: '状态' })
|
||||
status: 'pending' | 'approved' | 'rejected' | 'paid';
|
||||
|
||||
@Column({ name: 'reviewer_id', type: 'bigint', unsigned: true, nullable: true, comment: '审核人ID' })
|
||||
reviewerId: number;
|
||||
|
||||
@Column({ name: 'reviewed_at', type: 'datetime', nullable: true, comment: '审核时间' })
|
||||
reviewedAt: Date;
|
||||
|
||||
@Column({ name: 'reject_reason', type: 'varchar', length: 500, nullable: true, comment: '拒绝原因' })
|
||||
rejectReason: string;
|
||||
|
||||
@Column({ name: 'paid_at', type: 'datetime', nullable: true, comment: '打款时间' })
|
||||
paidAt: Date;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
Query,
|
||||
Param,
|
||||
UseGuards,
|
||||
Request,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { ActivityService } from './activity.service';
|
||||
import {
|
||||
CreateActivityDto,
|
||||
UpdateActivityDto,
|
||||
QueryActivityDto,
|
||||
BindInvitationDto,
|
||||
CreateInviteWithdrawalDto,
|
||||
QueryInviteRecordsDto,
|
||||
RejectInviteWithdrawalDto,
|
||||
QueryAdminInviteWithdrawalsDto,
|
||||
QueryAdminCashbacksDto,
|
||||
} from './dto/activity.dto';
|
||||
import { JwtAuthGuard, RolesGuard, Roles, CurrentUser } from '@/common';
|
||||
|
||||
// ===== 用户端邀请接口 =====
|
||||
@ApiTags('用户端-邀请返现')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('user/activity/invite')
|
||||
export class UserInviteController {
|
||||
constructor(private readonly activityService: ActivityService) {}
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: '获取我的邀请统计' })
|
||||
async getStats(@CurrentUser('userId') userId: number) {
|
||||
return this.activityService.getUserStats(userId);
|
||||
}
|
||||
|
||||
@Post('bind')
|
||||
@ApiOperation({ summary: '绑定邀请关系' })
|
||||
async bind(@CurrentUser('userId') userId: number, @Body() dto: BindInvitationDto) {
|
||||
return this.activityService.bindInvitation(userId, dto);
|
||||
}
|
||||
|
||||
@Get('records')
|
||||
@ApiOperation({ summary: '邀请记录列表' })
|
||||
async getRecords(@CurrentUser('userId') userId: number, @Query() dto: QueryInviteRecordsDto) {
|
||||
return this.activityService.getInviteRecords(userId, dto);
|
||||
}
|
||||
|
||||
@Get('cashbacks')
|
||||
@ApiOperation({ summary: '返现记录列表' })
|
||||
async getCashbacks(@CurrentUser('userId') userId: number, @Query() dto: QueryInviteRecordsDto) {
|
||||
return this.activityService.getCashbackRecords(userId, dto);
|
||||
}
|
||||
|
||||
@Post('withdraw')
|
||||
@ApiOperation({ summary: '申请提现' })
|
||||
async createWithdrawal(
|
||||
@CurrentUser('userId') userId: number,
|
||||
@Body() dto: CreateInviteWithdrawalDto,
|
||||
) {
|
||||
return this.activityService.createWithdrawal(userId, dto);
|
||||
}
|
||||
|
||||
@Get('withdrawals')
|
||||
@ApiOperation({ summary: '提现记录列表' })
|
||||
async getWithdrawals(
|
||||
@CurrentUser('userId') userId: number,
|
||||
@Query() dto: QueryInviteRecordsDto,
|
||||
) {
|
||||
return this.activityService.getWithdrawalRecords(userId, dto);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 管理端活动接口 =====
|
||||
@ApiTags('管理端-活动管理')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@Controller('admin/activity')
|
||||
export class AdminActivityController {
|
||||
constructor(private readonly activityService: ActivityService) {}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: '活动列表' })
|
||||
async list(@Query() dto: QueryActivityDto) {
|
||||
return this.activityService.getActivities(dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '活动详情' })
|
||||
@ApiParam({ name: 'id', description: '活动ID' })
|
||||
async getById(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.activityService.getActivityById(id);
|
||||
}
|
||||
|
||||
@Post('create')
|
||||
@ApiOperation({ summary: '创建活动' })
|
||||
async create(@Body() dto: CreateActivityDto) {
|
||||
return this.activityService.createActivity(dto);
|
||||
}
|
||||
|
||||
@Put(':id/update')
|
||||
@ApiOperation({ summary: '编辑活动' })
|
||||
@ApiParam({ name: 'id', description: '活动ID' })
|
||||
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateActivityDto) {
|
||||
return this.activityService.updateActivity(id, dto);
|
||||
}
|
||||
|
||||
@Put(':id/toggle')
|
||||
@ApiOperation({ summary: '启用/停用活动' })
|
||||
@ApiParam({ name: 'id', description: '活动ID' })
|
||||
async toggle(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.activityService.toggleActivity(id);
|
||||
}
|
||||
|
||||
@Get('invite/stats')
|
||||
@ApiOperation({ summary: '邀请数据总览' })
|
||||
async getStats() {
|
||||
return this.activityService.getAdminStats();
|
||||
}
|
||||
|
||||
@Get('invite/records')
|
||||
@ApiOperation({ summary: '邀请记录列表' })
|
||||
async getRecords(@Query() dto: QueryInviteRecordsDto) {
|
||||
return this.activityService.getAdminInvitations(dto);
|
||||
}
|
||||
|
||||
@Get('invite/cashbacks')
|
||||
@ApiOperation({ summary: '返现记录列表' })
|
||||
async getCashbacks(@Query() dto: QueryAdminCashbacksDto) {
|
||||
return this.activityService.getAdminCashbacks(dto);
|
||||
}
|
||||
|
||||
@Get('invite/withdrawals')
|
||||
@ApiOperation({ summary: '提现审核列表' })
|
||||
async getWithdrawals(@Query() dto: QueryAdminInviteWithdrawalsDto) {
|
||||
return this.activityService.getAdminWithdrawals(dto);
|
||||
}
|
||||
|
||||
@Put('invite/withdrawals/:id/approve')
|
||||
@ApiOperation({ summary: '审核通过' })
|
||||
@ApiParam({ name: 'id', description: '提现申请ID' })
|
||||
async approve(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser('userId') reviewerId: number,
|
||||
) {
|
||||
return this.activityService.approveWithdrawal(id, reviewerId);
|
||||
}
|
||||
|
||||
@Put('invite/withdrawals/:id/reject')
|
||||
@ApiOperation({ summary: '审核拒绝' })
|
||||
@ApiParam({ name: 'id', description: '提现申请ID' })
|
||||
async reject(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser('userId') reviewerId: number,
|
||||
@Body() dto: RejectInviteWithdrawalDto,
|
||||
) {
|
||||
return this.activityService.rejectWithdrawal(id, reviewerId, dto);
|
||||
}
|
||||
|
||||
@Put('invite/withdrawals/:id/pay')
|
||||
@ApiOperation({ summary: '确认打款' })
|
||||
@ApiParam({ name: 'id', description: '提现申请ID' })
|
||||
async pay(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.activityService.payWithdrawal(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ActivityService } from './activity.service';
|
||||
import { UserInviteController, AdminActivityController } from './activity.controller';
|
||||
import { MktActivity } from '@/entities/mkt-activity.entity';
|
||||
import { MktInvitation } from '@/entities/mkt-invitation.entity';
|
||||
import { MktCashback } from '@/entities/mkt-cashback.entity';
|
||||
import { MktUserInviteStats } from '@/entities/mkt-user-invite-stats.entity';
|
||||
import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { User } from '@/entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
MktActivity,
|
||||
MktInvitation,
|
||||
MktCashback,
|
||||
MktUserInviteStats,
|
||||
MktInviteWithdrawal,
|
||||
Order,
|
||||
User,
|
||||
]),
|
||||
],
|
||||
controllers: [UserInviteController, AdminActivityController],
|
||||
providers: [ActivityService],
|
||||
exports: [ActivityService],
|
||||
})
|
||||
export class ActivityModule {}
|
||||
@@ -0,0 +1,577 @@
|
||||
import { Injectable, BadRequestException, NotFoundException, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { MktActivity } from '@/entities/mkt-activity.entity';
|
||||
import { MktInvitation } from '@/entities/mkt-invitation.entity';
|
||||
import { MktCashback } from '@/entities/mkt-cashback.entity';
|
||||
import { MktUserInviteStats } from '@/entities/mkt-user-invite-stats.entity';
|
||||
import { MktInviteWithdrawal } from '@/entities/mkt-invite-withdrawal.entity';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { User } from '@/entities/user.entity';
|
||||
import {
|
||||
CreateActivityDto,
|
||||
UpdateActivityDto,
|
||||
QueryActivityDto,
|
||||
BindInvitationDto,
|
||||
CreateInviteWithdrawalDto,
|
||||
QueryInviteRecordsDto,
|
||||
RejectInviteWithdrawalDto,
|
||||
QueryAdminInviteWithdrawalsDto,
|
||||
QueryAdminCashbacksDto,
|
||||
} from './dto/activity.dto';
|
||||
|
||||
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
function encodeBase62(num: number): string {
|
||||
if (num === 0) return '0';
|
||||
let result = '';
|
||||
while (num > 0) {
|
||||
result = ALPHABET[num % 62] + result;
|
||||
num = Math.floor(num / 62);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeBase62(str: string): number {
|
||||
let result = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const idx = ALPHABET.indexOf(str[i]);
|
||||
if (idx === -1) return 0;
|
||||
result = result * 62 + idx;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ActivityService {
|
||||
private readonly logger = new Logger(ActivityService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(MktActivity)
|
||||
private activityRepo: Repository<MktActivity>,
|
||||
@InjectRepository(MktInvitation)
|
||||
private invitationRepo: Repository<MktInvitation>,
|
||||
@InjectRepository(MktCashback)
|
||||
private cashbackRepo: Repository<MktCashback>,
|
||||
@InjectRepository(MktUserInviteStats)
|
||||
private statsRepo: Repository<MktUserInviteStats>,
|
||||
@InjectRepository(MktInviteWithdrawal)
|
||||
private withdrawalRepo: Repository<MktInviteWithdrawal>,
|
||||
@InjectRepository(Order)
|
||||
private orderRepo: Repository<Order>,
|
||||
@InjectRepository(User)
|
||||
private userRepo: Repository<User>,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
// ===== 活动管理 =====
|
||||
|
||||
async createActivity(dto: CreateActivityDto) {
|
||||
// 固定活动名称
|
||||
const nameMap: Record<string, string> = { invite_cashback: '邀请返现' };
|
||||
const activity = this.activityRepo.create({
|
||||
...dto,
|
||||
name: nameMap[dto.type] || dto.name,
|
||||
});
|
||||
return this.activityRepo.save(activity);
|
||||
}
|
||||
|
||||
async updateActivity(id: number, dto: UpdateActivityDto) {
|
||||
const activity = await this.activityRepo.findOne({ where: { id } });
|
||||
if (!activity) throw new NotFoundException('活动不存在');
|
||||
Object.assign(activity, dto);
|
||||
return this.activityRepo.save(activity);
|
||||
}
|
||||
|
||||
async toggleActivity(id: number) {
|
||||
const activity = await this.activityRepo.findOne({ where: { id } });
|
||||
if (!activity) throw new NotFoundException('活动不存在');
|
||||
activity.enabled = !activity.enabled;
|
||||
return this.activityRepo.save(activity);
|
||||
}
|
||||
|
||||
async getActivities(dto: QueryActivityDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 10;
|
||||
const where: any = {};
|
||||
if (dto.type) where.type = dto.type;
|
||||
if (dto.enabled !== undefined) where.enabled = dto.enabled;
|
||||
|
||||
const [list, total] = await this.activityRepo.findAndCount({
|
||||
where,
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return { list, total, page, pageSize };
|
||||
}
|
||||
|
||||
async getActivityById(id: number) {
|
||||
const activity = await this.activityRepo.findOne({ where: { id } });
|
||||
if (!activity) throw new NotFoundException('活动不存在');
|
||||
return activity;
|
||||
}
|
||||
|
||||
async getEnabledInviteCashbackActivity(): Promise<MktActivity | null> {
|
||||
return this.activityRepo.findOne({
|
||||
where: { type: 'invite_cashback', enabled: true },
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 用户端邀请功能 =====
|
||||
|
||||
async getUserStats(userId: number) {
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) {
|
||||
return {
|
||||
inviteCode: '',
|
||||
totalInvites: 0,
|
||||
totalOrders: 0,
|
||||
totalCashback: 0,
|
||||
availableBalance: 0,
|
||||
withdrawnAmount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let stats = await this.statsRepo.findOne({
|
||||
where: { activityId: activity.id, userId },
|
||||
});
|
||||
|
||||
if (!stats) {
|
||||
const inviteCode = encodeBase62(userId);
|
||||
stats = this.statsRepo.create({
|
||||
activityId: activity.id,
|
||||
userId,
|
||||
inviteCode,
|
||||
totalInvites: 0,
|
||||
totalOrders: 0,
|
||||
totalCashback: 0,
|
||||
availableBalance: 0,
|
||||
withdrawnAmount: 0,
|
||||
});
|
||||
await this.statsRepo.save(stats);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
async bindInvitation(userId: number, dto: BindInvitationDto) {
|
||||
// 检查是否已被邀请
|
||||
const existing = await this.invitationRepo.findOne({ where: { inviteeId: userId } });
|
||||
if (existing) {
|
||||
return { success: false, message: '您已有邀请人' };
|
||||
}
|
||||
|
||||
// 解析邀请码
|
||||
const inviterId = decodeBase62(dto.inviteCode);
|
||||
if (!inviterId || inviterId === userId) {
|
||||
return { success: false, message: '邀请码无效' };
|
||||
}
|
||||
|
||||
// 检查邀请人是否存在
|
||||
const inviter = await this.userRepo.findOne({ where: { id: inviterId } });
|
||||
if (!inviter) {
|
||||
return { success: false, message: '邀请人不存在' };
|
||||
}
|
||||
|
||||
// 获取活动
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) {
|
||||
return { success: false, message: '活动未开启' };
|
||||
}
|
||||
|
||||
// 创建邀请关系
|
||||
const invitation = this.invitationRepo.create({
|
||||
activityId: activity.id,
|
||||
inviterId,
|
||||
inviteeId: userId,
|
||||
inviteCode: dto.inviteCode,
|
||||
});
|
||||
await this.invitationRepo.save(invitation);
|
||||
|
||||
// 更新邀请人统计
|
||||
await this.statsRepo.increment(
|
||||
{ activityId: activity.id, userId: inviterId },
|
||||
'totalInvites',
|
||||
1,
|
||||
);
|
||||
|
||||
// 确保邀请人有统计记录
|
||||
let inviterStats = await this.statsRepo.findOne({
|
||||
where: { activityId: activity.id, userId: inviterId },
|
||||
});
|
||||
if (!inviterStats) {
|
||||
inviterStats = this.statsRepo.create({
|
||||
activityId: activity.id,
|
||||
userId: inviterId,
|
||||
inviteCode: dto.inviteCode,
|
||||
totalInvites: 1,
|
||||
totalOrders: 0,
|
||||
totalCashback: 0,
|
||||
availableBalance: 0,
|
||||
withdrawnAmount: 0,
|
||||
});
|
||||
await this.statsRepo.save(inviterStats);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getInviteRecords(userId: number, dto: QueryInviteRecordsDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 20;
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) return { list: [], total: 0 };
|
||||
|
||||
const [list, total] = await this.invitationRepo.findAndCount({
|
||||
where: { activityId: activity.id, inviterId: userId },
|
||||
relations: ['invitee'],
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
// 获取每个被邀请人的订单数
|
||||
const inviteeIds = list.map((item) => item.inviteeId);
|
||||
const orderCounts = await this.orderRepo
|
||||
.createQueryBuilder('order')
|
||||
.select('order.userId', 'userId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('order.userId IN (:...ids)', { ids: inviteeIds })
|
||||
.andWhere('order.status = :status', { status: 'completed' })
|
||||
.groupBy('order.userId')
|
||||
.getRawMany();
|
||||
|
||||
const orderCountMap = new Map(orderCounts.map((item) => [item.userId, parseInt(item.count)]));
|
||||
|
||||
const records = list.map((item) => ({
|
||||
id: item.id,
|
||||
inviteeId: item.inviteeId,
|
||||
inviteeNickname: item.invitee?.nickname || '用户' + item.inviteeId,
|
||||
inviteeAvatar: item.invitee?.avatar || '',
|
||||
createdAt: item.createdAt,
|
||||
orderCount: orderCountMap.get(item.inviteeId) || 0,
|
||||
}));
|
||||
|
||||
return { list: records, total };
|
||||
}
|
||||
|
||||
async getCashbackRecords(userId: number, dto: QueryInviteRecordsDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 20;
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) return { list: [], total: 0 };
|
||||
|
||||
const [list, total] = await this.cashbackRepo.findAndCount({
|
||||
where: { activityId: activity.id, inviterId: userId },
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
async createWithdrawal(userId: number, dto: CreateInviteWithdrawalDto) {
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) throw new BadRequestException('活动未开启');
|
||||
|
||||
const stats = await this.statsRepo.findOne({
|
||||
where: { activityId: activity.id, userId },
|
||||
});
|
||||
if (!stats || stats.availableBalance < dto.amount) {
|
||||
throw new BadRequestException('余额不足');
|
||||
}
|
||||
|
||||
const threshold = activity.config.withdrawThreshold ?? 10;
|
||||
if (dto.amount < threshold) {
|
||||
throw new BadRequestException(`最低提现金额为${threshold}元`);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 创建提现申请
|
||||
const withdrawal = queryRunner.manager.create(MktInviteWithdrawal, {
|
||||
activityId: activity.id,
|
||||
userId,
|
||||
amount: dto.amount,
|
||||
status: 'pending',
|
||||
});
|
||||
await queryRunner.manager.save(withdrawal);
|
||||
|
||||
// 扣减余额
|
||||
stats.availableBalance = Number(stats.availableBalance) - dto.amount;
|
||||
await queryRunner.manager.save(stats);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return withdrawal;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getWithdrawalRecords(userId: number, dto: QueryInviteRecordsDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 20;
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) return { list: [], total: 0 };
|
||||
|
||||
const [list, total] = await this.withdrawalRepo.findAndCount({
|
||||
where: { activityId: activity.id, userId },
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
// ===== 订单完成时触发返现 =====
|
||||
|
||||
async handleOrderCompleted(orderId: number) {
|
||||
const order = await this.orderRepo.findOne({
|
||||
where: { id: orderId },
|
||||
relations: ['user'],
|
||||
});
|
||||
if (!order) return;
|
||||
|
||||
const activity = await this.getEnabledInviteCashbackActivity();
|
||||
if (!activity) return;
|
||||
|
||||
// 查找邀请关系
|
||||
const invitation = await this.invitationRepo.findOne({
|
||||
where: { inviteeId: order.userId },
|
||||
});
|
||||
if (!invitation) return;
|
||||
|
||||
// 检查是否已有返现记录
|
||||
const existing = await this.cashbackRepo.findOne({
|
||||
where: { orderId, inviterId: invitation.inviterId },
|
||||
});
|
||||
if (existing) return;
|
||||
|
||||
// 计算该被邀请人已完成的订单数(不含退款)
|
||||
const completedOrders = await this.orderRepo.count({
|
||||
where: {
|
||||
userId: order.userId,
|
||||
status: 'completed',
|
||||
},
|
||||
});
|
||||
|
||||
// 仅前两单返现
|
||||
if (completedOrders > 2) return;
|
||||
|
||||
const orderIndex = completedOrders;
|
||||
const config = activity.config;
|
||||
const rate = orderIndex === 1 ? config.firstOrderRate : config.secondOrderRate;
|
||||
|
||||
let amount = Number(order.payAmount) * rate;
|
||||
amount = Math.max(config.minCashback, Math.min(config.maxCashback, amount));
|
||||
amount = Math.round(amount * 100) / 100;
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 创建返现记录
|
||||
const cashback = queryRunner.manager.create(MktCashback, {
|
||||
activityId: activity.id,
|
||||
inviterId: invitation.inviterId,
|
||||
inviteeId: order.userId,
|
||||
orderId: order.id,
|
||||
orderNo: order.orderNo,
|
||||
orderAmount: order.payAmount,
|
||||
orderIndex,
|
||||
rate,
|
||||
amount,
|
||||
status: 'settled',
|
||||
settledAt: new Date(),
|
||||
});
|
||||
await queryRunner.manager.save(cashback);
|
||||
|
||||
// 更新邀请人统计
|
||||
await queryRunner.manager.increment(
|
||||
MktUserInviteStats,
|
||||
{ activityId: activity.id, userId: invitation.inviterId },
|
||||
'totalCashback',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.increment(
|
||||
MktUserInviteStats,
|
||||
{ activityId: activity.id, userId: invitation.inviterId },
|
||||
'availableBalance',
|
||||
amount,
|
||||
);
|
||||
|
||||
// 如果是首单,增加下单人数
|
||||
if (orderIndex === 1) {
|
||||
await queryRunner.manager.increment(
|
||||
MktUserInviteStats,
|
||||
{ activityId: activity.id, userId: invitation.inviterId },
|
||||
'totalOrders',
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
`订单 ${order.orderNo} 完成,邀请人 ${invitation.inviterId} 获得返现 ${amount} 元`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(`返现处理失败: ${err?.message}`);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 管理端功能 =====
|
||||
|
||||
async getAdminStats(activityId?: number) {
|
||||
const where: any = {};
|
||||
if (activityId) where.activityId = activityId;
|
||||
|
||||
const [totalInvitations, totalCashbacks, totalWithdrawals, pendingWithdrawals] =
|
||||
await Promise.all([
|
||||
this.invitationRepo.count({ where }),
|
||||
this.cashbackRepo.count({ where }),
|
||||
this.withdrawalRepo.count({ where }),
|
||||
this.withdrawalRepo.count({ where: { ...where, status: 'pending' } }),
|
||||
]);
|
||||
|
||||
const cashbackSum = await this.cashbackRepo
|
||||
.createQueryBuilder('cb')
|
||||
.select('SUM(cb.amount)', 'total')
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
totalInvitations,
|
||||
totalCashbacks,
|
||||
totalWithdrawals,
|
||||
pendingWithdrawals,
|
||||
totalCashbackAmount: cashbackSum?.total || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async getAdminInvitations(dto: QueryInviteRecordsDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 20;
|
||||
|
||||
const [list, total] = await this.invitationRepo.findAndCount({
|
||||
relations: ['inviter', 'invitee'],
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
async getAdminCashbacks(dto: QueryAdminCashbacksDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 20;
|
||||
|
||||
const [list, total] = await this.cashbackRepo.findAndCount({
|
||||
relations: ['inviter', 'invitee', 'order'],
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
async getAdminWithdrawals(dto: QueryAdminInviteWithdrawalsDto) {
|
||||
const page = dto.page ?? 1;
|
||||
const pageSize = dto.pageSize ?? 20;
|
||||
const where: any = {};
|
||||
if (dto.status) where.status = dto.status;
|
||||
|
||||
const [list, total] = await this.withdrawalRepo.findAndCount({
|
||||
where,
|
||||
relations: ['user'],
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return { list, total };
|
||||
}
|
||||
|
||||
async approveWithdrawal(id: number, reviewerId: number) {
|
||||
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
|
||||
if (!withdrawal) throw new NotFoundException('提现申请不存在');
|
||||
if (withdrawal.status !== 'pending') {
|
||||
throw new BadRequestException('当前状态不可审核');
|
||||
}
|
||||
|
||||
withdrawal.status = 'approved';
|
||||
withdrawal.reviewerId = reviewerId;
|
||||
withdrawal.reviewedAt = new Date();
|
||||
return this.withdrawalRepo.save(withdrawal);
|
||||
}
|
||||
|
||||
async rejectWithdrawal(id: number, reviewerId: number, dto: RejectInviteWithdrawalDto) {
|
||||
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
|
||||
if (!withdrawal) throw new NotFoundException('提现申请不存在');
|
||||
if (withdrawal.status !== 'pending') {
|
||||
throw new BadRequestException('当前状态不可审核');
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
withdrawal.status = 'rejected';
|
||||
withdrawal.reviewerId = reviewerId;
|
||||
withdrawal.reviewedAt = new Date();
|
||||
withdrawal.rejectReason = dto.reason;
|
||||
await queryRunner.manager.save(withdrawal);
|
||||
|
||||
// 返还余额
|
||||
await queryRunner.manager.increment(
|
||||
MktUserInviteStats,
|
||||
{ activityId: withdrawal.activityId, userId: withdrawal.userId },
|
||||
'availableBalance',
|
||||
withdrawal.amount,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return withdrawal;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async payWithdrawal(id: number) {
|
||||
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
|
||||
if (!withdrawal) throw new NotFoundException('提现申请不存在');
|
||||
if (withdrawal.status !== 'approved') {
|
||||
throw new BadRequestException('当前状态不可打款');
|
||||
}
|
||||
|
||||
withdrawal.status = 'paid';
|
||||
withdrawal.paidAt = new Date();
|
||||
|
||||
// 更新已提现金额
|
||||
await this.statsRepo.increment(
|
||||
{ activityId: withdrawal.activityId, userId: withdrawal.userId },
|
||||
'withdrawnAmount',
|
||||
withdrawal.amount,
|
||||
);
|
||||
|
||||
return this.withdrawalRepo.save(withdrawal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { IsOptional, IsString, IsBoolean, IsNumber, Min, Max, IsEnum, IsDateString, ValidateNested, IsObject } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class InviteCashbackConfig {
|
||||
@ApiProperty({ description: '首单返现比例' })
|
||||
@IsNumber()
|
||||
firstOrderRate: number;
|
||||
|
||||
@ApiProperty({ description: '二单返现比例' })
|
||||
@IsNumber()
|
||||
secondOrderRate: number;
|
||||
|
||||
@ApiProperty({ description: '最低返现' })
|
||||
@IsNumber()
|
||||
minCashback: number;
|
||||
|
||||
@ApiProperty({ description: '最高返现' })
|
||||
@IsNumber()
|
||||
maxCashback: number;
|
||||
|
||||
@ApiProperty({ description: '提现门槛' })
|
||||
@IsNumber()
|
||||
withdrawThreshold: number;
|
||||
}
|
||||
|
||||
// ===== 活动 DTO =====
|
||||
|
||||
export class CreateActivityDto {
|
||||
@ApiProperty({ description: '活动名称' })
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '活动类型', enum: ['invite_cashback'] })
|
||||
@IsEnum(['invite_cashback'])
|
||||
type: 'invite_cashback';
|
||||
|
||||
@ApiPropertyOptional({ description: '是否启用' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enabled?: boolean;
|
||||
|
||||
@ApiProperty({ description: '活动配置' })
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type(() => InviteCashbackConfig)
|
||||
config: InviteCashbackConfig;
|
||||
|
||||
@ApiPropertyOptional({ description: '活动开始时间' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '活动结束时间' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
export class UpdateActivityDto {
|
||||
@ApiPropertyOptional({ description: '活动名称' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '活动配置' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type(() => InviteCashbackConfig)
|
||||
config?: InviteCashbackConfig;
|
||||
|
||||
@ApiPropertyOptional({ description: '活动开始时间' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '活动结束时间' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
export class QueryActivityDto {
|
||||
@ApiPropertyOptional({ description: '页码' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页条数' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '活动类型' })
|
||||
@IsOptional()
|
||||
@IsEnum(['invite_cashback'])
|
||||
type?: 'invite_cashback';
|
||||
|
||||
@ApiPropertyOptional({ description: '是否启用' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
// ===== 用户端邀请 DTO =====
|
||||
|
||||
export class BindInvitationDto {
|
||||
@ApiProperty({ description: '邀请码' })
|
||||
@IsString()
|
||||
inviteCode: string;
|
||||
}
|
||||
|
||||
export class CreateInviteWithdrawalDto {
|
||||
@ApiProperty({ description: '提现金额' })
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(10)
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class QueryInviteRecordsDto {
|
||||
@ApiPropertyOptional({ description: '页码' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页条数' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// ===== 管理端审核 DTO =====
|
||||
|
||||
export class RejectInviteWithdrawalDto {
|
||||
@ApiProperty({ description: '拒绝原因' })
|
||||
@IsString()
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export class QueryAdminInviteWithdrawalsDto {
|
||||
@ApiPropertyOptional({ description: '页码' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页条数' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态筛选' })
|
||||
@IsOptional()
|
||||
@IsEnum(['pending', 'approved', 'rejected', 'paid'])
|
||||
status?: 'pending' | 'approved' | 'rejected' | 'paid';
|
||||
}
|
||||
|
||||
export class QueryAdminCashbacksDto {
|
||||
@ApiPropertyOptional({ description: '页码' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页条数' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { IsOptional, IsNumber, IsString, IsDateString, Min, IsIn, IsArray } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
// ==================== 商家端 DTO ====================
|
||||
|
||||
// 对账单列表查询
|
||||
export class QuerySettlementDto {
|
||||
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
pageSize?: number = 10;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态筛选', enum: ['pending', 'approved', 'rejected'] })
|
||||
@IsOptional()
|
||||
@IsIn(['pending', 'approved', 'rejected'])
|
||||
status?: 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@ApiPropertyOptional({ description: '周期开始日期' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
periodStart?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '周期结束日期' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
periodEnd?: string;
|
||||
}
|
||||
|
||||
// 提现申请
|
||||
export class CreateWithdrawalDto {
|
||||
@ApiProperty({ description: '提现金额' })
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({ description: '开户银行' })
|
||||
@IsString()
|
||||
bankName: string;
|
||||
|
||||
@ApiProperty({ description: '银行账号' })
|
||||
@IsString()
|
||||
bankAccount: string;
|
||||
|
||||
@ApiProperty({ description: '账户名' })
|
||||
@IsString()
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
// 更新银行卡信息
|
||||
export class UpdateBankInfoDto {
|
||||
@ApiProperty({ description: '开户银行' })
|
||||
@IsString()
|
||||
bankName: string;
|
||||
|
||||
@ApiProperty({ description: '银行账号' })
|
||||
@IsString()
|
||||
bankAccount: string;
|
||||
|
||||
@ApiProperty({ description: '账户名' })
|
||||
@IsString()
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
// 提现记录列表查询
|
||||
export class QueryWithdrawalDto {
|
||||
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
pageSize?: number = 10;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态筛选', enum: ['pending', 'approved', 'rejected', 'paid'] })
|
||||
@IsOptional()
|
||||
@IsIn(['pending', 'approved', 'rejected', 'paid'])
|
||||
status?: 'pending' | 'approved' | 'rejected' | 'paid';
|
||||
}
|
||||
|
||||
// ==================== 平台管理端 DTO ====================
|
||||
|
||||
// 管理端对账单列表查询
|
||||
export class AdminQuerySettlementDto extends QuerySettlementDto {
|
||||
@ApiPropertyOptional({ description: '商家ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
merchantId?: number;
|
||||
}
|
||||
|
||||
// 审核通过对账单
|
||||
export class ApproveSettlementDto {
|
||||
@ApiPropertyOptional({ description: '备注' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 拒绝对账单
|
||||
export class RejectSettlementDto {
|
||||
@ApiProperty({ description: '拒绝原因' })
|
||||
@IsString()
|
||||
rejectReason: string;
|
||||
}
|
||||
|
||||
// 管理端提现列表查询
|
||||
export class AdminQueryWithdrawalDto extends QueryWithdrawalDto {
|
||||
@ApiPropertyOptional({ description: '商家ID' })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
merchantId?: number;
|
||||
}
|
||||
|
||||
// 审核通过提现
|
||||
export class ApproveWithdrawalDto {
|
||||
@ApiPropertyOptional({ description: '备注' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 拒绝提现
|
||||
export class RejectWithdrawalDto {
|
||||
@ApiProperty({ description: '拒绝原因' })
|
||||
@IsString()
|
||||
rejectReason: string;
|
||||
}
|
||||
|
||||
// 确认打款
|
||||
export class PayWithdrawalDto {
|
||||
@ApiPropertyOptional({ description: '打款备注' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 平台收益统计查询
|
||||
export class QueryEarningsDto {
|
||||
@ApiPropertyOptional({ description: '开始日期' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '结束日期' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { FinanceService } from './finance.service';
|
||||
import { SellerJwtAuthGuard } from '@/common/guards/seller-jwt-auth.guard';
|
||||
import { JwtAuthGuard, RolesGuard } from '@/common';
|
||||
import { Roles } from '@/common/decorators/roles.decorator';
|
||||
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
|
||||
import { CurrentUser } from '@/common/decorators/current-user.decorator';
|
||||
import { MerchantService } from '../merchant/merchant.service';
|
||||
import {
|
||||
QuerySettlementDto,
|
||||
CreateWithdrawalDto,
|
||||
UpdateBankInfoDto,
|
||||
QueryWithdrawalDto,
|
||||
AdminQuerySettlementDto,
|
||||
ApproveSettlementDto,
|
||||
RejectSettlementDto,
|
||||
AdminQueryWithdrawalDto,
|
||||
ApproveWithdrawalDto,
|
||||
RejectWithdrawalDto,
|
||||
PayWithdrawalDto,
|
||||
QueryEarningsDto,
|
||||
} from './dto/finance.dto';
|
||||
|
||||
// ==================== 商家端 ====================
|
||||
|
||||
@ApiTags('财务管理(商家)')
|
||||
@Controller('seller/finance')
|
||||
@UseGuards(SellerJwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class MerchantFinanceController {
|
||||
constructor(
|
||||
private readonly financeService: FinanceService,
|
||||
private readonly merchantService: MerchantService,
|
||||
) {}
|
||||
|
||||
private async getMerchantId(sellerId: number): Promise<number> {
|
||||
const merchant = await this.merchantService.findBySellerId(sellerId);
|
||||
if (!merchant) throw new NotFoundException('店铺不存在');
|
||||
return merchant.id;
|
||||
}
|
||||
|
||||
// 钱包信息
|
||||
@Get('wallet')
|
||||
@ApiOperation({ summary: '获取钱包信息' })
|
||||
async getWallet(@CurrentSeller('sub') sellerId: number) {
|
||||
const merchantId = await this.getMerchantId(sellerId);
|
||||
return this.financeService.getWallet(merchantId);
|
||||
}
|
||||
|
||||
// 更新银行卡
|
||||
@Put('bank')
|
||||
@ApiOperation({ summary: '更新银行卡信息' })
|
||||
async updateBankInfo(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Body() dto: UpdateBankInfoDto,
|
||||
) {
|
||||
const merchantId = await this.getMerchantId(sellerId);
|
||||
return this.financeService.updateBankInfo(merchantId, dto);
|
||||
}
|
||||
|
||||
// 申请提现
|
||||
@Post('withdraw')
|
||||
@ApiOperation({ summary: '申请提现' })
|
||||
async createWithdrawal(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Body() dto: CreateWithdrawalDto,
|
||||
) {
|
||||
const merchantId = await this.getMerchantId(sellerId);
|
||||
return this.financeService.createWithdrawal(merchantId, dto);
|
||||
}
|
||||
|
||||
// 对账单列表
|
||||
@Get('settlements')
|
||||
@ApiOperation({ summary: '对账单列表' })
|
||||
async getSettlements(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Query() dto: QuerySettlementDto,
|
||||
) {
|
||||
const merchantId = await this.getMerchantId(sellerId);
|
||||
return this.financeService.getSettlements(merchantId, dto);
|
||||
}
|
||||
|
||||
// 对账单详情
|
||||
@Get('settlements/:id')
|
||||
@ApiOperation({ summary: '对账单详情' })
|
||||
async getSettlementDetail(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Param('id') id: number,
|
||||
) {
|
||||
const merchantId = await this.getMerchantId(sellerId);
|
||||
return this.financeService.getSettlementDetail(merchantId, id);
|
||||
}
|
||||
|
||||
// 提现记录
|
||||
@Get('withdrawals')
|
||||
@ApiOperation({ summary: '提现记录列表' })
|
||||
async getWithdrawals(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Query() dto: QueryWithdrawalDto,
|
||||
) {
|
||||
const merchantId = await this.getMerchantId(sellerId);
|
||||
return this.financeService.getWithdrawals(merchantId, dto);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 平台管理端 ====================
|
||||
|
||||
@ApiTags('财务管理(管理员)')
|
||||
@Controller('admin/finance')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@ApiBearerAuth()
|
||||
export class AdminFinanceController {
|
||||
constructor(private readonly financeService: FinanceService) {}
|
||||
|
||||
// 对账单列表
|
||||
@Get('settlements')
|
||||
@ApiOperation({ summary: '对账单列表' })
|
||||
async getSettlements(@Query() dto: AdminQuerySettlementDto) {
|
||||
return this.financeService.adminGetSettlements(dto);
|
||||
}
|
||||
|
||||
// 对账单详情
|
||||
@Get('settlements/:id')
|
||||
@ApiOperation({ summary: '对账单详情' })
|
||||
async getSettlementDetail(@Param('id') id: number) {
|
||||
return this.financeService.adminGetSettlementDetail(id);
|
||||
}
|
||||
|
||||
// 审核通过对账单
|
||||
@Put('settlements/:id/approve')
|
||||
@ApiOperation({ summary: '审核通过对账单' })
|
||||
async approveSettlement(
|
||||
@CurrentUser('sub') reviewerId: number,
|
||||
@Param('id') id: number,
|
||||
@Body() _dto: ApproveSettlementDto,
|
||||
) {
|
||||
return this.financeService.approveSettlement(id, reviewerId);
|
||||
}
|
||||
|
||||
// 拒绝对账单
|
||||
@Put('settlements/:id/reject')
|
||||
@ApiOperation({ summary: '拒绝对账单' })
|
||||
async rejectSettlement(
|
||||
@CurrentUser('sub') reviewerId: number,
|
||||
@Param('id') id: number,
|
||||
@Body() dto: RejectSettlementDto,
|
||||
) {
|
||||
return this.financeService.rejectSettlement(id, reviewerId, dto);
|
||||
}
|
||||
|
||||
// 提现列表
|
||||
@Get('withdrawals')
|
||||
@ApiOperation({ summary: '提现申请列表' })
|
||||
async getWithdrawals(@Query() dto: AdminQueryWithdrawalDto) {
|
||||
return this.financeService.adminGetWithdrawals(dto);
|
||||
}
|
||||
|
||||
// 审核通过提现
|
||||
@Put('withdrawals/:id/approve')
|
||||
@ApiOperation({ summary: '审核通过提现' })
|
||||
async approveWithdrawal(
|
||||
@CurrentUser('sub') reviewerId: number,
|
||||
@Param('id') id: number,
|
||||
@Body() _dto: ApproveWithdrawalDto,
|
||||
) {
|
||||
return this.financeService.approveWithdrawal(id, reviewerId);
|
||||
}
|
||||
|
||||
// 拒绝提现
|
||||
@Put('withdrawals/:id/reject')
|
||||
@ApiOperation({ summary: '拒绝提现' })
|
||||
async rejectWithdrawal(
|
||||
@CurrentUser('sub') reviewerId: number,
|
||||
@Param('id') id: number,
|
||||
@Body() dto: RejectWithdrawalDto,
|
||||
) {
|
||||
return this.financeService.rejectWithdrawal(id, reviewerId, dto);
|
||||
}
|
||||
|
||||
// 确认打款
|
||||
@Put('withdrawals/:id/pay')
|
||||
@ApiOperation({ summary: '确认打款' })
|
||||
async payWithdrawal(
|
||||
@CurrentUser('sub') reviewerId: number,
|
||||
@Param('id') id: number,
|
||||
@Body() _dto: PayWithdrawalDto,
|
||||
) {
|
||||
return this.financeService.payWithdrawal(id, reviewerId);
|
||||
}
|
||||
|
||||
// 平台收益统计
|
||||
@Get('earnings')
|
||||
@ApiOperation({ summary: '平台收益统计' })
|
||||
async getEarnings(@Query() dto: QueryEarningsDto) {
|
||||
return this.financeService.getEarnings(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Settlement } from '@/entities/settlement.entity';
|
||||
import { SettlementItem } from '@/entities/settlement-item.entity';
|
||||
import { Withdrawal } from '@/entities/withdrawal.entity';
|
||||
import { Merchant } from '@/entities/merchant.entity';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { FinanceService } from './finance.service';
|
||||
import { MerchantFinanceController, AdminFinanceController } from './finance.controller';
|
||||
import { MerchantModule } from '../merchant/merchant.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Settlement, SettlementItem, Withdrawal, Merchant, Order]),
|
||||
MerchantModule,
|
||||
],
|
||||
controllers: [MerchantFinanceController, AdminFinanceController],
|
||||
providers: [FinanceService],
|
||||
exports: [FinanceService],
|
||||
})
|
||||
export class FinanceModule {}
|
||||
@@ -0,0 +1,452 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, Between, FindOptionsWhere } from 'typeorm';
|
||||
import { Settlement } from '@/entities/settlement.entity';
|
||||
import { SettlementItem } from '@/entities/settlement-item.entity';
|
||||
import { Withdrawal } from '@/entities/withdrawal.entity';
|
||||
import { Merchant } from '@/entities/merchant.entity';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import {
|
||||
QuerySettlementDto,
|
||||
CreateWithdrawalDto,
|
||||
UpdateBankInfoDto,
|
||||
QueryWithdrawalDto,
|
||||
AdminQuerySettlementDto,
|
||||
RejectSettlementDto,
|
||||
AdminQueryWithdrawalDto,
|
||||
RejectWithdrawalDto,
|
||||
QueryEarningsDto,
|
||||
} from './dto/finance.dto';
|
||||
|
||||
@Injectable()
|
||||
export class FinanceService {
|
||||
constructor(
|
||||
@InjectRepository(Settlement)
|
||||
private settlementRepo: Repository<Settlement>,
|
||||
@InjectRepository(SettlementItem)
|
||||
private settlementItemRepo: Repository<SettlementItem>,
|
||||
@InjectRepository(Withdrawal)
|
||||
private withdrawalRepo: Repository<Withdrawal>,
|
||||
@InjectRepository(Merchant)
|
||||
private merchantRepo: Repository<Merchant>,
|
||||
@InjectRepository(Order)
|
||||
private orderRepo: Repository<Order>,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
// ==================== 商家端 ====================
|
||||
|
||||
// 对账单列表
|
||||
async getSettlements(merchantId: number, dto: QuerySettlementDto) {
|
||||
const where: FindOptionsWhere<Settlement> = { merchantId };
|
||||
if (dto.status) where.status = dto.status;
|
||||
if (dto.periodStart && dto.periodEnd) {
|
||||
where.periodStart = Between(dto.periodStart, dto.periodEnd);
|
||||
}
|
||||
|
||||
const [list, total] = await this.settlementRepo.findAndCount({
|
||||
where,
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
|
||||
take: dto.pageSize ?? 10,
|
||||
});
|
||||
|
||||
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
|
||||
}
|
||||
|
||||
// 对账单详情(含明细)
|
||||
async getSettlementDetail(merchantId: number, id: number) {
|
||||
const settlement = await this.settlementRepo.findOne({
|
||||
where: { id, merchantId },
|
||||
relations: ['items'],
|
||||
});
|
||||
if (!settlement) throw new NotFoundException('对账单不存在');
|
||||
return settlement;
|
||||
}
|
||||
|
||||
// 钱包信息
|
||||
async getWallet(merchantId: number) {
|
||||
const merchant = await this.merchantRepo.findOne({
|
||||
where: { id: merchantId },
|
||||
select: ['id', 'walletBalance', 'bankName', 'bankAccount', 'accountName'],
|
||||
});
|
||||
if (!merchant) throw new NotFoundException('商家不存在');
|
||||
|
||||
const pendingAmount = await this.withdrawalRepo
|
||||
.createQueryBuilder('w')
|
||||
.where('w.merchant_id = :merchantId AND w.status IN (:...statuses)', {
|
||||
merchantId,
|
||||
statuses: ['pending', 'approved'],
|
||||
})
|
||||
.select('COALESCE(SUM(w.amount), 0)', 'total')
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
walletBalance: merchant.walletBalance,
|
||||
bankName: merchant.bankName,
|
||||
bankAccount: merchant.bankAccount,
|
||||
accountName: merchant.accountName,
|
||||
pendingWithdrawAmount: Number(pendingAmount?.total || 0),
|
||||
};
|
||||
}
|
||||
|
||||
// 更新银行卡信息
|
||||
async updateBankInfo(merchantId: number, dto: UpdateBankInfoDto) {
|
||||
await this.merchantRepo.update({ id: merchantId }, {
|
||||
bankName: dto.bankName,
|
||||
bankAccount: dto.bankAccount,
|
||||
accountName: dto.accountName,
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 申请提现
|
||||
async createWithdrawal(merchantId: number, dto: CreateWithdrawalDto) {
|
||||
const merchant = await this.merchantRepo.findOne({ where: { id: merchantId } });
|
||||
if (!merchant) throw new NotFoundException('商家不存在');
|
||||
|
||||
if (Number(merchant.walletBalance) < dto.amount) {
|
||||
throw new BadRequestException('余额不足');
|
||||
}
|
||||
|
||||
if (dto.amount < 100) {
|
||||
throw new BadRequestException('最低提现金额为100元');
|
||||
}
|
||||
|
||||
// 查找已审核通过的对账单
|
||||
const approvedSettlements = await this.settlementRepo.find({
|
||||
where: { merchantId, status: 'approved' },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
// 计算手续费率
|
||||
const feeRate = 0.006;
|
||||
const fee = Math.round(dto.amount * feeRate * 100) / 100;
|
||||
const commissionRate = Number(merchant.walletBalance) > 0 ? 0.10 : 0;
|
||||
const commissionAmount = Math.round(dto.amount * commissionRate * 100) / 100;
|
||||
const actualAmount = Math.round((dto.amount - fee - commissionAmount) * 100) / 100;
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 扣减商家余额
|
||||
await queryRunner.manager.decrement(Merchant, { id: merchantId }, 'walletBalance', dto.amount);
|
||||
|
||||
const withdrawal = queryRunner.manager.create(Withdrawal, {
|
||||
merchantId,
|
||||
settlementIds: approvedSettlements.map(s => s.id),
|
||||
amount: dto.amount,
|
||||
fee,
|
||||
commissionAmount,
|
||||
actualAmount,
|
||||
bankName: dto.bankName,
|
||||
bankAccount: dto.bankAccount,
|
||||
accountName: dto.accountName,
|
||||
status: 'pending',
|
||||
});
|
||||
await queryRunner.manager.save(withdrawal);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return withdrawal;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 提现记录列表
|
||||
async getWithdrawals(merchantId: number, dto: QueryWithdrawalDto) {
|
||||
const where: FindOptionsWhere<Withdrawal> = { merchantId };
|
||||
if (dto.status) where.status = dto.status;
|
||||
|
||||
const [list, total] = await this.withdrawalRepo.findAndCount({
|
||||
where,
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
|
||||
take: dto.pageSize ?? 10,
|
||||
});
|
||||
|
||||
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
|
||||
}
|
||||
|
||||
// ==================== 平台管理端 ====================
|
||||
|
||||
// 管理端 - 对账单列表
|
||||
async adminGetSettlements(dto: AdminQuerySettlementDto) {
|
||||
const where: FindOptionsWhere<Settlement> = {};
|
||||
if (dto.status) where.status = dto.status;
|
||||
if (dto.merchantId) where.merchantId = dto.merchantId;
|
||||
if (dto.periodStart && dto.periodEnd) {
|
||||
where.periodStart = Between(dto.periodStart, dto.periodEnd);
|
||||
}
|
||||
|
||||
const [list, total] = await this.settlementRepo.findAndCount({
|
||||
where,
|
||||
relations: ['merchant'],
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
|
||||
take: dto.pageSize ?? 10,
|
||||
});
|
||||
|
||||
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
|
||||
}
|
||||
|
||||
// 管理端 - 对账单详情
|
||||
async adminGetSettlementDetail(id: number) {
|
||||
const settlement = await this.settlementRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['merchant', 'items'],
|
||||
});
|
||||
if (!settlement) throw new NotFoundException('对账单不存在');
|
||||
return settlement;
|
||||
}
|
||||
|
||||
// 审核通过对账单
|
||||
async approveSettlement(id: number, reviewerId: number) {
|
||||
const settlement = await this.settlementRepo.findOne({ where: { id } });
|
||||
if (!settlement) throw new NotFoundException('对账单不存在');
|
||||
if (settlement.status !== 'pending') throw new BadRequestException('只能审核待审核的对账单');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.manager.update(Settlement, { id }, {
|
||||
status: 'approved',
|
||||
reviewerId,
|
||||
reviewedAt: new Date(),
|
||||
});
|
||||
|
||||
// 对账单金额进入商家待提现余额
|
||||
await queryRunner.manager.increment(
|
||||
Merchant,
|
||||
{ id: settlement.merchantId },
|
||||
'walletBalance',
|
||||
Number(settlement.settlementAmount),
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝对账单
|
||||
async rejectSettlement(id: number, reviewerId: number, dto: RejectSettlementDto) {
|
||||
const settlement = await this.settlementRepo.findOne({ where: { id } });
|
||||
if (!settlement) throw new NotFoundException('对账单不存在');
|
||||
if (settlement.status !== 'pending') throw new BadRequestException('只能审核待审核的对账单');
|
||||
|
||||
await this.settlementRepo.update({ id }, {
|
||||
status: 'rejected',
|
||||
reviewerId,
|
||||
reviewedAt: new Date(),
|
||||
rejectReason: dto.rejectReason,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 管理端 - 提现列表
|
||||
async adminGetWithdrawals(dto: AdminQueryWithdrawalDto) {
|
||||
const where: FindOptionsWhere<Withdrawal> = {};
|
||||
if (dto.status) where.status = dto.status;
|
||||
if (dto.merchantId) where.merchantId = dto.merchantId;
|
||||
|
||||
const [list, total] = await this.withdrawalRepo.findAndCount({
|
||||
where,
|
||||
relations: ['merchant'],
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: ((dto.page ?? 1) - 1) * (dto.pageSize ?? 10),
|
||||
take: dto.pageSize ?? 10,
|
||||
});
|
||||
|
||||
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
|
||||
}
|
||||
|
||||
// 审核通过提现
|
||||
async approveWithdrawal(id: number, reviewerId: number) {
|
||||
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
|
||||
if (!withdrawal) throw new NotFoundException('提现记录不存在');
|
||||
if (withdrawal.status !== 'pending') throw new BadRequestException('只能审核待审核的提现');
|
||||
|
||||
await this.withdrawalRepo.update({ id }, {
|
||||
status: 'approved',
|
||||
reviewerId,
|
||||
reviewedAt: new Date(),
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 拒绝提现(退还余额)
|
||||
async rejectWithdrawal(id: number, reviewerId: number, dto: RejectWithdrawalDto) {
|
||||
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
|
||||
if (!withdrawal) throw new NotFoundException('提现记录不存在');
|
||||
if (withdrawal.status !== 'pending') throw new BadRequestException('只能审核待审核的提现');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.manager.update(Withdrawal, { id }, {
|
||||
status: 'rejected',
|
||||
reviewerId,
|
||||
reviewedAt: new Date(),
|
||||
rejectReason: dto.rejectReason,
|
||||
});
|
||||
|
||||
// 退还商家余额
|
||||
await queryRunner.manager.increment(
|
||||
Merchant,
|
||||
{ id: withdrawal.merchantId },
|
||||
'walletBalance',
|
||||
Number(withdrawal.amount),
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// 确认打款
|
||||
async payWithdrawal(id: number, reviewerId: number) {
|
||||
const withdrawal = await this.withdrawalRepo.findOne({ where: { id } });
|
||||
if (!withdrawal) throw new NotFoundException('提现记录不存在');
|
||||
if (withdrawal.status !== 'approved') throw new BadRequestException('只能对已审核通过的提现确认打款');
|
||||
|
||||
await this.withdrawalRepo.update({ id }, {
|
||||
status: 'paid',
|
||||
paidAt: new Date(),
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 平台收益统计
|
||||
async getEarnings(dto: QueryEarningsDto) {
|
||||
const qb = this.settlementRepo.createQueryBuilder('s');
|
||||
|
||||
if (dto.startDate && dto.endDate) {
|
||||
qb.where('s.period_start BETWEEN :startDate AND :endDate', {
|
||||
startDate: dto.startDate,
|
||||
endDate: dto.endDate,
|
||||
});
|
||||
}
|
||||
|
||||
const settlementStats = await qb
|
||||
.select('COUNT(*)', 'totalSettlements')
|
||||
.addSelect('COALESCE(SUM(s.order_amount), 0)', 'totalOrderAmount')
|
||||
.addSelect('COALESCE(SUM(s.commission_amount), 0)', 'totalCommission')
|
||||
.addSelect('COALESCE(SUM(s.settlement_amount), 0)', 'totalSettlementAmount')
|
||||
.getRawOne();
|
||||
|
||||
// 提现统计
|
||||
const wQb = this.withdrawalRepo.createQueryBuilder('w');
|
||||
const withdrawalStats = await wQb
|
||||
.select('COUNT(*)', 'totalWithdrawals')
|
||||
.addSelect('COALESCE(SUM(CASE WHEN w.status = \'paid\' THEN w.amount ELSE 0 END), 0)', 'paidAmount')
|
||||
.addSelect('COALESCE(SUM(w.fee), 0)', 'totalFee')
|
||||
.addSelect('COALESCE(SUM(w.commission_amount), 0)', 'totalWithdrawCommission')
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
orderAmount: Number(settlementStats?.totalOrderAmount || 0),
|
||||
commission: Number(settlementStats?.totalCommission || 0),
|
||||
settlementAmount: Number(settlementStats?.totalSettlementAmount || 0),
|
||||
totalSettlements: Number(settlementStats?.totalSettlements || 0),
|
||||
paidAmount: Number(withdrawalStats?.paidAmount || 0),
|
||||
totalFee: Number(withdrawalStats?.totalFee || 0),
|
||||
totalWithdrawCommission: Number(withdrawalStats?.totalWithdrawCommission || 0),
|
||||
totalWithdrawals: Number(withdrawalStats?.totalWithdrawals || 0),
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 定时任务调用 ====================
|
||||
|
||||
// 生成周期对账单
|
||||
async generateSettlements(periodStart: string, periodEnd: string) {
|
||||
// 查找所有已审核通过的商家
|
||||
const merchants = await this.merchantRepo.find({ where: { status: 'approved' } });
|
||||
|
||||
const results: { merchantId: number; settlementId: number; orderCount: number }[] = [];
|
||||
for (const merchant of merchants) {
|
||||
// 检查是否已生成过该周期的对账单
|
||||
const existing = await this.settlementRepo.findOne({
|
||||
where: { merchantId: merchant.id, periodStart, periodEnd },
|
||||
});
|
||||
if (existing) continue;
|
||||
|
||||
// 查询该周期内已完成的订单
|
||||
const orders = await this.orderRepo
|
||||
.createQueryBuilder('o')
|
||||
.where('o.merchant_id = :merchantId', { merchantId: merchant.id })
|
||||
.andWhere('o.status = :status', { status: 'completed' })
|
||||
.andWhere('o.check_in_date >= :periodStart', { periodStart })
|
||||
.andWhere('o.check_in_date <= :periodEnd', { periodEnd })
|
||||
.getMany();
|
||||
|
||||
if (orders.length === 0) continue;
|
||||
|
||||
const orderAmount = orders.reduce((sum, o) => sum + Number(o.payAmount), 0);
|
||||
const commissionRate = 0.10;
|
||||
const commissionAmount = Math.round(orderAmount * commissionRate * 100) / 100;
|
||||
const settlementAmount = Math.round((orderAmount - commissionAmount) * 100) / 100;
|
||||
const settlementNo = `ST${Date.now()}${merchant.id}`;
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const settlement = queryRunner.manager.create(Settlement, {
|
||||
merchantId: merchant.id,
|
||||
settlementNo,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
orderCount: orders.length,
|
||||
orderAmount,
|
||||
commissionRate,
|
||||
commissionAmount,
|
||||
settlementAmount,
|
||||
status: 'pending',
|
||||
});
|
||||
await queryRunner.manager.save(settlement);
|
||||
|
||||
// 保存对账单明细
|
||||
const items = orders.map(o => queryRunner.manager.create(SettlementItem, {
|
||||
settlementId: settlement.id,
|
||||
orderId: o.id,
|
||||
orderNo: o.orderNo,
|
||||
orderAmount: Number(o.payAmount),
|
||||
}));
|
||||
await queryRunner.manager.save(items);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
results.push({ merchantId: merchant.id, settlementId: settlement.id, orderCount: orders.length });
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
console.error(`生成对账单失败 merchantId=${merchant.id}:`, err.message);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { Room } from '@/entities/room.entity';
|
||||
@@ -6,11 +6,13 @@ import { RoomCalendar } from '@/entities/room-calendar.entity';
|
||||
import { OrderService } from './order.service';
|
||||
import { UserOrderController, MerchantOrderController, AdminOrderController } from './order.controller';
|
||||
import { MerchantModule } from '../merchant/merchant.module';
|
||||
import { ActivityModule } from '../activity/activity.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Order, Room, RoomCalendar]),
|
||||
MerchantModule,
|
||||
forwardRef(() => ActivityModule),
|
||||
],
|
||||
controllers: [UserOrderController, MerchantOrderController, AdminOrderController],
|
||||
providers: [OrderService],
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Order } from '@/entities/order.entity';
|
||||
import { Room } from '@/entities/room.entity';
|
||||
import { RoomCalendar } from '@/entities/room-calendar.entity';
|
||||
import { CreateOrderDto, QueryOrderDto, ConfirmOrderDto } from './dto/order.dto';
|
||||
import { ActivityService } from '@/modules/activity/activity.service';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
@@ -15,6 +16,7 @@ export class OrderService {
|
||||
private roomRepo: Repository<Room>,
|
||||
@InjectRepository(RoomCalendar)
|
||||
private calendarRepo: Repository<RoomCalendar>,
|
||||
private readonly activityService: ActivityService,
|
||||
) {}
|
||||
|
||||
async create(userId: number, dto: CreateOrderDto) {
|
||||
@@ -30,9 +32,8 @@ export class OrderService {
|
||||
|
||||
const roomCount = dto.roomCount || 1;
|
||||
const roomAmount = room.price * nights * roomCount;
|
||||
const serviceFee = Math.round(roomAmount * 0.05 * 100) / 100;
|
||||
const couponDiscount = 0; // TODO: 计算优惠券抵扣
|
||||
const totalAmount = roomAmount + serviceFee;
|
||||
const totalAmount = roomAmount;
|
||||
const payAmount = totalAmount - couponDiscount;
|
||||
|
||||
const now = new Date();
|
||||
@@ -58,7 +59,6 @@ export class OrderService {
|
||||
guestCount: dto.guestCount || 1,
|
||||
roomPrice: room.price,
|
||||
roomAmount,
|
||||
serviceFee,
|
||||
couponDiscount,
|
||||
totalAmount,
|
||||
payAmount,
|
||||
@@ -121,16 +121,39 @@ export class OrderService {
|
||||
async cancel(userId: number, id: number, reason: string) {
|
||||
const order = await this.findById(id);
|
||||
if (order.userId !== userId) throw new ForbiddenException('无权操作此订单');
|
||||
if (!['pending_pay', 'pending_confirm'].includes(order.status)) {
|
||||
if (!['pending_pay', 'pending_confirm', 'pending_checkin'].includes(order.status)) {
|
||||
throw new BadRequestException('当前订单状态不可取消');
|
||||
}
|
||||
|
||||
// 已支付的订单(pending_confirm、pending_checkin)取消时需退款并恢复库存
|
||||
const needRefund = order.status !== 'pending_pay';
|
||||
if (needRefund) {
|
||||
// 恢复房态库存
|
||||
const checkIn = new Date(order.checkInDate);
|
||||
const checkOut = new Date(order.checkOutDate);
|
||||
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
const calendar = await this.calendarRepo.findOne({
|
||||
where: { roomId: order.roomId, date: dateStr },
|
||||
});
|
||||
if (calendar) {
|
||||
await this.calendarRepo.update(calendar.id, {
|
||||
sold: Math.max(0, calendar.sold - order.roomCount),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.orderRepo.update(id, {
|
||||
status: 'cancelled',
|
||||
cancelReason: reason,
|
||||
cancelledAt: new Date(),
|
||||
...(needRefund ? {
|
||||
refundAmount: order.payAmount,
|
||||
refundAt: new Date(),
|
||||
} : {}),
|
||||
});
|
||||
return { message: '订单已取消' };
|
||||
return { message: needRefund ? '订单已取消,退款将原路返回' : '订单已取消' };
|
||||
}
|
||||
|
||||
async confirm(merchantId: number, id: number) {
|
||||
@@ -186,10 +209,14 @@ export class OrderService {
|
||||
status: 'completed',
|
||||
checkoutAt: new Date(),
|
||||
});
|
||||
|
||||
// 触发邀请返现
|
||||
this.activityService.handleOrderCompleted(id).catch(() => {});
|
||||
|
||||
return { message: '订单已完成' };
|
||||
}
|
||||
|
||||
// 用户申请退款(待确认、待入住状态)
|
||||
// 用户申请退款(待确认、待入住状态)- 直接退款,无需审核
|
||||
async refund(userId: number, id: number, reason: string) {
|
||||
const order = await this.findById(id);
|
||||
if (order.userId !== userId) throw new ForbiddenException('无权操作此订单');
|
||||
@@ -197,14 +224,33 @@ export class OrderService {
|
||||
throw new BadRequestException('当前订单状态不可申请退款');
|
||||
}
|
||||
|
||||
// 恢复房态库存
|
||||
const checkIn = new Date(order.checkInDate);
|
||||
const checkOut = new Date(order.checkOutDate);
|
||||
for (let d = new Date(checkIn); d < checkOut; d.setDate(d.getDate() + 1)) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
const calendar = await this.calendarRepo.findOne({
|
||||
where: { roomId: order.roomId, date: dateStr },
|
||||
});
|
||||
if (calendar) {
|
||||
await this.calendarRepo.update(calendar.id, {
|
||||
sold: Math.max(0, calendar.sold - order.roomCount),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 直接退款,状态改为 refunded
|
||||
await this.orderRepo.update(id, {
|
||||
status: 'refunding',
|
||||
status: 'refunded',
|
||||
cancelReason: reason,
|
||||
refundAmount: order.payAmount,
|
||||
refundAt: new Date(),
|
||||
cancelledAt: new Date(),
|
||||
});
|
||||
return { message: '退款申请已提交' };
|
||||
return { message: '退款成功' };
|
||||
}
|
||||
|
||||
// 商家同意退款
|
||||
// 商家同意退款(已废弃,退款不再需要审核)
|
||||
async approveRefund(merchantId: number, id: number) {
|
||||
const order = await this.findById(id);
|
||||
if (order.merchantId !== merchantId) throw new ForbiddenException('无权操作此订单');
|
||||
@@ -236,7 +282,7 @@ export class OrderService {
|
||||
return { message: '退款成功' };
|
||||
}
|
||||
|
||||
// 商家拒绝退款
|
||||
// 商家拒绝退款(已废弃,退款不再需要审核)
|
||||
async rejectRefund(merchantId: number, id: number, reason: string) {
|
||||
const order = await this.findById(id);
|
||||
if (order.merchantId !== merchantId) throw new ForbiddenException('无权操作此订单');
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IsNotEmpty, IsOptional, IsNumber, IsString, IsBoolean, Min, Max, IsInt } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateReviewDto {
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
rating: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
images?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isAnonymous?: boolean;
|
||||
}
|
||||
|
||||
export class QueryReviewDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
pageSize?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
merchantId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
roomId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
auditStatus?: string;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ReviewService } from './review.service';
|
||||
import { CreateReviewDto, QueryReviewDto } from './dto/review.dto';
|
||||
import { JwtAuthGuard, RolesGuard } from '@/common';
|
||||
import { Roles } from '@/common/decorators/roles.decorator';
|
||||
|
||||
// 用户端评价接口
|
||||
@Controller('reviews')
|
||||
export class ReviewPublicController {
|
||||
constructor(private readonly reviewService: ReviewService) {}
|
||||
|
||||
// 提交评价(需登录)
|
||||
@Post('order/:orderId')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async create(
|
||||
@Request() req,
|
||||
@Param('orderId') orderId: number,
|
||||
@Body() dto: CreateReviewDto,
|
||||
) {
|
||||
return this.reviewService.create(req.user.userId, Number(orderId), dto);
|
||||
}
|
||||
|
||||
// 查询评价列表(公开)
|
||||
@Get()
|
||||
async list(@Query() query: QueryReviewDto) {
|
||||
return this.reviewService.findPublic(query);
|
||||
}
|
||||
|
||||
// 检查订单是否已评价
|
||||
@Get('check/:orderId')
|
||||
async checkOrder(@Param('orderId') orderId: number) {
|
||||
const review = await this.reviewService.findByOrder(Number(orderId));
|
||||
return { reviewed: !!review, review };
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员评价审核接口
|
||||
@Controller('admin/reviews')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
export class AdminReviewController {
|
||||
constructor(private readonly reviewService: ReviewService) {}
|
||||
|
||||
// 评价审核列表
|
||||
@Get()
|
||||
async list(@Query() query: QueryReviewDto) {
|
||||
return this.reviewService.findAllForAdmin(query);
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
@Put(':id/approve')
|
||||
async approve(@Param('id') id: number) {
|
||||
return this.reviewService.approve(Number(id));
|
||||
}
|
||||
|
||||
// 审核拒绝
|
||||
@Put(':id/reject')
|
||||
async reject(@Param('id') id: number, @Body('reason') reason: string) {
|
||||
return this.reviewService.reject(Number(id), reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Review } from '@/entities/review.entity';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { Merchant } from '@/entities/merchant.entity';
|
||||
import { Room } from '@/entities/room.entity';
|
||||
import { ReviewService } from './review.service';
|
||||
import { ReviewPublicController, AdminReviewController } from './review.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Review, Order, Merchant, Room])],
|
||||
controllers: [ReviewPublicController, AdminReviewController],
|
||||
providers: [ReviewService],
|
||||
exports: [ReviewService],
|
||||
})
|
||||
export class ReviewModule {}
|
||||
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Review } from '@/entities/review.entity';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { Merchant } from '@/entities/merchant.entity';
|
||||
import { Room } from '@/entities/room.entity';
|
||||
import { CreateReviewDto, QueryReviewDto } from './dto/review.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ReviewService {
|
||||
constructor(
|
||||
@InjectRepository(Review)
|
||||
private reviewRepo: Repository<Review>,
|
||||
@InjectRepository(Order)
|
||||
private orderRepo: Repository<Order>,
|
||||
@InjectRepository(Merchant)
|
||||
private merchantRepo: Repository<Merchant>,
|
||||
@InjectRepository(Room)
|
||||
private roomRepo: Repository<Room>,
|
||||
) {}
|
||||
|
||||
// 用户提交评价
|
||||
async create(userId: number, orderId: number, dto: CreateReviewDto) {
|
||||
const order = await this.orderRepo.findOne({ where: { id: orderId } });
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
if (order.userId !== userId) throw new ForbiddenException('无权操作此订单');
|
||||
if (order.status !== 'completed') throw new BadRequestException('只有已完成的订单才能评价');
|
||||
|
||||
// 检查是否已评价
|
||||
const existing = await this.reviewRepo.findOne({ where: { orderId } });
|
||||
if (existing) throw new BadRequestException('该订单已评价');
|
||||
|
||||
if (dto.rating < 1 || dto.rating > 5) throw new BadRequestException('评分范围为1-5');
|
||||
|
||||
const review = this.reviewRepo.create({
|
||||
orderId,
|
||||
userId,
|
||||
merchantId: order.merchantId,
|
||||
roomId: order.roomId,
|
||||
rating: dto.rating,
|
||||
content: dto.content,
|
||||
images: dto.images,
|
||||
isAnonymous: dto.isAnonymous || false,
|
||||
auditStatus: 'pending',
|
||||
});
|
||||
|
||||
await this.reviewRepo.save(review);
|
||||
return { message: '评价已提交,等待审核' };
|
||||
}
|
||||
|
||||
// 查询评价列表(公开,只显示审核通过的)
|
||||
async findPublic(query: QueryReviewDto) {
|
||||
const { page = 1, pageSize = 10, merchantId, roomId } = query;
|
||||
const qb = this.reviewRepo
|
||||
.createQueryBuilder('r')
|
||||
.leftJoinAndSelect('r.order', 'o')
|
||||
.where('r.auditStatus = :auditStatus', { auditStatus: 'approved' });
|
||||
|
||||
if (merchantId) {
|
||||
qb.andWhere('r.merchantId = :merchantId', { merchantId: Number(merchantId) });
|
||||
}
|
||||
if (roomId) {
|
||||
qb.andWhere('r.roomId = :roomId', { roomId: Number(roomId) });
|
||||
}
|
||||
|
||||
qb.orderBy('r.createdAt', 'DESC');
|
||||
const safePage = Number(page) || 1;
|
||||
const safePageSize = Number(pageSize) || 10;
|
||||
qb.skip((safePage - 1) * safePageSize).take(safePageSize);
|
||||
|
||||
const [list, total] = await qb.getManyAndCount();
|
||||
return { list, total, page: safePage, pageSize: safePageSize, totalPages: Math.ceil(total / safePageSize) };
|
||||
}
|
||||
|
||||
// 检查订单是否已评价
|
||||
async findByOrder(orderId: number) {
|
||||
return this.reviewRepo.findOne({ where: { orderId } });
|
||||
}
|
||||
|
||||
// 管理员审核评价列表
|
||||
async findAllForAdmin(query: QueryReviewDto) {
|
||||
const { page = 1, pageSize = 10, auditStatus } = query;
|
||||
const qb = this.reviewRepo
|
||||
.createQueryBuilder('r')
|
||||
.leftJoinAndSelect('r.order', 'o');
|
||||
|
||||
if (auditStatus) {
|
||||
qb.andWhere('r.auditStatus = :auditStatus', { auditStatus });
|
||||
}
|
||||
|
||||
qb.orderBy('r.createdAt', 'DESC');
|
||||
const safePage = Number(page) || 1;
|
||||
const safePageSize = Number(pageSize) || 10;
|
||||
qb.skip((safePage - 1) * safePageSize).take(safePageSize);
|
||||
|
||||
const [list, total] = await qb.getManyAndCount();
|
||||
return { list, total, page: safePage, pageSize: safePageSize, totalPages: Math.ceil(total / safePageSize) };
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
async approve(id: number) {
|
||||
const review = await this.reviewRepo.findOne({ where: { id } });
|
||||
if (!review) throw new NotFoundException('评价不存在');
|
||||
|
||||
await this.reviewRepo.update(id, { auditStatus: 'approved', auditRejectReason: '' });
|
||||
|
||||
// 更新商家和房源的评分
|
||||
await this.updateMerchantRating(review.merchantId);
|
||||
await this.updateRoomRating(review.roomId);
|
||||
|
||||
return { message: '评价已通过审核' };
|
||||
}
|
||||
|
||||
// 审核拒绝
|
||||
async reject(id: number, reason: string) {
|
||||
const review = await this.reviewRepo.findOne({ where: { id } });
|
||||
if (!review) throw new NotFoundException('评价不存在');
|
||||
|
||||
await this.reviewRepo.update(id, { auditStatus: 'rejected', auditRejectReason: reason });
|
||||
return { message: '评价已拒绝' };
|
||||
}
|
||||
|
||||
// 更新商家平均评分
|
||||
private async updateMerchantRating(merchantId: number) {
|
||||
const result = await this.reviewRepo
|
||||
.createQueryBuilder('r')
|
||||
.select('AVG(r.rating)', 'avgRating')
|
||||
.addSelect('COUNT(r.id)', 'count')
|
||||
.where('r.merchantId = :merchantId', { merchantId })
|
||||
.andWhere('r.auditStatus = :auditStatus', { auditStatus: 'approved' })
|
||||
.getRawOne();
|
||||
|
||||
if (result) {
|
||||
await this.merchantRepo.update(merchantId, {
|
||||
rating: parseFloat(parseFloat(result.avgRating || 0).toFixed(1)),
|
||||
reviewCount: parseInt(result.count || 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新房源平均评分
|
||||
private async updateRoomRating(roomId: number) {
|
||||
const result = await this.reviewRepo
|
||||
.createQueryBuilder('r')
|
||||
.select('AVG(r.rating)', 'avgRating')
|
||||
.addSelect('COUNT(r.id)', 'count')
|
||||
.where('r.roomId = :roomId', { roomId })
|
||||
.andWhere('r.auditStatus = :auditStatus', { auditStatus: 'approved' })
|
||||
.getRawOne();
|
||||
|
||||
if (result) {
|
||||
await this.roomRepo.update(roomId, {
|
||||
rating: parseFloat(parseFloat(result.avgRating || 0).toFixed(1)),
|
||||
reviewCount: parseInt(result.count || 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
|
||||
@Injectable()
|
||||
export class OrderSchedule {
|
||||
private readonly logger = new Logger(OrderSchedule.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Order)
|
||||
private orderRepo: Repository<Order>,
|
||||
) {}
|
||||
|
||||
// 每天凌晨1点执行,将前一天已入住且离店日期为前一天的订单标记为已完成
|
||||
@Cron('0 1 * * *')
|
||||
async completeExpiredOrders() {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
const result = await this.orderRepo
|
||||
.createQueryBuilder()
|
||||
.update(Order)
|
||||
.set({ status: 'completed', checkoutAt: new Date() })
|
||||
.where('status = :status', { status: 'checked_in' })
|
||||
.andWhere('DATE(checkOutDate) = :date', { date: yesterdayStr })
|
||||
.execute();
|
||||
|
||||
if (result.affected && result.affected > 0) {
|
||||
this.logger.log(
|
||||
`已自动完成 ${result.affected} 笔过期订单 (${yesterdayStr})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Order } from '@/entities/order.entity';
|
||||
import { OrderSchedule } from './order.schedule';
|
||||
import { SettlementSchedule } from './settlement.schedule';
|
||||
import { FinanceModule } from '@/modules/finance/finance.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Order]),
|
||||
FinanceModule,
|
||||
],
|
||||
providers: [OrderSchedule, SettlementSchedule],
|
||||
exports: [OrderSchedule, SettlementSchedule],
|
||||
})
|
||||
export class ScheduleModule {}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { FinanceService } from '@/modules/finance/finance.service';
|
||||
|
||||
@Injectable()
|
||||
export class SettlementSchedule {
|
||||
constructor(private readonly financeService: FinanceService) {}
|
||||
|
||||
// 每周日凌晨1点生成上周对账单
|
||||
@Cron('0 1 * * 0')
|
||||
async handleWeeklySettlement() {
|
||||
const now = new Date();
|
||||
// 上周日~上周六
|
||||
const dayOfWeek = now.getDay(); // 0=Sunday
|
||||
const lastSunday = new Date(now);
|
||||
lastSunday.setDate(now.getDate() - dayOfWeek - 7);
|
||||
const lastSaturday = new Date(lastSunday);
|
||||
lastSaturday.setDate(lastSunday.getDate() + 6);
|
||||
|
||||
const periodStart = lastSunday.toISOString().split('T')[0];
|
||||
const periodEnd = lastSaturday.toISOString().split('T')[0];
|
||||
|
||||
console.log(
|
||||
`[SettlementSchedule] 开始生成对账单: ${periodStart} ~ ${periodEnd}`,
|
||||
);
|
||||
try {
|
||||
const results = await this.financeService.generateSettlements(
|
||||
periodStart,
|
||||
periodEnd,
|
||||
);
|
||||
console.log(
|
||||
`[SettlementSchedule] 生成完成,共 ${results.length} 条对账单`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[SettlementSchedule] 生成对账单失败:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user