feat(pending-actions): add special deduction feature for admin-created user actions
实现特殊扣减功能,允许管理员为用户创建扣减待办操作,由用户在移动端确认执行。 ## 后端 (wallet-service) ### 领域层 - 新增 `SPECIAL_DEDUCTION` 到 LedgerEntryType 枚举 用于记录特殊扣减的账本流水类型 ### 应用层 - 新增 `executeSpecialDeduction` 方法 - 验证用户钱包存在性 - 检查余额是否充足 - 乐观锁控制并发 - 扣减余额并记录账本流水 - 返回操作结果和新余额 ### API层 - 新增内部API: POST /api/v1/wallets/special-deduction/execute 供移动端调用执行特殊扣减操作 ## 前端 (admin-web) ### 类型定义 - 新增 `SPECIAL_DEDUCTION` 到 ACTION_CODES - 新增 `SpecialDeductionParams` 接口定义扣减参数 - amount: 扣减金额 - reason: 扣减原因 ### 页面 - 更新待办操作管理页面 - 当选择 SPECIAL_DEDUCTION 时显示扣减金额和原因输入框 - 验证扣减金额必须大于0 - 验证扣减原因不能为空 ### 样式 - 新增特殊扣减表单区域样式 ## 前端 (mobile-app) ### 服务层 - 新增 `executeSpecialDeduction` 方法到 WalletService - 新增 `SpecialDeductionResult` 结果类 - 新增 `specialDeduction` 到 PendingActionCode 枚举 ### 页面 - 新增 `SpecialDeductionPage` 特殊扣减确认页面 - 显示扣减金额和管理员备注 - 显示当前余额和扣减后余额 - 余额不足时禁用确认按钮 - 温馨提示说明操作性质 - 更新 `PendingActionsPage` - 处理 SPECIAL_DEDUCTION 类型的待办操作 - 从 actionParams 解析 amount 和 reason - 导航到特殊扣减确认页面 ## 工作流程 1. 管理员在 admin-web 创建 SPECIAL_DEDUCTION 待办操作 - 选择目标用户 - 输入扣减金额 - 输入扣减原因 2. 用户在 mobile-app 待办操作列表看到该操作 3. 用户点击后进入特殊扣减确认页面 - 查看扣减详情 - 确认余额充足 - 点击确认执行扣减 4. 后端执行扣减并记录账本流水 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a609600cd8
commit
dfdd8ed65a
|
|
@ -273,4 +273,32 @@ export class InternalWalletController {
|
||||||
remark: dto.remark,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2331,6 +2331,8 @@ export class WalletApplicationService {
|
||||||
TRANSFER_TO_POOL: '转入矿池',
|
TRANSFER_TO_POOL: '转入矿池',
|
||||||
SWAP_EXECUTED: '兑换执行',
|
SWAP_EXECUTED: '兑换执行',
|
||||||
WITHDRAWAL: '提现',
|
WITHDRAWAL: '提现',
|
||||||
|
FIAT_WITHDRAWAL: '法币提现',
|
||||||
|
SPECIAL_DEDUCTION: '特殊扣减',
|
||||||
TRANSFER_IN: '转入',
|
TRANSFER_IN: '转入',
|
||||||
TRANSFER_OUT: '转出',
|
TRANSFER_OUT: '转出',
|
||||||
FREEZE: '冻结',
|
FREEZE: '冻结',
|
||||||
|
|
@ -2339,4 +2341,151 @@ export class WalletApplicationService {
|
||||||
};
|
};
|
||||||
return nameMap[entryType] || entryType;
|
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}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export enum LedgerEntryType {
|
||||||
SWAP_EXECUTED = 'SWAP_EXECUTED',
|
SWAP_EXECUTED = 'SWAP_EXECUTED',
|
||||||
WITHDRAWAL = 'WITHDRAWAL', // 区块链划转
|
WITHDRAWAL = 'WITHDRAWAL', // 区块链划转
|
||||||
FIAT_WITHDRAWAL = 'FIAT_WITHDRAWAL', // 法币提现
|
FIAT_WITHDRAWAL = 'FIAT_WITHDRAWAL', // 法币提现
|
||||||
|
SPECIAL_DEDUCTION = 'SPECIAL_DEDUCTION', // 特殊扣减(管理员操作)
|
||||||
TRANSFER_IN = 'TRANSFER_IN',
|
TRANSFER_IN = 'TRANSFER_IN',
|
||||||
TRANSFER_OUT = 'TRANSFER_OUT',
|
TRANSFER_OUT = 'TRANSFER_OUT',
|
||||||
FREEZE = 'FREEZE',
|
FREEZE = 'FREEZE',
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import {
|
||||||
PendingAction,
|
PendingAction,
|
||||||
PendingActionStatus,
|
PendingActionStatus,
|
||||||
ACTION_CODE_OPTIONS,
|
ACTION_CODE_OPTIONS,
|
||||||
|
ACTION_CODES,
|
||||||
STATUS_OPTIONS,
|
STATUS_OPTIONS,
|
||||||
getStatusInfo,
|
getStatusInfo,
|
||||||
getActionCodeLabel,
|
getActionCodeLabel,
|
||||||
|
|
@ -118,6 +119,12 @@ export default function PendingActionsPage() {
|
||||||
expiresAt: '',
|
expiresAt: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 特殊扣减表单状态
|
||||||
|
const [specialDeductionForm, setSpecialDeductionForm] = useState({
|
||||||
|
amount: '',
|
||||||
|
reason: '',
|
||||||
|
});
|
||||||
|
|
||||||
// 单个操作编辑时使用的表单
|
// 单个操作编辑时使用的表单
|
||||||
const [editFormData, setEditFormData] = useState({
|
const [editFormData, setEditFormData] = useState({
|
||||||
actionParams: '',
|
actionParams: '',
|
||||||
|
|
@ -167,6 +174,7 @@ export default function PendingActionsPage() {
|
||||||
actionParamsMap: {},
|
actionParamsMap: {},
|
||||||
expiresAt: '',
|
expiresAt: '',
|
||||||
});
|
});
|
||||||
|
setSpecialDeductionForm({ amount: '', reason: '' });
|
||||||
setShowCreateModal(true);
|
setShowCreateModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -283,6 +291,25 @@ export default function PendingActionsPage() {
|
||||||
// 解析每个操作的参数
|
// 解析每个操作的参数
|
||||||
const actionParamsMap: Record<string, Record<string, unknown> | undefined> = {};
|
const actionParamsMap: Record<string, Record<string, unknown> | undefined> = {};
|
||||||
for (const actionCode of formData.selectedActions) {
|
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];
|
const paramsStr = formData.actionParamsMap[actionCode];
|
||||||
if (paramsStr?.trim()) {
|
if (paramsStr?.trim()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -718,6 +745,40 @@ export default function PendingActionsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 特殊扣减专用表单 */}
|
||||||
|
{formData.selectedActions.includes(ACTION_CODES.SPECIAL_DEDUCTION) && (
|
||||||
|
<div className={styles.pendingActions__specialDeduction}>
|
||||||
|
<div className={styles.pendingActions__specialDeductionTitle}>
|
||||||
|
特殊扣减设置
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__formRow}>
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>扣减金额 (绿积分) *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={specialDeductionForm.amount}
|
||||||
|
onChange={(e) => setSpecialDeductionForm({ ...specialDeductionForm, amount: e.target.value })}
|
||||||
|
placeholder="请输入扣减金额"
|
||||||
|
min={0}
|
||||||
|
step={0.01}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pendingActions__formGroup}>
|
||||||
|
<label>扣减原因/备注 *</label>
|
||||||
|
<textarea
|
||||||
|
value={specialDeductionForm.reason}
|
||||||
|
onChange={(e) => setSpecialDeductionForm({ ...specialDeductionForm, reason: e.target.value })}
|
||||||
|
placeholder="请详细说明扣减原因,用户会在确认页面看到此信息"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<span className={styles.pendingActions__formHint}>
|
||||||
|
此信息将显示给用户,请确保描述清晰准确
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.pendingActions__formGroup}>
|
<div className={styles.pendingActions__formGroup}>
|
||||||
<label>过期时间 (可选,所有操作共用)</label>
|
<label>过期时间 (可选,所有操作共用)</label>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -422,4 +422,27 @@
|
||||||
color: $error-color;
|
color: $error-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 特殊扣减样式
|
||||||
|
&__specialDeduction {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff7e6;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__specialDeductionTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d46b08;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '⚠️';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const ACTION_CODES = {
|
||||||
FORCE_KYC: 'FORCE_KYC', // 强制 KYC
|
FORCE_KYC: 'FORCE_KYC', // 强制 KYC
|
||||||
SIGN_CONTRACT: 'SIGN_CONTRACT', // 签订合同
|
SIGN_CONTRACT: 'SIGN_CONTRACT', // 签订合同
|
||||||
CUSTOM_NOTICE: 'CUSTOM_NOTICE', // 自定义通知
|
CUSTOM_NOTICE: 'CUSTOM_NOTICE', // 自定义通知
|
||||||
|
SPECIAL_DEDUCTION: 'SPECIAL_DEDUCTION', // 特殊扣减
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ActionCode = (typeof ACTION_CODES)[keyof typeof ACTION_CODES] | string;
|
export type ActionCode = (typeof ACTION_CODES)[keyof typeof ACTION_CODES] | string;
|
||||||
|
|
@ -108,8 +109,18 @@ export const ACTION_CODE_OPTIONS = [
|
||||||
{ value: ACTION_CODES.FORCE_KYC, label: '强制 KYC' },
|
{ value: ACTION_CODES.FORCE_KYC, label: '强制 KYC' },
|
||||||
{ value: ACTION_CODES.SIGN_CONTRACT, label: '签订合同' },
|
{ value: ACTION_CODES.SIGN_CONTRACT, label: '签订合同' },
|
||||||
{ value: ACTION_CODES.CUSTOM_NOTICE, label: '自定义通知' },
|
{ value: ACTION_CODES.CUSTOM_NOTICE, label: '自定义通知' },
|
||||||
|
{ value: ACTION_CODES.SPECIAL_DEDUCTION, label: '特殊扣减' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特殊扣减操作参数
|
||||||
|
*/
|
||||||
|
export interface SpecialDeductionParams {
|
||||||
|
amount: number; // 扣减金额
|
||||||
|
reason: string; // 扣减原因/备注
|
||||||
|
createdBy: string; // 创建人
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态选项(用于筛选和显示)
|
* 状态选项(用于筛选和显示)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ enum PendingActionCode {
|
||||||
bindPhone('BIND_PHONE', '绑定手机'),
|
bindPhone('BIND_PHONE', '绑定手机'),
|
||||||
forceKyc('FORCE_KYC', '强制实名'),
|
forceKyc('FORCE_KYC', '强制实名'),
|
||||||
signContract('SIGN_CONTRACT', '签署合同'),
|
signContract('SIGN_CONTRACT', '签署合同'),
|
||||||
updateProfile('UPDATE_PROFILE', '更新资料');
|
updateProfile('UPDATE_PROFILE', '更新资料'),
|
||||||
|
specialDeduction('SPECIAL_DEDUCTION', '特殊扣减');
|
||||||
|
|
||||||
final String code;
|
final String code;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
|
||||||
|
|
@ -824,6 +824,75 @@ class WalletService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 执行特殊扣减
|
||||||
|
///
|
||||||
|
/// 调用 POST /api/v1/wallets/special-deduction/execute (wallet-service 内部 API)
|
||||||
|
/// 此方法由待办操作页面调用,用户确认后执行扣减
|
||||||
|
Future<SpecialDeductionResult> executeSpecialDeduction({
|
||||||
|
required String accountSequence,
|
||||||
|
required String pendingActionId,
|
||||||
|
required double amount,
|
||||||
|
required String reason,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
debugPrint('[WalletService] ========== 执行特殊扣减 ==========');
|
||||||
|
debugPrint('[WalletService] accountSequence: $accountSequence');
|
||||||
|
debugPrint('[WalletService] pendingActionId: $pendingActionId');
|
||||||
|
debugPrint('[WalletService] amount: $amount');
|
||||||
|
debugPrint('[WalletService] reason: $reason');
|
||||||
|
|
||||||
|
final response = await _apiClient.post(
|
||||||
|
'/api/v1/wallets/special-deduction/execute',
|
||||||
|
data: {
|
||||||
|
'accountSequence': accountSequence,
|
||||||
|
'pendingActionId': pendingActionId,
|
||||||
|
'amount': amount,
|
||||||
|
'reason': reason,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint('[WalletService] 响应状态码: ${response.statusCode}');
|
||||||
|
debugPrint('[WalletService] 响应数据: ${response.data}');
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
final responseData = response.data as Map<String, dynamic>;
|
||||||
|
final data = responseData['data'] as Map<String, dynamic>? ?? responseData;
|
||||||
|
final result = SpecialDeductionResult.fromJson(data);
|
||||||
|
debugPrint('[WalletService] 特殊扣减成功: orderNo=${result.orderNo}, newBalance=${result.newBalance}');
|
||||||
|
debugPrint('[WalletService] ================================');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('特殊扣减失败: ${response.statusCode}');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('[WalletService] !!!!!!!!!! 特殊扣减异常 !!!!!!!!!!');
|
||||||
|
debugPrint('[WalletService] 错误: $e');
|
||||||
|
debugPrint('[WalletService] 堆栈: $stackTrace');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 特殊扣减结果
|
||||||
|
class SpecialDeductionResult {
|
||||||
|
final bool success;
|
||||||
|
final String orderNo;
|
||||||
|
final double newBalance;
|
||||||
|
|
||||||
|
SpecialDeductionResult({
|
||||||
|
required this.success,
|
||||||
|
required this.orderNo,
|
||||||
|
required this.newBalance,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SpecialDeductionResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SpecialDeductionResult(
|
||||||
|
success: json['success'] as bool? ?? false,
|
||||||
|
orderNo: json['orderNo'] as String? ?? '',
|
||||||
|
newBalance: (json['newBalance'] ?? 0).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提取响应
|
/// 提取响应
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../../../../core/services/contract_signing_service.dart';
|
||||||
import '../../../../core/services/reward_service.dart';
|
import '../../../../core/services/reward_service.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
import '../../../kyc/data/kyc_service.dart';
|
import '../../../kyc/data/kyc_service.dart';
|
||||||
|
import 'special_deduction_page.dart';
|
||||||
|
|
||||||
/// 待办操作页面
|
/// 待办操作页面
|
||||||
/// 强制用户按后端配置执行待办任务,不可跳过
|
/// 强制用户按后端配置执行待办任务,不可跳过
|
||||||
|
|
@ -166,6 +167,29 @@ class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
final result = await context.push<bool>(RoutePaths.editProfile);
|
final result = await context.push<bool>(RoutePaths.editProfile);
|
||||||
return result == true;
|
return result == true;
|
||||||
|
|
||||||
|
case 'SPECIAL_DEDUCTION':
|
||||||
|
// 特殊扣减操作
|
||||||
|
final amount = (action.actionParams?['amount'] ?? 0).toDouble();
|
||||||
|
final reason = action.actionParams?['reason'] as String? ?? '管理员扣减';
|
||||||
|
|
||||||
|
if (amount <= 0) {
|
||||||
|
debugPrint('[PendingActionsPage] 特殊扣减金额无效: $amount');
|
||||||
|
return true; // 金额无效,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await Navigator.of(context).push<bool>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SpecialDeductionPage(
|
||||||
|
params: SpecialDeductionParams(
|
||||||
|
pendingActionId: action.id.toString(),
|
||||||
|
amount: amount,
|
||||||
|
reason: reason,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return result == true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 未知操作类型,显示提示并标记为完成
|
// 未知操作类型,显示提示并标记为完成
|
||||||
debugPrint('[PendingActionsPage] 未知操作类型: ${action.actionCode}');
|
debugPrint('[PendingActionsPage] 未知操作类型: ${action.actionCode}');
|
||||||
|
|
@ -588,6 +612,8 @@ class _PendingActionsPageState extends ConsumerState<PendingActionsPage> {
|
||||||
return Icons.description;
|
return Icons.description;
|
||||||
case 'UPDATE_PROFILE':
|
case 'UPDATE_PROFILE':
|
||||||
return Icons.edit;
|
return Icons.edit;
|
||||||
|
case 'SPECIAL_DEDUCTION':
|
||||||
|
return Icons.remove_circle_outline;
|
||||||
default:
|
default:
|
||||||
return Icons.assignment;
|
return Icons.assignment;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,537 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/wallet_service.dart';
|
||||||
|
|
||||||
|
/// 特殊扣减参数
|
||||||
|
class SpecialDeductionParams {
|
||||||
|
final String pendingActionId;
|
||||||
|
final double amount;
|
||||||
|
final String reason;
|
||||||
|
|
||||||
|
SpecialDeductionParams({
|
||||||
|
required this.pendingActionId,
|
||||||
|
required this.amount,
|
||||||
|
required this.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 特殊扣减确认页面
|
||||||
|
class SpecialDeductionPage extends ConsumerStatefulWidget {
|
||||||
|
final SpecialDeductionParams params;
|
||||||
|
|
||||||
|
const SpecialDeductionPage({
|
||||||
|
super.key,
|
||||||
|
required this.params,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<SpecialDeductionPage> createState() =>
|
||||||
|
_SpecialDeductionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpecialDeductionPageState extends ConsumerState<SpecialDeductionPage> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _isExecuting = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
WalletResponse? _walletInfo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadWalletInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadWalletInfo() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final walletService = ref.read(walletServiceProvider);
|
||||||
|
final wallet = await walletService.getMyWallet();
|
||||||
|
setState(() {
|
||||||
|
_walletInfo = wallet;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = '加载钱包信息失败: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _executeDeduction() async {
|
||||||
|
if (_walletInfo == null) return;
|
||||||
|
|
||||||
|
// 检查余额是否足够
|
||||||
|
if (_walletInfo!.balances.usdt.available < widget.params.amount) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('余额不足,无法执行扣减'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isExecuting = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
final accountSequence = await accountService.getUserSerialNum();
|
||||||
|
|
||||||
|
if (accountSequence == null || accountSequence.isEmpty) {
|
||||||
|
throw Exception('未登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
final walletService = ref.read(walletServiceProvider);
|
||||||
|
final result = await walletService.executeSpecialDeduction(
|
||||||
|
accountSequence: accountSequence,
|
||||||
|
pendingActionId: widget.params.pendingActionId,
|
||||||
|
amount: widget.params.amount,
|
||||||
|
reason: widget.params.reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('扣减成功,新余额: ${result.newBalance.toStringAsFixed(2)} 绿积分'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception('扣减失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isExecuting = false;
|
||||||
|
_errorMessage = '执行扣减失败: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFFF7E6),
|
||||||
|
Color(0xFFEAE0C8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(),
|
||||||
|
Expanded(child: _buildContent()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'特殊扣减',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'请确认以下扣减信息',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(color: Color(0xFFD4AF37)),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'正在加载钱包信息...',
|
||||||
|
style: TextStyle(fontSize: 14, color: Color(0xFF666666)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage != null) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _loadWalletInfo,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFD4AF37),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 扣减信息卡片
|
||||||
|
_buildDeductionInfoCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 钱包余额卡片
|
||||||
|
_buildBalanceCard(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// 确认按钮
|
||||||
|
_buildConfirmButton(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 提示信息
|
||||||
|
_buildNotice(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDeductionInfoCard() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF7E6),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.remove_circle_outline,
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text(
|
||||||
|
'扣减金额',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'${widget.params.amount.toStringAsFixed(2)} 绿积分',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFFE53935),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'扣减原因',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF7E6),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: const Color(0xFFFFD591)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.params.reason,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBalanceCard() {
|
||||||
|
final available = _walletInfo?.balances.usdt.available ?? 0;
|
||||||
|
final afterDeduction = available - widget.params.amount;
|
||||||
|
final isInsufficient = afterDeduction < 0;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'当前余额',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF666666),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${available.toStringAsFixed(2)} 绿积分',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'扣减金额',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF666666),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'-${widget.params.amount.toStringAsFixed(2)} 绿积分',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFFE53935),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'扣减后余额',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${afterDeduction.toStringAsFixed(2)} 绿积分',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isInsufficient ? Colors.red : const Color(0xFF4CAF50),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isInsufficient) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFEBEE),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: Colors.red, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'余额不足,无法执行此扣减操作',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildConfirmButton() {
|
||||||
|
final available = _walletInfo?.balances.usdt.available ?? 0;
|
||||||
|
final isInsufficient = available < widget.params.amount;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isExecuting || isInsufficient ? null : _executeDeduction,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: isInsufficient ? Colors.grey : const Color(0xFFE53935),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
disabledBackgroundColor: Colors.grey[300],
|
||||||
|
),
|
||||||
|
child: _isExecuting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
isInsufficient ? '余额不足' : '确认扣减',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNotice() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF7E6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFFFFD591)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Color(0xFFD46B08),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'温馨提示',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFFD46B08),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'• 此操作由系统管理员发起\n'
|
||||||
|
'• 扣减将从您的绿积分余额中直接扣除\n'
|
||||||
|
'• 扣减完成后无法撤销\n'
|
||||||
|
'• 如有疑问,请联系客服',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
height: 1.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue