epic-6-wallet
执行 Epic 6 —— 持股客户鑉包(余额账本、充値(管理员)、提现申请(线下审核)、流水明细)。依赖 Epic 5 已完成。
SKILL.md.claude/skills/epic-6-wallet/SKILL.md
GitHubEpic 6 — 投资人钱包与提现
前置条件
- Epic 5 已完成(DividendDistributedEvent 可用)
- 已读取
shareflow-context/SKILL.md
任务清单
后端
Domain 层
Wallet 实体 (src/Domain/Entities/Wallet.cs)
public class Wallet : Entity<Guid>
{
public Guid InvestorUserId { get; private set; }
public decimal Balance { get; private set; } // 当前可用余额
public decimal FrozenAmount { get; private set; } // 提现申请中的冻结额
public string Currency { get; private set; } = string.Empty;
public void Credit(decimal amount, string remark) {
if (amount <= 0) throw new BusinessException("wallet.invalidAmount");
Balance += amount;
AddDomainEvent(new WalletCreditedEvent(Id, InvestorUserId, amount, Currency, remark));
}
public void Freeze(decimal amount) {
if (amount > Balance) throw new BusinessException("wallet.insufficientBalance");
Balance -= amount;
FrozenAmount += amount;
}
public void Unfreeze(decimal amount) {
FrozenAmount -= amount;
Balance += amount;
}
public void DeductFrozen(decimal amount) {
FrozenAmount -= amount;
}
}
WalletTransaction 实体 (src/Domain/Entities/WalletTransaction.cs)
public class WalletTransaction : Entity<long> // long for high-freq log
{
public Guid WalletId { get; private set; }
public Guid InvestorUserId { get; private set; }
public TransactionType Type { get; private set; } // Dividend / AdminCredit / Withdrawal
public TransactionDirection Direction { get; private set; } // In / Out
public decimal Amount { get; private set; }
public string Currency { get; private set; } = string.Empty;
public decimal BalanceBefore { get; private set; }
public decimal BalanceAfter { get; private set; }
public string Remark { get; private set; } = string.Empty;
public Guid? ReferenceId { get; private set; } // DividendRecordId / WithdrawalRequestId
}
public enum TransactionType { Dividend, AdminCredit, Withdrawal }
public enum TransactionDirection { In, Out }
WithdrawalRequest 实体 (src/Domain/Entities/WithdrawalRequest.cs)
public class WithdrawalRequest : Entity<Guid>
{
public Guid InvestorUserId { get; private set; }
public Guid WalletId { get; private set; }
public decimal Amount { get; private set; }
public string Currency { get; private set; } = string.Empty;
public WithdrawalStatus Status { get; private set; } // Pending / Approved / Rejected / Completed
public string? PaymentMethod { get; private set; } // 线下付款方式备注
public string? RejectReason { get; private set; }
public Guid? ApprovedByUserId { get; private set; }
public DateTime? ProcessedAt { get; private set; }
public void Approve(Guid adminId) { ... } // Wallet.Freeze()
public void Complete(Guid adminId) { ... } // Wallet.DeductFrozen()
public void Reject(Guid adminId, string reason) { ... } // Wallet.Unfreeze()
}
public enum WithdrawalStatus { Pending, Approved, Rejected, Completed }
Repository IWalletRepository, IWithdrawalRequestRepository, IWalletTransactionRepository
Application 层
Event Handler (DividendDistributedEventHandler)
- 监听
DividendDistributedEvent - 找到 investor 的 Wallet →
wallet.Credit(amount, "Dividend") - 记录
WalletTransaction
IWalletService
public interface IWalletService
{
Task<WalletDto> GetByInvestorIdAsync(Guid investorId);
Task AdminCreditAsync(Guid investorId, decimal amount, string remark, Guid adminId);
Task<Guid> RequestWithdrawalAsync(Guid investorId, decimal amount, string? paymentMethod);
Task ApproveWithdrawalAsync(Guid requestId, Guid adminId);
Task CompleteWithdrawalAsync(Guid requestId, Guid adminId);
Task RejectWithdrawalAsync(Guid requestId, Guid adminId, string reason);
Task<PagedResult<WalletTransactionDto>> GetTransactionsAsync(Guid investorId, PagedRequest paged);
}
Mapster 配置
config.NewConfig<WalletTransaction, WalletTransactionDto>()
.Map(dest => dest.TypeLabel, src => src.Type.ToString())
.Map(dest => dest.DirectionLabel, src => src.Direction == TransactionDirection.In ? "+" : "-");
API Controller
管理端(后台):
GET /v1/admin/wallets 所有投资人钱包列表
POST /v1/admin/wallets/{investorId}/credit 管理员充值 [Permission("dividend.write")]
GET /v1/admin/withdrawals 提现申请列表
POST /v1/admin/withdrawals/{id}/approve [Permission("withdrawal.approve")]
POST /v1/admin/withdrawals/{id}/complete
POST /v1/admin/withdrawals/{id}/reject
客户端(自服务):
GET /v1/client/wallet 查看余额
GET /v1/client/wallet/transactions
POST /v1/client/wallet/withdrawals 申请提现
前端
管理端 views/admin/wallet/
WalletListView.vue— 投资人余额汇总列表WithdrawalRequestListView.vue— 提现申请列表 + 审批操作- 提现流程:Pending → Approve(冻结)→ Complete(打款后标记完成)
客户端 views/client/wallet/
WalletView.vue— 余额 + 流水 TimelineWithdrawalFormModal.vue— 申请提现表单
完成标准
- 分红分发后钱包余额自动更新
- 每次余额变化均有
WalletTransaction记录(含 before/after) - 提现状态机:Pending → Approved → Completed 正确流转
-
withdrawal.approve权限校验 - 提现金额超过余额时返回
wallet.insufficientBalance错误