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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tai.Wallet.EntityFrameworkCore" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tai.Wallet.EntityFrameworkCore" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Tai.Wallet.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Tai.Wallet.EntityFrameworkCore --version 1.2.0
                    
#r "nuget: Tai.Wallet.EntityFrameworkCore, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Tai.Wallet.EntityFrameworkCore@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Tai.Wallet.EntityFrameworkCore&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Tai.Wallet.EntityFrameworkCore&version=1.2.0
                    
Install as a Cake Tool

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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