feat: 开户备注、账单展示优化与后台代理管理增强
- 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示 - 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分 - 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端 - 后台代理/玩家表格操作栏重构,充值订单增加备注列 - 前台个人中心、充值、账单与验证码组件体验优化 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -83,6 +83,7 @@ export class WalletService {
|
||||
operatorId: bigint,
|
||||
remark?: string,
|
||||
referenceId?: string,
|
||||
transactionType = 'MANUAL_WITHDRAW',
|
||||
) {
|
||||
const amt = new Decimal(amount);
|
||||
if (amt.lte(0)) throw appBadRequest('AMOUNT_MUST_BE_POSITIVE');
|
||||
@@ -106,7 +107,7 @@ export class WalletService {
|
||||
transactionId: generateTransactionId(),
|
||||
userId,
|
||||
walletId: w.id,
|
||||
transactionType: 'MANUAL_WITHDRAW',
|
||||
transactionType,
|
||||
amount: amt.neg(),
|
||||
balanceBefore,
|
||||
balanceAfter,
|
||||
@@ -260,26 +261,178 @@ export class WalletService {
|
||||
return this.prisma.$transaction(run);
|
||||
}
|
||||
|
||||
private static readonly DEPOSIT_TX_TYPES = [
|
||||
'MANUAL_DEPOSIT',
|
||||
'ADMIN_DEPOSIT',
|
||||
'AGENT_DEPOSIT',
|
||||
'INITIAL_DEPOSIT',
|
||||
'DEPOSIT',
|
||||
'MANUAL_ADJUST',
|
||||
'PLAYER_DEPOSIT',
|
||||
] as const;
|
||||
|
||||
private static readonly WITHDRAW_TX_TYPES = [
|
||||
'MANUAL_WITHDRAW',
|
||||
'ADMIN_WITHDRAW',
|
||||
'AGENT_WITHDRAW',
|
||||
'WITHDRAW',
|
||||
] as const;
|
||||
|
||||
private static readonly SYSTEM_REMARKS = new Set([
|
||||
'管理员上分',
|
||||
'管理员下分',
|
||||
'代理上分',
|
||||
'代理下分',
|
||||
'开户初始余额',
|
||||
'Resettlement adjustment',
|
||||
]);
|
||||
|
||||
private resolveDisplayType(
|
||||
transactionType: string,
|
||||
operatorType?: string | null,
|
||||
): string {
|
||||
const type = transactionType.toUpperCase();
|
||||
if (type === 'INITIAL_DEPOSIT') {
|
||||
if (operatorType === 'AGENT') return 'AGENT_DEPOSIT';
|
||||
return 'ADMIN_DEPOSIT';
|
||||
}
|
||||
if (
|
||||
[
|
||||
'ADMIN_DEPOSIT',
|
||||
'AGENT_DEPOSIT',
|
||||
'ADMIN_WITHDRAW',
|
||||
'AGENT_WITHDRAW',
|
||||
'PLAYER_DEPOSIT',
|
||||
].includes(type)
|
||||
) {
|
||||
return type;
|
||||
}
|
||||
if (type === 'MANUAL_DEPOSIT') {
|
||||
if (operatorType === 'ADMIN') return 'ADMIN_DEPOSIT';
|
||||
if (operatorType === 'AGENT') return 'AGENT_DEPOSIT';
|
||||
return type;
|
||||
}
|
||||
if (type === 'MANUAL_WITHDRAW') {
|
||||
if (operatorType === 'ADMIN') return 'ADMIN_WITHDRAW';
|
||||
if (operatorType === 'AGENT') return 'AGENT_WITHDRAW';
|
||||
return type;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private isCustomRemark(remark: string | null | undefined): boolean {
|
||||
const r = remark?.trim();
|
||||
if (!r) return false;
|
||||
if (WalletService.SYSTEM_REMARKS.has(r)) return false;
|
||||
if (r.startsWith('Cashback batch ')) return false;
|
||||
if (r.startsWith('Deposit order ')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private buildPlayerTxSummary(
|
||||
tx: {
|
||||
transactionType: string;
|
||||
referenceType: string | null;
|
||||
referenceId: string | null;
|
||||
remark: string | null;
|
||||
},
|
||||
depositMethodName?: string | null,
|
||||
): string | null {
|
||||
const type = tx.transactionType.toUpperCase();
|
||||
if (tx.referenceType === 'BET' && tx.referenceId) {
|
||||
return tx.referenceId;
|
||||
}
|
||||
if (type === 'CASHBACK' || type === 'CASHBACK_DEPOSIT') {
|
||||
return tx.referenceId ?? null;
|
||||
}
|
||||
if (type === 'PLAYER_DEPOSIT') {
|
||||
const parts = [depositMethodName?.trim(), tx.referenceId?.trim()].filter(Boolean);
|
||||
return parts.length ? parts.join(' · ') : null;
|
||||
}
|
||||
if (this.isCustomRemark(tx.remark)) return tx.remark!.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
private resolveSummaryKind(remark: string | null | undefined): 'opening_bonus' | null {
|
||||
const r = remark?.trim();
|
||||
if (r === '开户初始余额') return 'opening_bonus';
|
||||
return null;
|
||||
}
|
||||
|
||||
private async enrichPlayerTransactions(
|
||||
rows: Array<{
|
||||
id: bigint;
|
||||
transactionId: string;
|
||||
transactionType: string;
|
||||
amount: Decimal;
|
||||
balanceBefore: Decimal;
|
||||
balanceAfter: Decimal;
|
||||
frozenBefore: Decimal;
|
||||
frozenAfter: Decimal;
|
||||
referenceType: string | null;
|
||||
referenceId: string | null;
|
||||
remark: string | null;
|
||||
operatorId: bigint | null;
|
||||
createdAt: Date;
|
||||
}>,
|
||||
) {
|
||||
const operatorIds = [
|
||||
...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
const operators =
|
||||
operatorIds.length > 0
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: operatorIds } },
|
||||
select: { id: true, userType: true },
|
||||
})
|
||||
: [];
|
||||
const operatorTypeById = new Map(operators.map((o) => [o.id.toString(), o.userType]));
|
||||
|
||||
const depositMethodByRowId = await this.resolveDepositMethodsForRows(rows);
|
||||
|
||||
return rows.map((row) => {
|
||||
const operatorType = row.operatorId
|
||||
? operatorTypeById.get(row.operatorId.toString())
|
||||
: null;
|
||||
const displayType = this.resolveDisplayType(row.transactionType, operatorType);
|
||||
const depositMethod = depositMethodByRowId.get(row.id.toString());
|
||||
const summary = this.buildPlayerTxSummary(
|
||||
row,
|
||||
depositMethod?.depositMethodName,
|
||||
);
|
||||
const summaryKind = summary ? null : this.resolveSummaryKind(row.remark);
|
||||
return {
|
||||
transactionId: row.transactionId,
|
||||
transactionType: row.transactionType,
|
||||
displayType,
|
||||
summaryKind,
|
||||
amount: row.amount.toString(),
|
||||
balanceBefore: row.balanceBefore.toString(),
|
||||
balanceAfter: row.balanceAfter.toString(),
|
||||
frozenBefore: row.frozenBefore.toString(),
|
||||
frozenAfter: row.frozenAfter.toString(),
|
||||
referenceType: row.referenceType,
|
||||
referenceId: row.referenceId,
|
||||
remark: row.remark,
|
||||
summary,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
betNo: row.referenceType === 'BET' ? row.referenceId : null,
|
||||
cashbackBatchNo:
|
||||
row.transactionType === 'CASHBACK' || row.transactionType === 'CASHBACK_DEPOSIT'
|
||||
? row.referenceId
|
||||
: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getTransactionDetail(userId: bigint, transactionId: string) {
|
||||
const tx = await this.prisma.walletTransaction.findFirst({
|
||||
where: { userId, transactionId },
|
||||
});
|
||||
if (!tx) return null;
|
||||
|
||||
return {
|
||||
transactionId: tx.transactionId,
|
||||
transactionType: tx.transactionType,
|
||||
amount: tx.amount.toString(),
|
||||
balanceBefore: tx.balanceBefore.toString(),
|
||||
balanceAfter: tx.balanceAfter.toString(),
|
||||
frozenBefore: tx.frozenBefore.toString(),
|
||||
frozenAfter: tx.frozenAfter.toString(),
|
||||
referenceType: tx.referenceType,
|
||||
referenceId: tx.referenceId,
|
||||
remark: tx.remark,
|
||||
createdAt: tx.createdAt.toISOString(),
|
||||
betNo: tx.referenceType === 'BET' ? tx.referenceId : null,
|
||||
};
|
||||
const [enriched] = await this.enrichPlayerTransactions([tx]);
|
||||
return enriched;
|
||||
}
|
||||
|
||||
async getTransactions(userId: bigint, page = 1, pageSize = 20, typeFilter?: string) {
|
||||
@@ -287,9 +440,9 @@ export class WalletService {
|
||||
|
||||
let typeWhere: Record<string, unknown> = {};
|
||||
if (typeFilter === 'deposit') {
|
||||
typeWhere = { transactionType: { in: ['MANUAL_DEPOSIT', 'DEPOSIT', 'MANUAL_ADJUST', 'PLAYER_DEPOSIT'] } };
|
||||
typeWhere = { transactionType: { in: [...WalletService.DEPOSIT_TX_TYPES] } };
|
||||
} else if (typeFilter === 'withdraw') {
|
||||
typeWhere = { transactionType: { in: ['MANUAL_WITHDRAW', 'WITHDRAW'] } };
|
||||
typeWhere = { transactionType: { in: [...WalletService.WITHDRAW_TX_TYPES] } };
|
||||
} else if (typeFilter === 'bet') {
|
||||
typeWhere = { transactionType: { in: ['BET_FREEZE', 'BET_DEDUCT', 'BET_SETTLE_WIN', 'BET_SETTLE_LOSE', 'BET_SETTLE_PUSH', 'BET_WIN', 'BET_REFUND', 'BET_VOID', 'BET_VOID_REFUND', 'RESETTLE_REVERSE'] } };
|
||||
} else if (typeFilter === 'cashback') {
|
||||
@@ -306,16 +459,21 @@ export class WalletService {
|
||||
}),
|
||||
this.prisma.walletTransaction.count({ where }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
return {
|
||||
items: await this.enrichPlayerTransactions(items),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
private walletTypeCategoryWhere(category?: string): Prisma.WalletTransactionWhereInput {
|
||||
const cat = category?.trim();
|
||||
if (cat === 'deposit') {
|
||||
return { transactionType: { in: ['MANUAL_DEPOSIT', 'DEPOSIT', 'MANUAL_ADJUST', 'PLAYER_DEPOSIT'] } };
|
||||
return { transactionType: { in: [...WalletService.DEPOSIT_TX_TYPES] } };
|
||||
}
|
||||
if (cat === 'withdraw') {
|
||||
return { transactionType: { in: ['MANUAL_WITHDRAW', 'WITHDRAW'] } };
|
||||
return { transactionType: { in: [...WalletService.WITHDRAW_TX_TYPES] } };
|
||||
}
|
||||
if (cat === 'bet') {
|
||||
return {
|
||||
@@ -343,6 +501,9 @@ export class WalletService {
|
||||
|
||||
private static readonly DEPOSIT_RECHARGE_TYPES = new Set([
|
||||
'MANUAL_DEPOSIT',
|
||||
'ADMIN_DEPOSIT',
|
||||
'AGENT_DEPOSIT',
|
||||
'INITIAL_DEPOSIT',
|
||||
'DEPOSIT',
|
||||
'PLAYER_DEPOSIT',
|
||||
]);
|
||||
@@ -644,7 +805,15 @@ export class WalletService {
|
||||
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const transferTypes = ['MANUAL_DEPOSIT', 'MANUAL_WITHDRAW'];
|
||||
const transferTypes = [
|
||||
'MANUAL_DEPOSIT',
|
||||
'MANUAL_WITHDRAW',
|
||||
'ADMIN_DEPOSIT',
|
||||
'ADMIN_WITHDRAW',
|
||||
'AGENT_DEPOSIT',
|
||||
'AGENT_WITHDRAW',
|
||||
'INITIAL_DEPOSIT',
|
||||
];
|
||||
const where: Prisma.WalletTransactionWhereInput = {
|
||||
transactionType: params.transactionType?.trim()
|
||||
? params.transactionType.trim()
|
||||
|
||||
Reference in New Issue
Block a user