dev
This commit is contained in:
@@ -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('房源管理(商家)')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user