feat: 迭代
This commit is contained in:
@@ -19,9 +19,15 @@ export class Merchant {
|
||||
@Column({ length: 500, default: '', comment: '店铺Logo' })
|
||||
logo: string;
|
||||
|
||||
@Column({ name: 'hotel_images', type: 'text', nullable: true, comment: '酒店照片,多张URL用逗号分隔' })
|
||||
hotelImages: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true, comment: '店铺描述' })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'store_license', length: 255, nullable: true, comment: '门店营业执照' })
|
||||
storeLicense: string;
|
||||
|
||||
@Column({ length: 20, comment: '联系电话' })
|
||||
phone: string;
|
||||
|
||||
@@ -46,6 +52,22 @@ export class Merchant {
|
||||
@Column({ name: 'business_license', length: 500, default: '', comment: '营业执照图片' })
|
||||
businessLicense: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'contract_type', length: 20, default: 'personal', comment: '签约类型:personal-个人签约,company-公司签约' })
|
||||
contractType: string;
|
||||
|
||||
@Column({ name: 'id_card_front', length: 255, nullable: true, comment: '身份证正面(个人签约)' })
|
||||
idCardFront: string;
|
||||
|
||||
@Column({ name: 'id_card_back', length: 255, nullable: true, comment: '身份证反面(个人签约)' })
|
||||
idCardBack: string;
|
||||
|
||||
@Column({ name: 'legal_id_card_front', length: 255, nullable: true, comment: '法人身份证正面(公司签约)' })
|
||||
legalIdCardFront: string;
|
||||
|
||||
@Column({ name: 'legal_id_card_back', length: 255, nullable: true, comment: '法人身份证反面(公司签约)' })
|
||||
legalIdCardBack: string;
|
||||
|
||||
@Column({ name: 'license_no', length: 50, default: '', comment: '营业执照编号' })
|
||||
licenseNo: string;
|
||||
|
||||
@@ -65,6 +87,10 @@ export class Merchant {
|
||||
@Column({ name: 'wallet_balance', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0, comment: '待提现余额' })
|
||||
walletBalance: number;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'account_type', length: 20, default: 'company', comment: '账户类型:company-对公账户,personal-对私账户' })
|
||||
accountType: string;
|
||||
|
||||
@Column({ name: 'bank_name', length: 100, default: '', comment: '开户银行' })
|
||||
bankName: string;
|
||||
|
||||
@@ -74,6 +100,18 @@ export class Merchant {
|
||||
@Column({ name: 'account_name', length: 50, default: '', comment: '账户名' })
|
||||
accountName: string;
|
||||
|
||||
@Column({ name: 'bank_branch', length: 100, nullable: true, comment: '支行信息(对私账户)' })
|
||||
bankBranch: string;
|
||||
|
||||
@Column({ name: 'bank_license', length: 255, nullable: true, comment: '开户营业执照(对公账户)' })
|
||||
bankLicense: string;
|
||||
|
||||
@Column({ name: 'account_id_card_front', length: 255, nullable: true, comment: '开户人身份证正面(对私账户)' })
|
||||
accountIdCardFront: string;
|
||||
|
||||
@Column({ name: 'account_id_card_back', length: 255, nullable: true, comment: '开户人身份证反面(对私账户)' })
|
||||
accountIdCardBack: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'decimal', precision: 2, scale: 1, unsigned: true, default: 5.0, comment: '评分' })
|
||||
rating: number;
|
||||
|
||||
@@ -46,8 +46,8 @@ 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({ type: 'enum', enum: ['pending', 'visible', 'hidden'], default: 'pending', comment: '状态:pending-待审核, visible-已通过, hidden-已隐藏' })
|
||||
status: 'pending' | 'visible' | 'hidden';
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@@ -27,9 +27,42 @@ export class ApplyMerchantDto {
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '营业执照不能为空' })
|
||||
businessLicense: string;
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '门店营业执照不能为空' })
|
||||
storeLicense: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
hotelImages?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '签约类型不能为空' })
|
||||
@IsEnum(['personal', 'company'], { message: '签约类型必须是personal或company' })
|
||||
contractType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
idCardFront?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
idCardBack?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
legalIdCardFront?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
legalIdCardBack?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
businessLicense?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -39,9 +72,38 @@ export class ApplyMerchantDto {
|
||||
@IsString()
|
||||
legalPerson?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '账户类型不能为空' })
|
||||
@IsEnum(['company', 'personal'], { message: '账户类型必须是company或personal' })
|
||||
accountType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
accountName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '银行账号不能为空' })
|
||||
bankAccount: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '开户银行不能为空' })
|
||||
bankName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankLicense?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountIdCardFront?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountIdCardBack?: string;
|
||||
}
|
||||
|
||||
export class UpdateMerchantDto {
|
||||
|
||||
@@ -77,3 +77,11 @@ export class ConfirmOrderDto {
|
||||
@IsString()
|
||||
rejectReason?: string;
|
||||
}
|
||||
|
||||
export class PayOrderDto {
|
||||
@IsNumber()
|
||||
orderId: number;
|
||||
|
||||
@IsEnum(['wechat', 'alipay', 'balance'])
|
||||
paymentMethod: 'wechat' | 'alipay' | 'balance';
|
||||
}
|
||||
|
||||
@@ -32,4 +32,10 @@ export class OrderAdminController {
|
||||
async findById(@Param('id') id: number) {
|
||||
return this.orderService.findById(id);
|
||||
}
|
||||
|
||||
@Get('service-fees/list')
|
||||
@ApiOperation({ summary: '服务费记录列表' })
|
||||
async getServiceFees(@Query() query: QueryOrderDto) {
|
||||
return this.orderService.getServiceFees(query);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Param,
|
||||
Body,
|
||||
@@ -36,6 +37,21 @@ export class OrderSellerController {
|
||||
return this.orderService.findByMerchant(merchant.id, query);
|
||||
}
|
||||
|
||||
@Post('detail')
|
||||
@ApiOperation({ summary: '商家订单详情' })
|
||||
async getDetail(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Body('orderId') orderId: number,
|
||||
) {
|
||||
const merchant = await this.merchantService.findBySellerId(sellerId);
|
||||
if (!merchant) throw new NotFoundException('店铺不存在');
|
||||
const order = await this.orderService.findById(orderId);
|
||||
if (order.merchantId !== merchant.id) {
|
||||
throw new NotFoundException('订单不存在');
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
@Put(':id/confirm')
|
||||
@ApiOperation({ summary: '确认订单' })
|
||||
async confirm(
|
||||
@@ -69,4 +85,15 @@ export class OrderSellerController {
|
||||
if (!merchant) throw new NotFoundException('店铺不存在');
|
||||
return this.orderService.checkin(merchant.id, id);
|
||||
}
|
||||
|
||||
@Put(':id/checkout')
|
||||
@ApiOperation({ summary: '确认离店' })
|
||||
async checkout(
|
||||
@CurrentSeller('sub') sellerId: number,
|
||||
@Param('id') id: number,
|
||||
) {
|
||||
const merchant = await this.merchantService.findBySellerId(sellerId);
|
||||
if (!merchant) throw new NotFoundException('店铺不存在');
|
||||
return this.orderService.checkout(merchant.id, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CurrentUser } from '@/common/decorators/current-user.decorator';
|
||||
import {
|
||||
CreateOrderDto,
|
||||
QueryOrderDto,
|
||||
PayOrderDto,
|
||||
} from './dto/order.dto';
|
||||
|
||||
@ApiTags('订单(用户)')
|
||||
@@ -57,4 +58,23 @@ export class OrderUserController {
|
||||
) {
|
||||
return this.orderService.cancel(userId, id, reason);
|
||||
}
|
||||
|
||||
@Post('pay')
|
||||
@ApiOperation({ summary: '支付订单' })
|
||||
async pay(
|
||||
@CurrentUser('sub') userId: number,
|
||||
@Body() dto: PayOrderDto,
|
||||
) {
|
||||
return this.orderService.pay(userId, dto.orderId, dto.paymentMethod);
|
||||
}
|
||||
|
||||
@Put(':id/refund')
|
||||
@ApiOperation({ summary: '申请退款' })
|
||||
async refund(
|
||||
@CurrentUser('sub') userId: number,
|
||||
@Param('id') id: number,
|
||||
@Body('reason') reason: string,
|
||||
) {
|
||||
return this.orderService.refund(userId, id, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,30 @@ export class OrderService {
|
||||
return { message: '已办理入住' };
|
||||
}
|
||||
|
||||
async checkout(merchantId: number, id: number) {
|
||||
const order = await this.findById(id);
|
||||
if (order.merchantId !== merchantId) throw new ForbiddenException('无权操作此订单');
|
||||
if (order.status !== 'checked_in') {
|
||||
throw new BadRequestException('当前订单状态不可确认离店');
|
||||
}
|
||||
|
||||
// 检查是否已到离店日期
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (order.checkOutDate > today) {
|
||||
throw new BadRequestException('未到离店日期,不可确认离店');
|
||||
}
|
||||
|
||||
await this.orderRepo.update(id, {
|
||||
status: 'completed',
|
||||
checkoutAt: new Date(),
|
||||
});
|
||||
|
||||
// 触发邀请返现
|
||||
this.activityService.handleOrderCompleted(id).catch(() => {});
|
||||
|
||||
return { message: '已确认离店,订单已完成' };
|
||||
}
|
||||
|
||||
async complete(id: number) {
|
||||
const order = await this.findById(id);
|
||||
if (order.status !== 'checked_in') {
|
||||
@@ -359,4 +383,40 @@ export class OrderService {
|
||||
const [list, total] = await qb.getManyAndCount();
|
||||
return { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };
|
||||
}
|
||||
|
||||
// 获取服务费记录列表(只查询已完成的订单)
|
||||
async getServiceFees(query: QueryOrderDto) {
|
||||
const { page = 1, pageSize = 10, orderNo, startDate, endDate } = query;
|
||||
const qb = this.orderRepo.createQueryBuilder('o')
|
||||
.leftJoinAndSelect('o.room', 'r')
|
||||
.leftJoinAndSelect('o.merchant', 'm')
|
||||
.where('o.status = :status', { status: 'completed' })
|
||||
.andWhere('o.service_fee > 0');
|
||||
|
||||
if (orderNo) qb.andWhere('o.order_no LIKE :orderNo', { orderNo: `%${orderNo}%` });
|
||||
if (startDate) qb.andWhere('o.checkout_at >= :startDate', { startDate });
|
||||
if (endDate) qb.andWhere('o.checkout_at <= :endDate', { endDate });
|
||||
|
||||
qb.orderBy('o.checkoutAt', 'DESC');
|
||||
qb.skip((page - 1) * pageSize).take(pageSize);
|
||||
|
||||
const [list, total] = await qb.getManyAndCount();
|
||||
|
||||
// 计算总服务费
|
||||
const totalResult = await this.orderRepo
|
||||
.createQueryBuilder('o')
|
||||
.select('SUM(o.service_fee)', 'totalServiceFee')
|
||||
.where('o.status = :status', { status: 'completed' })
|
||||
.andWhere('o.service_fee > 0')
|
||||
.getRawOne();
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
totalServiceFee: parseFloat(totalResult?.totalServiceFee || '0'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import { IsNotEmpty, IsOptional, IsNumber, IsString, IsBoolean, Min, Max, IsInt
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateReviewDto {
|
||||
@IsNotEmpty()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
orderId: number;
|
||||
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
rating: number;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ReviewService } from './review.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
|
||||
@Controller('seller/reviews')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('seller')
|
||||
export class ReviewSellerController {
|
||||
constructor(private readonly reviewService: ReviewService) {}
|
||||
|
||||
@Get()
|
||||
async getSellerReviews(
|
||||
@Request() req,
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
@Query('roomId') roomId?: string,
|
||||
) {
|
||||
const sellerId = req.user.sub;
|
||||
return this.reviewService.getSellerReviews(
|
||||
sellerId,
|
||||
parseInt(page),
|
||||
parseInt(limit),
|
||||
roomId ? parseInt(roomId) : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,13 @@ export class ReviewUserController {
|
||||
constructor(private readonly reviewService: ReviewService) {}
|
||||
|
||||
// 提交评价(需登录)
|
||||
@Post('order/:orderId')
|
||||
@Post('order')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async create(
|
||||
@Request() req,
|
||||
@Param('orderId') orderId: number,
|
||||
@Body() dto: CreateReviewDto,
|
||||
) {
|
||||
return this.reviewService.create(req.user.userId, Number(orderId), dto);
|
||||
return this.reviewService.create(req.user.sub, Number(dto.orderId), dto);
|
||||
}
|
||||
|
||||
// 查询评价列表(公开)
|
||||
|
||||
@@ -7,10 +7,11 @@ import { Room } from '@/entities/room.entity';
|
||||
import { ReviewService } from './review.service';
|
||||
import { ReviewUserController } from './review-user.controller';
|
||||
import { ReviewAdminController } from './review-admin.controller';
|
||||
import { ReviewSellerController } from './review-seller.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Review, Order, Merchant, Room])],
|
||||
controllers: [ReviewUserController, ReviewAdminController],
|
||||
controllers: [ReviewUserController, ReviewSellerController, ReviewAdminController],
|
||||
providers: [ReviewService],
|
||||
exports: [ReviewService],
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ReviewService {
|
||||
content: dto.content,
|
||||
images: dto.images,
|
||||
isAnonymous: dto.isAnonymous || false,
|
||||
status: 'visible',
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
await this.reviewRepo.save(review);
|
||||
@@ -83,6 +83,37 @@ export class ReviewService {
|
||||
return this.reviewRepo.findOne({ where: { orderId } });
|
||||
}
|
||||
|
||||
// 商家查询自己的评价列表(只显示已审核通过的)
|
||||
async getSellerReviews(sellerId: number, page: number = 1, limit: number = 10, roomId?: number) {
|
||||
// 先查询商家信息
|
||||
const merchant = await this.merchantRepo.findOne({ where: { sellerId } });
|
||||
if (!merchant) throw new NotFoundException('商家不存在');
|
||||
|
||||
const qb = this.reviewRepo
|
||||
.createQueryBuilder('r')
|
||||
.leftJoinAndSelect('r.order', 'o')
|
||||
.where('r.merchant_id = :merchantId', { merchantId: merchant.id })
|
||||
.andWhere('r.status = :status', { status: 'visible' });
|
||||
|
||||
if (roomId) {
|
||||
qb.andWhere('r.room_id = :roomId', { roomId });
|
||||
}
|
||||
|
||||
qb.orderBy('r.createdAt', 'DESC');
|
||||
const safePage = Number(page) || 1;
|
||||
const safeLimit = Number(limit) || 10;
|
||||
qb.skip((safePage - 1) * safeLimit).take(safeLimit);
|
||||
|
||||
const [list, total] = await qb.getManyAndCount();
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page: safePage,
|
||||
pageSize: safeLimit,
|
||||
totalPages: Math.ceil(total / safeLimit)
|
||||
};
|
||||
}
|
||||
|
||||
// 管理员评价列表
|
||||
async findAllForAdmin(query: QueryReviewDto) {
|
||||
const { page = 1, pageSize = 10, auditStatus } = query;
|
||||
|
||||
Reference in New Issue
Block a user