This commit is contained in:
2026-04-23 00:48:07 +08:00
parent 7473fdcc03
commit 3284321919
21 changed files with 2703 additions and 31 deletions
+6
View File
@@ -78,6 +78,12 @@ export class Order {
@Column({ name: 'contact_phone', length: 20, comment: '联系人手机' })
contactPhone: string;
@Column({ name: 'contact_id_card', length: 18, nullable: true, comment: '联系人身份证号' })
contactIdCard?: string;
@Column({ name: 'guest_count', type: 'tinyint', unsigned: true, default: 1, comment: '入住人数' })
guestCount: number;
@Column({ length: 500, default: '', comment: '备注' })
remark: string;
@@ -17,6 +17,10 @@ export class CreateOrderDto {
@IsNumber()
roomCount?: number = 1;
@IsOptional()
@IsNumber()
guestCount?: number = 1;
@IsOptional()
@IsNumber()
couponId?: number;
@@ -29,6 +33,10 @@ export class CreateOrderDto {
@IsNotEmpty({ message: '联系人手机不能为空' })
contactPhone: string;
@IsOptional()
@IsString()
contactIdCard?: string;
@IsOptional()
@IsString()
remark?: string;
@@ -48,10 +48,14 @@ export class UserOrderController {
return this.orderService.findByUser(userId, query);
}
@Get(':id')
@ApiOperation({ summary: '订单详情' })
async findById(@Param('id') id: number) {
return this.orderService.findById(id);
@Put(':id/pay')
@ApiOperation({ summary: '模拟支付' })
async pay(
@CurrentUser('sub') userId: number,
@Param('id') id: number,
@Body('paymentMethod') paymentMethod: 'wechat' | 'alipay' | 'balance',
) {
return this.orderService.pay(userId, id, paymentMethod || 'wechat');
}
@Put(':id/cancel')
@@ -63,6 +67,22 @@ export class UserOrderController {
) {
return this.orderService.cancel(userId, id, reason);
}
@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);
}
@Get(':id')
@ApiOperation({ summary: '订单详情' })
async findById(@Param('id') id: number) {
return this.orderService.findById(id);
}
}
@ApiTags('订单管理(商家)')
@@ -86,6 +106,12 @@ export class MerchantOrderController {
return this.orderService.findByMerchant(merchant.id, query);
}
@Get(':id')
@ApiOperation({ summary: '商家订单详情' })
async findById(@Param('id') id: number) {
return this.orderService.findById(id);
}
@Put(':id/confirm')
@ApiOperation({ summary: '确认订单' })
async confirm(
@@ -119,6 +145,29 @@ export class MerchantOrderController {
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.checkin(merchant.id, id);
}
@Put(':id/approve-refund')
@ApiOperation({ summary: '同意退款' })
async approveRefund(
@CurrentSeller('sub') sellerId: number,
@Param('id') id: number,
) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.approveRefund(merchant.id, id);
}
@Put(':id/reject-refund')
@ApiOperation({ summary: '拒绝退款' })
async rejectRefund(
@CurrentSeller('sub') sellerId: number,
@Param('id') id: number,
@Body('reason') reason: string,
) {
const merchant = await this.merchantService.findBySellerId(sellerId);
if (!merchant) throw new NotFoundException('店铺不存在');
return this.orderService.rejectRefund(merchant.id, id, reason);
}
}
@ApiTags('订单管理(管理员)')
@@ -55,6 +55,7 @@ export class OrderService {
checkOutDate: dto.checkOutDate,
nights,
roomCount,
guestCount: dto.guestCount || 1,
roomPrice: room.price,
roomAmount,
serviceFee,
@@ -63,6 +64,7 @@ export class OrderService {
payAmount,
contactName: dto.contactName,
contactPhone: dto.contactPhone,
contactIdCard: dto.contactIdCard || undefined,
remark: dto.remark || '',
paymentMethod: dto.paymentMethod || null,
});
@@ -187,6 +189,103 @@ export class OrderService {
return { message: '订单已完成' };
}
// 用户申请退款(待确认、待入住状态)
async refund(userId: number, id: number, reason: string) {
const order = await this.findById(id);
if (order.userId !== userId) throw new ForbiddenException('无权操作此订单');
if (!['pending_confirm', 'pending_checkin'].includes(order.status)) {
throw new BadRequestException('当前订单状态不可申请退款');
}
await this.orderRepo.update(id, {
status: 'refunding',
cancelReason: reason,
});
return { message: '退款申请已提交' };
}
// 商家同意退款
async approveRefund(merchantId: number, id: number) {
const order = await this.findById(id);
if (order.merchantId !== merchantId) throw new ForbiddenException('无权操作此订单');
if (order.status !== 'refunding') {
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),
});
}
}
await this.orderRepo.update(id, {
status: 'refunded',
refundAmount: order.payAmount,
refundAt: new Date(),
cancelledAt: new Date(),
});
return { message: '退款成功' };
}
// 商家拒绝退款
async rejectRefund(merchantId: number, id: number, reason: string) {
const order = await this.findById(id);
if (order.merchantId !== merchantId) throw new ForbiddenException('无权操作此订单');
if (order.status !== 'refunding') {
throw new BadRequestException('当前订单状态不可处理退款');
}
await this.orderRepo.update(id, {
status: 'cancelled',
cancelReason: `退款被拒: ${reason}`,
cancelledAt: new Date(),
});
return { message: '已拒绝退款' };
}
async pay(userId: number, id: number, paymentMethod: 'wechat' | 'alipay' | 'balance') {
const order = await this.findById(id);
if (order.userId !== userId) throw new ForbiddenException('无权操作此订单');
if (order.status !== 'pending_pay') {
throw new BadRequestException('当前订单状态不可支付');
}
// 模拟支付成功
const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 10000)}`;
await this.orderRepo.update(id, {
status: 'pending_confirm',
paymentMethod,
paymentNo,
paidAt: new Date(),
});
// 扣减房态库存
const 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: calendar.sold + order.roomCount,
});
}
}
return { message: '支付成功', paymentNo };
}
async findAll(query: QueryOrderDto) {
const { page = 1, pageSize = 10, status, orderNo, startDate, endDate } = query;
const qb = this.orderRepo.createQueryBuilder('o')
@@ -17,12 +17,16 @@ import { JwtAuthGuard, RolesGuard } from '@/common';
import { Roles } from '@/common/decorators/roles.decorator';
import { CurrentSeller } from '@/common/decorators/current-seller.decorator';
import { MerchantService } from '../merchant/merchant.service';
import { RoomCalendarService } from '../room-calendar/room-calendar.service';
import { CreateRoomDto, UpdateRoomDto, QueryRoomDto } from './dto/room.dto';
@ApiTags('房源')
@Controller('rooms')
export class RoomPublicController {
constructor(private readonly roomService: RoomService) {}
constructor(
private readonly roomService: RoomService,
private readonly calendarService: RoomCalendarService,
) {}
@Get()
@ApiOperation({ summary: '房源列表(公开)' })
@@ -35,6 +39,16 @@ export class RoomPublicController {
async findById(@Param('id') id: number) {
return this.roomService.findById(id);
}
@Get(':id/calendar')
@ApiOperation({ summary: '房源日历(公开)' })
async getCalendar(
@Param('id') id: number,
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
) {
return this.calendarService.getCalendar(Number(id), { startDate, endDate });
}
}
@ApiTags('房源管理(商家)')
+2 -1
View File
@@ -9,9 +9,10 @@ import {
AdminRoomController,
} from './room.controller';
import { MerchantModule } from '../merchant/merchant.module';
import { RoomCalendarModule } from '../room-calendar/room-calendar.module';
@Module({
imports: [TypeOrmModule.forFeature([Room, RoomCalendar]), MerchantModule],
imports: [TypeOrmModule.forFeature([Room, RoomCalendar]), MerchantModule, RoomCalendarModule],
controllers: [
RoomPublicController,
MerchantRoomController,
+15 -13
View File
@@ -135,8 +135,8 @@ export class RoomService {
qb.skip((safePage - 1) * safePageSize).take(safePageSize);
const [list, total] = await qb.getManyAndCount();
// 日期筛选:检查时间区间内是否有库存
let filteredList = list;
// 日期筛选:检查时间区间内是否有库存,添加 isAvailable 标记
let resultList: (Room & { isAvailable: boolean })[] = list.map(r => ({ ...r, isAvailable: true }));
if (checkIn && checkOut && list.length > 0) {
const roomIds = list.map(r => r.id);
const checkInDate = new Date(checkIn);
@@ -164,30 +164,32 @@ export class RoomService {
}
}
// 计算需要的入住天数
const daysNeeded = Math.ceil((checkOutDate.getTime() - checkInDate.getTime()) / (1000 * 60 * 60 * 24));
// 过滤出日期区间内每天都有库存的房间
filteredList = list.filter(room => {
// 为每个房间标记是否可用(日期区间内每天都有库存)
resultList = list.map(room => {
const availableDays = roomAvailableDays[room.id];
if (!availableDays) return false;
if (!availableDays) {
return { ...room, isAvailable: false };
}
// 检查每一天是否都有库存
for (let d = new Date(checkInDate); d < checkOutDate; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];
if (!availableDays.has(dateStr)) {
return false;
return { ...room, isAvailable: false };
}
}
return true;
return { ...room, isAvailable: true };
});
}
// 有库存的排前面,满房的排后面
resultList.sort((a, b) => (a.isAvailable === b.isAvailable ? 0 : a.isAvailable ? -1 : 1));
return {
list: filteredList,
total: filteredList.length,
list: resultList,
total,
page: safePage,
pageSize: safePageSize,
totalPages: Math.ceil(filteredList.length / safePageSize),
totalPages: Math.ceil(total / safePageSize),
};
}