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:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user