feat(store): 门店套餐 v3.4.10 全端实现

新增 StorePackage 与变更审核流,覆盖总部直存、合伙/门店提审、C 端展示与套餐异议工单;同步 PRD/开发文档与 REQ 索引。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 19:30:59 +08:00
parent 7e3dc131ad
commit 3c19b7dd97
35 changed files with 2128 additions and 28 deletions
@@ -9,7 +9,7 @@ import {
export class CreateAfterSaleTicketDto {
@IsString()
@IsIn(['REFUND', 'RESHIPMENT', 'DAMAGE_RETURN', 'RETURN_REFUND'])
@IsIn(['REFUND', 'RESHIPMENT', 'DAMAGE_RETURN', 'RETURN_REFUND', 'PACKAGE_DISPUTE'])
ticketType: string;
@IsOptional()
@@ -21,6 +21,21 @@ export class CreateAfterSaleTicketDto {
evidenceUrls?: string[];
}
export class CreatePackageDisputeDto {
@IsString()
@IsNotEmpty()
storeId: string;
@IsOptional()
@IsString()
@MaxLength(512)
remark?: string;
@IsOptional()
@IsString()
redeemRecordId?: string;
}
export class CreateInvoiceDto {
@IsString()
@IsIn(['PERSONAL', 'ENTERPRISE'])
@@ -12,7 +12,7 @@ import {
PartnerProxyOrderPreviewDto,
} from './dto/partner-proxy-order.dto';
import { ManualShipOrderDto } from '../ops/dto/admin-mutate.dto';
import { CreateAfterSaleTicketDto, CreateInvoiceDto } from './dto/after-sale.dto';
import { CreateAfterSaleTicketDto, CreateInvoiceDto, CreatePackageDisputeDto } from './dto/after-sale.dto';
@Controller('trade/orders')
@UseGuards(JwtAuthGuard)
@@ -103,6 +103,17 @@ export class TradeController {
}
}
@Controller('trade/package-disputes')
@UseGuards(JwtAuthGuard)
export class TradePackageDisputeController {
constructor(private readonly tradeService: TradeService) {}
@Post()
create(@CurrentUser() user: AuthUser, @Body() body: CreatePackageDisputeDto) {
return this.tradeService.createPackageDispute(user.actorId, body);
}
}
@Controller('trade/after-sale-tickets')
@UseGuards(JwtAuthGuard)
export class TradeAfterSaleTicketController {
@@ -15,6 +15,7 @@ import {
PartnerReshipmentController,
TradeAfterSaleTicketController,
TradeInvoiceController,
TradePackageDisputeController,
} from './trade.controller';
import { TradeService } from './trade.service';
@@ -32,6 +33,7 @@ import { TradeService } from './trade.service';
],
controllers: [
TradeController,
TradePackageDisputeController,
TradeAfterSaleTicketController,
TradeInvoiceController,
PartnerOrderController,
@@ -730,6 +730,40 @@ export class TradeService {
return serializeBigInt({ ...ticket, orderNo: order.orderNo, orderStatus: order.status });
}
async createPackageDispute(
userId: bigint,
body: { storeId: string; remark?: string; redeemRecordId?: string },
) {
const storeId = BigInt(body.storeId);
const store = await this.prisma.store.findFirst({
where: { id: storeId, status: 'OPEN' },
});
if (!store) throw new NotFoundException('门店不存在');
const pending = await this.prisma.commonTicket.findFirst({
where: {
ticketType: 'PACKAGE_DISPUTE',
refType: 'STORE',
refId: storeId,
status: { in: ['PENDING', 'OPEN'] },
},
});
if (pending) throw new BadRequestException('该门店套餐异议已在处理中');
const extraJson: Record<string, unknown> = { userId: userId.toString() };
if (body.redeemRecordId) {
extraJson.redeemRecordId = body.redeemRecordId;
}
return this.ticketService.create({
ticketType: 'PACKAGE_DISPUTE',
refType: 'STORE',
refId: storeId.toString(),
remark: body.remark ?? '用户对门店套餐有异议',
extraJson,
});
}
private generateInvoiceNo() {
return `INV${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
}