Tai.Wallet.EntityFrameworkCore
1.2.0
dotnet add package Tai.Wallet.EntityFrameworkCore --version 1.2.0
NuGet\Install-Package Tai.Wallet.EntityFrameworkCore -Version 1.2.0
<PackageReference Include="Tai.Wallet.EntityFrameworkCore" Version="1.2.0" />
<PackageVersion Include="Tai.Wallet.EntityFrameworkCore" Version="1.2.0" />
<PackageReference Include="Tai.Wallet.EntityFrameworkCore" />
paket add Tai.Wallet.EntityFrameworkCore --version 1.2.0
#r "nuget: Tai.Wallet.EntityFrameworkCore, 1.2.0"
#:package Tai.Wallet.EntityFrameworkCore@1.2.0
#addin nuget:?package=Tai.Wallet.EntityFrameworkCore&version=1.2.0
#tool nuget:?package=Tai.Wallet.EntityFrameworkCore&version=1.2.0
Tai.Wallet
业务储值钱包 1.2.0,基于 .NET 10 / ABP 10.7.0 / EF Core。支持按用户或租户、币种开户,余额、流水、冻结与退款情况查询、入账、消费、部分退款、冻结与结算。不包含支付渠道和出款渠道;不提供 MongoDB 实现。
1.0.0 是破坏性重构:旧 Balance / BalanceTransaction 契约和开发期迁移已移除,Initial 示例迁移是新数据库基线,不可直接应用到旧结构。若保留开发数据,应先备份,再另行编写转换脚本,不能用删库方式处理已有业务数据。1.1.0 在此基础上增加查询与索引,保留 1.0.0 的 HTTP 路由和默认交易列表响应。1.2.0 将交易列表改为 ABP 标准分页:请求使用 SkipCount、MaxResultCount,响应为 TotalCount 与 Items。账户列表和冻结列表仍使用游标。
资金与交易
| 资金 | 用途 | 可提现 |
|---|---|---|
| Cash | 可退回现金的本金 | 是 |
| StoredValue | 兑换码等受限本金 | 否 |
| Bonus | 赠送金 | 否 |
一个账户属于一个租户范围、所有者类型、所有者 ID 和币种。开户显式执行,查询不存在的账户返回 404。金额为最小货币单位的正整数 long,账户已记账总额不超过 long.MaxValue;HTTP 金额和流水序号输出 JSON 字符串,避免 JavaScript 精度损失。核心只校验三位大写币种代码,不执行汇率换算。
五张表为 TaiWalletAccounts、TaiWalletFundBalances、TaiWalletTransactions、TaiWalletEntries、TaiWalletHolds。账户保存并发版本与递增流水序号;资金余额保存 PostedAmount 和 HeldAmount,可用额为二者之差。数据库约束保证 PostedAmount >= HeldAmount >= 0。一笔交易可以含多条资金分录,因此一次充值的本金、赠金共同提交。
Transaction 和 Entry 入账后不可修改、删除;EF 拦截器也保护组合宿主的正常 SaveChanges 写入路径。这不是对数据库管理员或绕过 EF 的 SQL 提供防篡改能力。退款追加交易和分录,关联原交易及原分录,按剩余可退金额分配,手续费不随消费退款自动退回。没有支付渠道退款用例。
默认消费按照可用本金和赠金比例分摊,本金内先使用 StoredValue,再使用 Cash;可通过 ABP Setting Wallet.DebitOrder 选择 Proportional、PrincipalFirst、BonusFirst。消费分摊前先预留手续费,手续费单独从 Cash 扣除。DebitBlocked 禁止新消费、新冻结,允许充值、退款和既有冻结的结算或释放。
安装与宿主
生产包按层独立安装:Tai.Wallet.Domain.Shared、Domain、Application.Contracts、Application、EntityFrameworkCore、HttpApi、HttpApi.Client。宿主通过对应 TaiWallet*Module 声明 DependsOn;数据库宿主实现 IWalletDbContext 的 Accounts、FundBalances、Transactions、Entries、Holds,调用 builder.ConfigureWallet(),通过 AddAbpDbContext 的 ReplaceDbContext<IWalletDbContext>() 接入仓储。参照 samples/Tai.Wallet.Sample 的两个数据库宿主上下文;连接字符串名为 TaiWallet,驱动和实际迁移由宿主负责。
同一钱包写入的账户余额、交易、分录和冻结状态必须在同一个数据库、同一个事务型 ABP Unit of Work 提交。应用集成层也应共享宿主 DbContext,以便业务记录与钱包写入原子提交;不可仅凭相同连接字符串就假定跨 DbContext 共享事务。并发或唯一键冲突后丢弃当前工作单元,在全新工作单元重试相同请求;不要在失败事务内继续写入。
进程内可信调用使用 WalletManager;普通用户 HTTP 不暴露任意充值、扣款能力。例子(执行于事务型 UOW 内):
var account = await wallet.OpenAsync(WalletOwnerType.User, userId, "CNY");
var transaction = await wallet.CreditAsync(account.Id,
new WalletRequest("recharge:business-id", "Recharge", "business-id",
ReasonCode: "MemberTopUp", Description: "会员充值",
Metadata: new Dictionary<string, string> { ["OrderId"] = "order-123" }),
new Dictionary<WalletFund, long> { [WalletFund.Cash] = 10000, [WalletFund.Bonus] = 1000 });
IdempotencyKey 区分大小写,唯一范围为租户、账户、操作。同键同参数返回原结果,同键异参数抛 RequestConflict。数据库唯一索引使用键的 SHA-256,避免 MySQL 默认排序规则忽略大小写。业务 ID、幂等键、交易 ID 分别承担不同职责;金额、关联 ID 和 metadata 都参与请求指纹,服务端消费策略保存在首次交易快照中。
SourceType / SourceId 用于业务来源和过滤;ReasonCode / Description 用于原因与展示。Metadata 为字符串字典,最多 32 项,键最长 64、值最长 1024,按默认 System.Text.Json 编码后的 UTF-8 JSON 不超过 16 KiB。不存凭据或秘密,不以任意 metadata 代替权限、金额、币种等结构化规则。退款未传 metadata 时继承原交易内容。
HTTP 与客户端
| 方法 | 路径 | 行为 |
|---|---|---|
| POST | /api/wallet/accounts | 为当前用户开户,输入 currency |
| GET | /api/wallet/accounts/me?currency=CNY | 当前用户余额 |
| GET | /api/wallet/accounts/me/list | 当前用户账户,支持 currency、status、afterCurrency、limit |
| GET | /api/wallet/accounts/{id} | 指定账户余额 |
| GET | /api/wallet/transactions/{id} | 交易详情及分录、metadata |
| GET | /api/wallet/accounts/{id}/transactions | 账户交易,ABP 分页(skipCount、maxResultCount、totalCount)及下述筛选 |
| GET | /api/wallet/transactions/{id}/refund-summary | 原消费或结算交易的可退、已退、剩余可退金额,按资金拆分 |
| GET | /api/wallet/accounts/{id}/holds | 账户冻结,支持 beforeSequence、limit、status、purpose、sourceType、sourceId、createdFrom、createdBefore |
| GET | /api/wallet/holds/{id} | 冻结详情及原始资金分配、预留与处理交易 ID |
读取需要 Wallet.Accounts.Read,开户另需 Wallet.Accounts.Open;所有公开查询仅访问当前租户中当前用户拥有的账户,不接受任意所有者或租户参数。HttpApi.Client 提供 IWalletAccountAppService、IWalletTransactionAppService、IWalletHoldAppService,RemoteServices 配置键 Wallet。
账户返回三类资金明细,以及 TotalPostedAmount(含冻结的当前总余额)、TotalHeldAmount、TotalAvailableAmount;三者满足 Posted = Held + Available。这些合计仅汇总当前账户的同币种资金,不代表累计充值或实际可提现额度。账户列表按币种升序,以 NextAfterCurrency 作为下次 afterCurrency;默认每页 20,最多 100,不执行 COUNT 查询,也不创建不存在的账户。
交易筛选支持 skipCount、maxResultCount、sourceType、sourceId、operation、fund、component、reasonCode、originalTransactionId、createdFrom、createdBefore。Operation 是钱包操作枚举(Credit / Debit / Refund / Reserve / Capture / Release);Fund 是分录资金类型(Cash / StoredValue / Bonus);Component 是分录组成(Amount / Fee / BonusForfeit);ReasonCode 是集成方定义的稳定原因代码字符串,最长 64 字符。枚举可使用名称或当前数值,非法值会被拒绝。字符串使用精确相等比较,大小写行为遵循宿主数据库排序规则。
Fund 与 Component 同时传入时必须命中同一条分录,命中后返回完整交易和所有分录,新增分录 LineNumber 用于明确顺序;币种从所属账户获取。Metadata 不作为查询条件,由业务方自行处理;为兼容 1.0.0,交易列表默认仍包含 Metadata。建议列表传 includeMetadata=false,此时 SQL 不读取 MetadataJson,返回的 Metadata 为空字典,详情仍返回完整内容。所有列表路径和详情继续经过原 Map(WalletTransaction) 扩展点,保留宿主既有的脱敏映射;关闭 Metadata 时传入非跟踪读取快照,仅 MetadataJson 为 {},其他映射输入保持一致。仅含 sourceType、sourceId 且默认包含 Metadata 的查询仍调用仓储 GetPageAsync;其他筛选使用 GetQueryPageAsync。两种路径都按 Sequence 倒序 Skip/Take。总条数由 CountQueryAsync 按相同筛选计算,不加载分录和 Metadata。宿主可覆写这些扩展点。
交易列表使用 ABP 的 PagedResultRequestDto 与 PagedResultDto:SkipCount 从 0 起,MaxResultCount 默认 10、最大 1000,响应包含 TotalCount 与 Items。排序固定为账户 Sequence 倒序,不接受动态 Sorting。新交易会出现在第一页,后续写入会使同一 SkipCount 的内容变化。冻结列表按预留交易的 Sequence 倒序,使用 beforeSequence 与 limit;默认每页 20,最多 100,多读一条判断后续页,不计算总条数。冻结状态是查询时状态,分页期间状态可能变化,不保证跨请求快照。
时间筛选采用 createdFrom ⇐ CreationTime < createdBefore,HTTP 建议传 UTC ISO 8601(带 Z)时间;同时传两个端点时结束时间必须大于开始时间。冻结筛选的来源取预留交易,Allocations 是最初预留的资金及手续费、赠金扣除项;结算或释放后仍保留这些原始金额,当前是否占用通过 Status 判断,不应把已处理冻结的 Allocations 当成当前冻结额。
退款汇总仅适用于 Debit、Capture,主体 Amount 分录参与可退款额度计算,Fee 和 BonusForfeit 不参与。关联退款列表复用交易筛选 operation=Refund&originalTransactionId=<id>。汇总是查询时的结果,实际退款仍重新校验额度和并发状态,不预占可退额度。
客户端查询示例:
var mine = await accountService.GetMineListAsync(new GetWalletAccountsInput { Limit = 20 });
var page = await transactionService.GetListAsync(accountId, new GetWalletTransactionsInput
{
Operation = WalletOperation.Debit,
Fund = WalletFund.Cash,
Component = WalletEntryComponent.Fee,
IncludeMetadata = false,
SkipCount = 0,
MaxResultCount = 20
});
var holds = await holdService.GetListAsync(accountId,
new GetWalletHoldsInput { Status = WalletHoldStatus.Reserved });
var refund = await transactionService.GetRefundSummaryAsync(transactionId);
1.1.0 保持五张业务表,不新增历史余额或报表快照表。新增账户所有者/币种、交易账户/时间/序号、交易账户/操作/序号索引,并将冻结的租户/账户/状态索引扩展为包含创建时间。示例的 PostgreSQL / MySQL QueryIndexes 迁移支持从 1.0.0 升级;组合宿主应基于 ConfigureWallet() 的更新生成自己的迁移。建索引可能占用数据库资源,部署由宿主安排。低命中率的分录、原因及组合筛选仍应按真实数据观察执行计划,不为所有排列预建索引。历史账期汇总不属于本版本公开查询。
冻结分 Reserve、Capture、Release:Reserve 仅占用可用余额,Capture 扣减已冻结资金,Release 释放占用;同一冻结只能成功结算或释放一次。未知外部出款结果不可释放,具体流程由 wallet-withdrawal 管理。
ReconcileAsync 用分录汇总核对余额,用于新工作单元中的巡检;并发写入期间多次查询可能跨越提交边界,持续差异应在稳定读视图或停写窗口重新确认,方法不自动修改金额。
验证
bash scripts/verify.sh
# 从仓库根目录按本地 NuGet 包依赖顺序验证全系列:
bash scripts/verify-wallet-series.sh
模块脚本在模块目录解析路径。示例支持 PostgreSQL / MySQL;WALLET_TEST_PROVIDER=postgres 或 mysql 配合 WALLET_TEST_CONNECTION 可执行 scripts/verify-relational.sh,连接应指向专用测试实例,需要创建和删除随机测试数据库的权限。SQLite 不执行三项真实关系数据库并发测试。HTTP 示例使用经过验证的 JWT,生产权限授予由消费宿主配置。
可选模块:wallet-recharge 组合 Tai.Payment;wallet-voucher 管理兑换码与充值券;wallet-withdrawal 管理提现工作流。详见各自 README 和 术语。
宿主管理查询(1.2.0)
宿主添加 TaiWalletApplicationModule 依赖后,可在进程内注入 Tai.Wallet.Administration.IWalletAdministrationAppService。服务及契约均标记 [RemoteService(false)],不生成管理 HTTP 接口。所有方法要求独立权限 Wallet.Administration.Read,客户权限 Wallet.Accounts.Read 不授予此能力。管理权限定义在现有 Wallet group 下,需由宿主单独分配。
GetStatisticsAsync 返回指定币种的当前账户数及各资金的可用、冻结、入账余额;周期消费按交易创建时间 [CreatedFrom, CreatedBefore) 统计 Debit 与 Capture,排除来源为 Withdrawal 的交易,金额包含手续费等全部分录,笔数按交易计。当前余额不受周期筛选影响。
FindAccountByUserAsync 按当前租户、用户与币种查找,不存在返回 null。GetAccountAsync、GetTransactionsAsync、GetTransactionAsync 可读取当前租户中其他用户的数据。非用户账户的 UserId 为 Guid.Empty。管理交易列表使用 GetWalletTransactionsInput.BeforeSequence 和 Limit(默认 20,上限 100),响应 WalletTransactionPageDto.NextBeforeSequence 为下一页游标,没有总数。普通客户交易列表继续使用 SkipCount、MaxResultCount。筛选覆盖时间、操作、资金、分录组件、来源、原因及原交易 ID;管理响应始终省略内部元数据,Metadata 为空。
本次仅在 ConfigureWallet() 增加交易 (TenantId, CreationTime, Operation) 索引,不生成模块或示例迁移。Vetus 等消费宿主调用更新后的模型配置并生成自己的迁移;示例若需运行 --migrate,也应先在示例宿主中生成匹配当前模型的迁移。scripts/verify.sh 验证还原、Release 构建、真实服务与数据库测试及生产包打包,数据库测试从当前模型建库;示例迁移快照一致性及生产数据库迁移由宿主验证。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Tai.Wallet.Domain (>= 1.2.0)
- Volo.Abp.EntityFrameworkCore (>= 10.7.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Tai.Wallet.EntityFrameworkCore:
| Package | Downloads |
|---|---|
|
Tai.Wallet.Withdrawal.EntityFrameworkCore
Optional ABP stored-value wallet withdrawal workflow |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.2.0 | 132 | 11/1/2026 |
| 1.1.0 | 131 | 10/18/2026 |
| 1.0.0 | 169 | 10/17/2026 |
| 0.2.0 | 102 | 10/11/2026 |
| 0.1.9 | 102 | 10/7/2026 |
| 0.1.8 | 105 | 10/5/2026 |
| 0.1.7 | 1,464 | 8/21/2020 |
| 0.1.6 | 653 | 8/20/2020 |
| 0.1.5 | 657 | 8/20/2020 |
| 0.1.4 | 719 | 7/8/2020 |
| 0.1.3 | 663 | 7/8/2020 |
| 0.1.2 | 702 | 7/7/2020 |
| 0.1.1 | 662 | 7/6/2020 |
| 0.1.0 | 648 | 6/29/2020 |