diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index 57e8a9fa..e4cea227 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -273,4 +273,32 @@ export class InternalWalletController { remark: dto.remark, }); } + + // =============== 特殊扣减 API =============== + + @Post('special-deduction/execute') + @Public() + @ApiOperation({ summary: '执行特殊扣减(内部API) - 用户确认扣减操作' }) + @ApiResponse({ status: 200, description: '扣减结果' }) + async executeSpecialDeduction( + @Body() dto: { + accountSequence: string; + pendingActionId: string; + amount: number; + reason: string; + }, + ) { + this.logger.log(`========== special-deduction/execute 请求 ==========`); + this.logger.log(`请求参数: ${JSON.stringify(dto)}`); + + const result = await this.walletService.executeSpecialDeduction({ + accountSequence: dto.accountSequence, + amount: dto.amount, + memo: dto.reason, + operatorId: `pending-action:${dto.pendingActionId}`, + }); + + this.logger.log(`特殊扣减结果: ${JSON.stringify(result)}`); + return result; + } } diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 86decef1..9bb8a771 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -2331,6 +2331,8 @@ export class WalletApplicationService { TRANSFER_TO_POOL: '转入矿池', SWAP_EXECUTED: '兑换执行', WITHDRAWAL: '提现', + FIAT_WITHDRAWAL: '法币提现', + SPECIAL_DEDUCTION: '特殊扣减', TRANSFER_IN: '转入', TRANSFER_OUT: '转出', FREEZE: '冻结', @@ -2339,4 +2341,151 @@ export class WalletApplicationService { }; return nameMap[entryType] || entryType; } + + // =============== 特殊扣减 (管理员操作) =============== + + /** + * 特殊扣减命令 + */ + async executeSpecialDeduction(command: { + accountSequence: string; + amount: number; + memo: string; + operatorId: string; + }): Promise<{ success: boolean; orderNo: string; newBalance: number }> { + const MAX_RETRIES = 3; + let retries = 0; + + while (retries < MAX_RETRIES) { + try { + return await this.doExecuteSpecialDeduction(command); + } catch (error) { + if (this.isOptimisticLockError(error)) { + retries++; + this.logger.warn(`[executeSpecialDeduction] Optimistic lock conflict for ${command.accountSequence}, retry ${retries}/${MAX_RETRIES}`); + if (retries >= MAX_RETRIES) { + this.logger.error(`[executeSpecialDeduction] Max retries exceeded for ${command.accountSequence}`); + throw error; + } + await this.sleep(50 * retries); + } else { + throw error; + } + } + } + + throw new Error('Unexpected: exited retry loop without result'); + } + + private async doExecuteSpecialDeduction(command: { + accountSequence: string; + amount: number; + memo: string; + operatorId: string; + }): Promise<{ success: boolean; orderNo: string; newBalance: number }> { + this.logger.log(`[executeSpecialDeduction] 执行特殊扣减: accountSequence=${command.accountSequence}, amount=${command.amount}`); + + // 验证金额 + if (command.amount <= 0) { + throw new BadRequestException('扣减金额必须大于 0'); + } + + // 查找钱包 + const wallet = await this.walletRepo.findByAccountSequence(command.accountSequence); + if (!wallet) { + throw new WalletNotFoundError(`accountSequence: ${command.accountSequence}`); + } + + // 验证余额是否足够 + const deductAmount = new (require('decimal.js').default)(command.amount); + if (wallet.balances.usdt.available.value < command.amount) { + throw new BadRequestException( + `余额不足: 需要 ${command.amount} 绿积分, 当前可用 ${wallet.balances.usdt.available.value} 绿积分`, + ); + } + + // 生成订单号 + const orderNo = this.generateSpecialDeductionOrderNo(); + + // 使用事务处理 + let newBalance = 0; + await this.prisma.$transaction(async (tx) => { + const walletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: command.accountSequence }, + }); + + if (!walletRecord) { + throw new Error(`Wallet not found: ${command.accountSequence}`); + } + + const currentAvailable = new (require('decimal.js').default)(walletRecord.usdtAvailable.toString()); + const currentVersion = walletRecord.version; + + if (currentAvailable.lessThan(deductAmount)) { + throw new BadRequestException(`余额不足: 需要 ${command.amount} 绿积分, 当前可用 ${currentAvailable} 绿积分`); + } + + const newAvailable = currentAvailable.minus(deductAmount); + newBalance = newAvailable.toNumber(); + + // 乐观锁更新 + const updateResult = await tx.walletAccount.updateMany({ + where: { + id: walletRecord.id, + version: currentVersion, + }, + data: { + usdtAvailable: newAvailable, + version: currentVersion + 1, + updatedAt: new Date(), + }, + }); + + if (updateResult.count === 0) { + throw new OptimisticLockError(`Optimistic lock conflict for wallet ${walletRecord.id}`); + } + + // 记录账本流水 + await tx.ledgerEntry.create({ + data: { + accountSequence: command.accountSequence, + userId: walletRecord.userId, + entryType: LedgerEntryType.SPECIAL_DEDUCTION, + amount: deductAmount.negated(), + assetType: 'USDT', + balanceAfter: newAvailable, + refOrderId: orderNo, + memo: `特殊扣减: ${command.memo}`, + payloadJson: { + operatorId: command.operatorId, + originalAmount: command.amount, + reason: command.memo, + executedAt: new Date().toISOString(), + }, + }, + }); + + this.logger.log(`[executeSpecialDeduction] 成功扣减 ${command.amount} 绿积分, 新余额: ${newBalance}`); + }); + + // 清除缓存 + await this.walletCacheService.invalidateWallet(wallet.userId.value); + + return { + success: true, + orderNo, + newBalance, + }; + } + + /** + * 生成特殊扣减订单号 + */ + private generateSpecialDeductionOrderNo(): string { + const now = new Date(); + const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); + const timeStr = now.toISOString().slice(11, 19).replace(/:/g, ''); + const random = Math.random().toString(36).substring(2, 8).toUpperCase(); + return `SD${dateStr}${timeStr}${random}`; + } } diff --git a/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts b/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts index 8e9ab3e6..fdaf9093 100644 --- a/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts +++ b/backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts @@ -12,6 +12,7 @@ export enum LedgerEntryType { SWAP_EXECUTED = 'SWAP_EXECUTED', WITHDRAWAL = 'WITHDRAWAL', // 区块链划转 FIAT_WITHDRAWAL = 'FIAT_WITHDRAWAL', // 法币提现 + SPECIAL_DEDUCTION = 'SPECIAL_DEDUCTION', // 特殊扣减(管理员操作) TRANSFER_IN = 'TRANSFER_IN', TRANSFER_OUT = 'TRANSFER_OUT', FREEZE = 'FREEZE', diff --git a/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx b/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx index a1ad9eec..03136f03 100644 --- a/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx @@ -34,6 +34,7 @@ import { PendingAction, PendingActionStatus, ACTION_CODE_OPTIONS, + ACTION_CODES, STATUS_OPTIONS, getStatusInfo, getActionCodeLabel, @@ -118,6 +119,12 @@ export default function PendingActionsPage() { expiresAt: '', }); + // 特殊扣减表单状态 + const [specialDeductionForm, setSpecialDeductionForm] = useState({ + amount: '', + reason: '', + }); + // 单个操作编辑时使用的表单 const [editFormData, setEditFormData] = useState({ actionParams: '', @@ -167,6 +174,7 @@ export default function PendingActionsPage() { actionParamsMap: {}, expiresAt: '', }); + setSpecialDeductionForm({ amount: '', reason: '' }); setShowCreateModal(true); }; @@ -283,6 +291,25 @@ export default function PendingActionsPage() { // 解析每个操作的参数 const actionParamsMap: Record | undefined> = {}; for (const actionCode of formData.selectedActions) { + // 特殊扣减有专用表单 + if (actionCode === ACTION_CODES.SPECIAL_DEDUCTION) { + const amount = parseFloat(specialDeductionForm.amount); + if (isNaN(amount) || amount <= 0) { + toast.error('特殊扣减金额必须大于 0'); + return; + } + if (!specialDeductionForm.reason.trim()) { + toast.error('特殊扣减必须填写原因/备注'); + return; + } + actionParamsMap[actionCode] = { + amount, + reason: specialDeductionForm.reason.trim(), + createdBy: 'admin', // TODO: 从登录状态获取 + }; + continue; + } + const paramsStr = formData.actionParamsMap[actionCode]; if (paramsStr?.trim()) { try { @@ -718,6 +745,40 @@ export default function PendingActionsPage() { )} + {/* 特殊扣减专用表单 */} + {formData.selectedActions.includes(ACTION_CODES.SPECIAL_DEDUCTION) && ( +
+
+ 特殊扣减设置 +
+
+
+ + setSpecialDeductionForm({ ...specialDeductionForm, amount: e.target.value })} + placeholder="请输入扣减金额" + min={0} + step={0.01} + /> +
+
+
+ +