feat: WC2026 赛事 seed、生产上线初始化脚本与目录归档

重构 seed 为 WC2026 72 场小组赛与 48 强优胜盘;新增 production 模式仅保留 admin 与赛事示例;提供 prod-init-db 全量重置脚本;管理端 i18n 分包与赛事归档能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 18:17:00 +08:00
parent 8f14e85ebd
commit e7e938f261
94 changed files with 12332 additions and 976 deletions

View File

@@ -5,6 +5,7 @@ import { resolveTranslationFallback } from '@thebet365/shared';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { WalletService } from '../ledger/wallet.service';
import { appBadRequest } from '../../shared/common/app-error';
import { deleteUploadFileByUrl } from '../../shared/uploads/delete-upload-file';
function generateOrderNo(): string {
const ts = Date.now().toString(36).toUpperCase();
@@ -12,6 +13,9 @@ function generateOrderNo(): string {
return `DEP${ts}${rand}`;
}
/** 已通过充值订单允许撤回的时间窗口 */
const DEPOSIT_REVOKE_WINDOW_MS = 5 * 60 * 1000;
@Injectable()
export class DepositService {
constructor(
@@ -21,6 +25,20 @@ export class DepositService {
// ============ Payment Methods (Admin CRUD) ============
/** isActive 与 showOnPlayer 合并为同一开关DB 两列保持同步以兼容旧数据。 */
private normalizePaymentMethodActive(data: {
isActive?: boolean;
showOnPlayer?: boolean;
}): { isActive?: boolean; showOnPlayer?: boolean } {
if (data.isActive !== undefined) {
return { isActive: data.isActive, showOnPlayer: data.isActive };
}
if (data.showOnPlayer !== undefined) {
return { isActive: data.showOnPlayer, showOnPlayer: data.showOnPlayer };
}
return {};
}
async createPaymentMethod(data: {
methodType: string;
bankName?: string;
@@ -38,6 +56,7 @@ export class DepositService {
bankName?: Record<string, string>;
};
}) {
const active = data.isActive ?? data.showOnPlayer ?? true;
const method = await this.prisma.paymentMethod.create({
data: {
methodType: data.methodType,
@@ -48,8 +67,8 @@ export class DepositService {
qrCodeUrl: data.qrCodeUrl,
displayName: data.displayName,
sortOrder: data.sortOrder ?? 0,
isActive: data.isActive ?? true,
showOnPlayer: data.showOnPlayer ?? true,
isActive: active,
showOnPlayer: active,
createdBy: data.createdBy,
},
});
@@ -78,9 +97,10 @@ export class DepositService {
},
) {
const { translations, ...rest } = data;
const activePatch = this.normalizePaymentMethodActive(rest);
const method = await this.prisma.paymentMethod.update({
where: { id },
data: rest,
data: { ...rest, ...activePatch },
});
if (translations) {
await this.upsertPaymentMethodTranslations(id, translations);
@@ -128,7 +148,6 @@ export class DepositService {
async listPlayerPaymentMethods(methodType?: string, locale?: string) {
const where: Prisma.PaymentMethodWhereInput = {
isActive: true,
showOnPlayer: true,
};
if (methodType) {
where.methodType = methodType;
@@ -438,4 +457,124 @@ export class DepositService {
return { success: true };
}
private async reverseApprovedDepositCredit(
order: {
playerId: bigint;
orderNo: string;
approvedAmount: Decimal | null;
amount: Decimal;
},
operatorId: bigint,
remark: string,
) {
const credit = order.approvedAmount ?? order.amount;
await this.wallet.withdraw(
order.playerId,
credit,
operatorId,
remark,
order.orderNo,
'PLAYER_DEPOSIT_REVERSAL',
);
}
/** 已拒绝恢复待审核已通过5 分钟内):作废期间待结算注单并扣回入账 */
async reopenDepositOrderForReview(orderId: bigint, operatorId: bigint) {
const order = await this.prisma.depositOrder.findUnique({ where: { id: orderId } });
if (!order) throw appBadRequest('ORDER_NOT_FOUND');
if (order.status === 'PENDING') throw appBadRequest('ORDER_ALREADY_PENDING');
if (order.status === 'REJECTED') {
await this.prisma.depositOrder.update({
where: { id: orderId },
data: {
status: 'PENDING',
approvedAmount: null,
reviewerId: null,
reviewedAt: null,
rejectReason: null,
remark: null,
},
});
return { success: true };
}
if (order.status !== 'APPROVED') {
throw appBadRequest('ORDER_NOT_APPROVED');
}
if (!order.reviewedAt || Date.now() - order.reviewedAt.getTime() > DEPOSIT_REVOKE_WINDOW_MS) {
throw appBadRequest('DEPOSIT_REVOKE_WINDOW_EXPIRED');
}
const reviewedAt = order.reviewedAt;
return this.prisma.$transaction(async (tx) => {
const betsAfterReview = await tx.bet.findMany({
where: {
userId: order.playerId,
placedAt: { gte: reviewedAt },
status: { not: 'VOID' },
},
});
const settled = betsAfterReview.filter((b) => b.status !== 'PENDING');
if (settled.length > 0) {
throw appBadRequest('DEPOSIT_REVOKE_SETTLED_BETS');
}
for (const bet of betsAfterReview) {
await this.wallet.settleBet(
bet.userId,
bet.stake,
bet.stake,
bet.betNo,
'VOID',
tx,
);
await tx.bet.update({
where: { id: bet.id },
data: { status: 'VOID', actualReturn: bet.stake, settledAt: new Date() },
});
}
const credit = order.approvedAmount ?? order.amount;
await this.wallet.withdraw(
order.playerId,
credit,
operatorId,
`Revoke approved deposit ${order.orderNo}`,
order.orderNo,
'PLAYER_DEPOSIT_REVERSAL',
tx,
);
await tx.depositOrder.update({
where: { id: orderId },
data: {
status: 'PENDING',
approvedAmount: null,
reviewerId: null,
reviewedAt: null,
rejectReason: null,
remark: null,
},
});
return { success: true, voidedBets: betsAfterReview.length };
});
}
/** 删除充值订单记录及截图(不调整玩家钱包或注单,与撤销无关) */
async deleteDepositOrder(orderId: bigint, _operatorId: bigint) {
const order = await this.prisma.depositOrder.findUnique({ where: { id: orderId } });
if (!order) throw appBadRequest('ORDER_NOT_FOUND');
const screenshotUrl = order.screenshotUrl;
await this.prisma.depositOrder.delete({ where: { id: orderId } });
await deleteUploadFileByUrl(screenshotUrl);
return { success: true };
}
}