RuoVea.OmiApi.Ledger
10.0.1.5
dotnet add package RuoVea.OmiApi.Ledger --version 10.0.1.5
NuGet\Install-Package RuoVea.OmiApi.Ledger -Version 10.0.1.5
<PackageReference Include="RuoVea.OmiApi.Ledger" Version="10.0.1.5" />
<PackageVersion Include="RuoVea.OmiApi.Ledger" Version="10.0.1.5" />
<PackageReference Include="RuoVea.OmiApi.Ledger" />
paket add RuoVea.OmiApi.Ledger --version 10.0.1.5
#r "nuget: RuoVea.OmiApi.Ledger, 10.0.1.5"
#:package RuoVea.OmiApi.Ledger@10.0.1.5
#addin nuget:?package=RuoVea.OmiApi.Ledger&version=10.0.1.5
#tool nuget:?package=RuoVea.OmiApi.Ledger&version=10.0.1.5
RuoVea.OmiApi.Ledger
账本管理模块 —— 基于 .NET 构建的轻量级、跨平台记账管理系统后端。
RuoVea.OmiApi.Ledger 是一个开箱即用的记账管理 NuGet 包,提供账本管理、账单记录、收支途径、收支项目、数据统计分析等完整能力。基于 SqlSugar ORM 和 DynamicWebApi,注册即自动生成 RESTful API 端点,支持 MySql / SqlServer / PostgreSQL / SQLite / Oracle 等多种数据库。
目录
概览
功能特性
| 模块 | 功能 |
|---|---|
| 📒 账本管理 | 多账本创建、编辑、删除,每个用户可管理多个账本,支持默认账本、排序、禁用/启用 |
| 📝 账单记录 | 记录日常收支,支持按账本、项目、渠道、时间范围多维度联合筛选,关联查询账本/项目/途径名称 |
| 🏦 收支途径管理 | 管理支付渠道(现金、银行卡、支付宝、微信等),含系统默认途径、排序、禁用/启用 |
| 🏷️ 收支项目管理 | 自定义收支分类(餐饮、交通、工资、奖金等),区分支出(FlowsType=1)和收入(FlowsType=2) |
| 📊 数据统计分析 | 按日/月/年/周维度统计汇总;月度收支透视表;收入/支出/结余趋势图数据;按途径/按项目饼图数据;日/月折线图数据 |
| ⚡ 数据缓存 | 月度收入、月度支出、月度结余数据 10 分钟内存缓存,减少数据库重复查询 |
| 🚀 数据初始化 | 一键创建账本、途径和项目,含种子数据(资金流动字典类型及收支字典值),CodeFirst 自动建表 |
| 🌍 多语言国际化 | 支持中文简体(zh-CN)、中文繁体(zh-TW/zh-HK)、英文(en-US)、日文(ja-JP)、法文(fr-FR)、越南文(vi-VN) |
| 🛡️ 数据保护 | 删除前校验关联数据(账本/途径/项目有账单记录时阻止删除),防止最后一条记录被删 |
| 🗄️ 多库支持 | MySql、SqlServer、PostgreSQL、SQLite、Oracle、Dm 等 |
| 🧩 自动 API | 实现 IApplicationService 即自动映射为 REST 控制器 |
架构一览
┌──────────────────────────────────────────────────────┐
│ NuGet Package │
│ RuoVea.OmiApi.Ledger │
├──────────────────────────────────────────────────────┤
│ Service Layer (5 Services) │
│ ┌───────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ Book │ │ Record │ │ Channel │ │
│ │ Service │ │ Service │ │ Service │ │
│ ├───────────┤ ├────────────┤ ├───────────────┤ │
│ │ Itemt │ │ Analys │ │
│ │ Service │ │ Service │ │
│ └───────────┘ └────────────┘ │
├──────────────────────────────────────────────────────┤
│ Domain Layer (4 Entities) │
│ BillBook ──< BillRecord │
│ BillChannel ──< BillRecord │
│ BillItemt ──< BillRecord │
│ BillRecord (中心实体,关联其他三个) │
├──────────────────────────────────────────────────────┤
│ Infrastructure │
│ SqlSugar ORM · DynamicWebApi · ExSugar Repo │
│ Mapster(对象映射) · CacheFactery(数据缓存) │
└──────────────────────────────────────────────────────┘
实体关系图
┌──────────────┐
│ BillBook │
│ (账本) │
└──────┬───────┘
│ 1:N (BookId)
│
┌──────┴───────┐ ┌──────────────┐
│ │ │ BillChannel │
│ BillRecord │◄──────│ (收支途径) │
│ (账单记录) │ 1:N └──────────────┘
│ │ (ChannelId)
│ │
└──────┬───────┘
│ ┌──────────────┐
│ │ BillItemt │
└────────│ (收支项目) │
1:N └──────────────┘
(ItemtId)
支持的 .NET 版本
| TFM | NuGet 版本 |
|---|---|
net8.0 |
8.0.1.5 |
net10.0 |
10.0.1.4 |
安装
NuGet 包管理器
# .NET 8 项目
Install-Package RuoVea.OmiApi.Ledger -Version 8.0.1.5
# .NET 10 项目
Install-Package RuoVea.OmiApi.Ledger -Version 10.0.1.4
.NET CLI
# .NET 8
dotnet add package RuoVea.OmiApi.Ledger --version 8.0.1.5
# .NET 10
dotnet add package RuoVea.OmiApi.Ledger --version 10.0.1.4
依赖项
本包依赖以下组件(安装时会自动引入):
| 包名 | 用途 |
|---|---|
RuoVea.OmiApi.Dict |
系统字典服务(收支类型分类数据) |
RuoVea.DynamicWebApi |
动态 API 控制器生成 |
RuoVea.ExSugar |
SqlSugar 仓储模式封装 |
30 秒快速开始
1. 配置数据库连接 (appsettings.json)
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./ruovea.db"
}
],
"DbInitConfig": {
"InitTable": true,
"InitSeedData": true
},
"Swagger": {
"ApiVersions": [
{
"Title": "记账管理",
"Version": "Ledger"
}
]
}
}
支持的 DbType 值:
MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、OpenGauss、ClickHouse、Access、GBase等。
2. 注册服务 (Program.cs)
// <summary>
// 在 Program.cs 中注册 OmiApi.Ledger 组件服务
// </summary>
var builder = WebApplication.CreateBuilder(args);
// 注册动态 Web API(自动将 Service 映射为 REST 控制器)
builder.Services.AddDynamicWebApi(options =>
{
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
// 注册 Ledger 模块服务(默认 Scoped 生命周期)
builder.Services.AddOmiLedgerSetup();
// 注册 SqlSugar ORM
builder.Services.AddSqlSugarSetup();
// 初始化数据库表结构和种子数据(BillBook、BillChannel、BillRecord、BillItemt)
builder.Services.AddLedgerInitSetup();
var app = builder.Build();
app.Run();
3. 启动并访问 Swagger
启动项目后,访问 https://localhost:xxxx/swagger,即可看到 "记账管理" 分组下的全部 RESTful API 端点。
核心场景
场景一:初始化账本数据(账本 + 收支途径 + 收支项目 + 种子数据)
// <summary>
// 一键初始化账本 —— 创建默认账本、收支途径和收支项目,并写入种子数据。
// 所有操作在事务内完成,任一步骤失败即回滚。
// </summary>
public async Task<bool> InitLedgerAsync(BookService bookService)
{
var dto = new BciDto
{
BookName = "我的账本",
ChannelName = "支付宝",
OutItemName = "餐饮",
InItemName = "工资"
};
var result = await bookService.Init(dto);
return result.Code == CodeStatus.OK;
}
// <summary>
// 同步写法 —— 仅在 Console/测试环境使用。
// ⚠️ 注意:ASP.NET 上下文中可能导致死锁。
// </summary>
public bool InitLedger(BookService bookService)
{
var dto = new BciDto
{
BookName = "我的账本",
ChannelName = "支付宝",
OutItemName = "餐饮",
InItemName = "工资"
};
return bookService.Init(dto).Code == CodeStatus.OK;
}
初始化事务流程:
校验字段(名称均不能为空)
│
开始事务
│
├─ 1. INSERT BillBook (账本)
│
├─ 2. INSERT BillChannel (收支途径)
│
├─ 3. INSERT BillItemt[] (支出项目 + 收入项目)
│
├─ 4. TypeDataSeed.Init() (资金流动字典种子数据)
│
提交 / 回滚
场景二:记录日常收支(添加账单 + 多维度查询)
// <summary>
// 记录一笔支出 —— 关联账本、收支项目、收支途径。
// </summary>
public async Task<bool> AddExpenseAsync(RecordService recordService)
{
var dto = new RecordAddDto
{
BookId = 1, // 账本 ID
ItemtId = 1, // 支出项目 ID(餐饮)
ChannelId = 1, // 收支途径 ID(支付宝)
FlowsType = 1, // 1=支出
Amount = 50.00m,
RecordTime = DateTime.Now,
Remark = "午餐"
};
var result = await recordService.AddAsync(dto);
return result.Code == CodeStatus.OK;
}
// <summary>
// 多维度分页查询账单 —— 按收支类型、账本、项目、渠道、时间范围联合筛选。
// 返回结果自动 Include Books/Itemts/CashChannels 关联实体。
// </summary>
public async Task<PageResult<BillRecord>> QueryRecordsAsync(RecordService recordService)
{
var param = new RecordPageParam
{
PageNo = 1,
PageSize = 20,
FlowsType = 1, // 仅查询支出
BookId = 1, // 指定账本
ChannelId = null, // 不限途径
BeginTime = DateTime.Now.AddDays(-30), // 近30天
EndTime = DateTime.Now
};
var result = await recordService.GetPagesAsync(param);
return result.Data;
}
// <summary>
// 同步写法 —— 记录一笔收入
// </summary>
public bool AddIncome(RecordService recordService)
{
var dto = new RecordAddDto
{
BookId = 1,
ItemtId = 2, // 收入项目 ID(工资)
ChannelId = 2, // 收支途径 ID(银行卡)
FlowsType = 2, // 2=收入
Amount = 15000.00m,
RecordTime = DateTime.Now,
Remark = "6月工资"
};
return recordService.AddAsync(dto).GetAwaiter().GetResult().Code == CodeStatus.OK;
}
场景三:数据统计分析(月度趋势 + 饼图分组 + 折线图)
// <summary>
// 获取月度收支透视表 —— 按月 x 收支类型分组,透视表格式。
// </summary>
public async Task<dynamic> GetMonthlyStatsAsync(AnalysService analysService)
{
return await analysService.PMonthly();
}
// <summary>
// 获取月度收入数据 —— 10分钟缓存,按年份和月份分组展开。
// </summary>
public async Task<dynamic> GetIncomeTrendAsync(AnalysService analysService)
{
return await analysService.MonthlyIncomeData();
}
// <summary>
// 获取月度支出数据 —— 10分钟缓存。
// </summary>
public async Task<dynamic> GetDisburseTrendAsync(AnalysService analysService)
{
return await analysService.MonthlydDisburseData();
}
// <summary>
// 获取月度结余数据(收入-支出)—— 10分钟缓存,按年份和12个月展开。
// </summary>
public async Task<List<YearMonDto>> GetSurplusTrendAsync(AnalysService analysService)
{
return await analysService.MonthlydSurplusData();
}
// <summary>
// 按支付途径分组统计支出(饼图数据源)
// </summary>
public async Task<List<PieDto>> GetChannelPieAsync(AnalysService analysService)
{
var query = new ConcurQuery
{
StartTime = DateTime.Now.AddMonths(-1),
EndTime = DateTime.Now
};
return await analysService.GroupAvenue(query);
}
// <summary>
// 按支出项目分组统计支出(饼图数据源)
// </summary>
public async Task<List<PieDto>> GetItemPieAsync(AnalysService analysService)
{
var query = new ConcurQuery
{
StartTime = DateTime.Now.AddMonths(-1),
EndTime = DateTime.Now
};
return await analysService.GroupItem(query);
}
// <summary>
// 获取日收支折线图数据 —— 按日期和筛选条件分组。
// ❗ 注意:连续调用多个统计接口时,缓存可减少重复查询,但首次调用会执行全表扫描。
// </summary>
public async Task<dynamic> GetDailyLineAsync(AnalysService analysService)
{
var query = new ConcurQuery
{
StartTime = DateTime.Now.AddMonths(-3),
EndTime = DateTime.Now,
FlowsType = 1 // 仅查询支出
};
return await analysService.BillLineData(query);
}
// <summary>
// 同步写法 —— 获取年度综合统计(按日/月/年/周)。
// </summary>
public dynamic GetYearlyStats(AnalysService analysService)
{
return analysService.GetYearlyStatistics();
}
数据缓存流程:
请求统计数据
│
├─ 构建缓存 Key (UserId + 缓存标识,如 ":MonthlyIncome")
│
├─ 从 Cache 读取
│ │
│ ├─ 命中 ──→ 直接返回
│ │
│ └─ 未命中 ──→ 查询数据库
│ │
│ ├─ 按年份+月份分组聚合
│ ├─ 透视表转换 (ToPivotList)
│ │
│ ├─ 写入 Cache (TimeSpan.FromMinutes(10))
│ └─ 返回数据
场景四:管理收支途径与项目(创建 + 安全删除)
// <summary>
// 创建自定义收支途径 —— 自动校验同名去重。
// </summary>
public async Task<bool> CreateChannelAsync(ChannelService channelService)
{
var dto = new ChannelAddDto
{
Name = "微信支付",
Sort = 20,
Remark = "日常微信扫码支付"
};
var result = await channelService.AddAsync(dto);
return result.Code == CodeStatus.OK;
}
// <summary>
// 创建自定义收支项目 —— 区分支出和收入类型。
// </summary>
public async Task<bool> CreateItemtAsync(ItemtService itemtService)
{
var outItem = new ItemtAddDto
{
Name = "交通出行",
FlowsType = 1, // 1=支出
Sort = 15
};
var inItem = new ItemtAddDto
{
Name = "兼职收入",
FlowsType = 2, // 2=收入
Sort = 20
};
var outResult = await itemtService.AddAsync(outItem);
var inResult = await itemtService.AddAsync(inItem);
return outResult.Code == CodeStatus.OK && inResult.Code == CodeStatus.OK;
}
// <summary>
// 安全删除 —— 校验关联数据和至少保留一条。
// ⚠️ 注意:当账本/途径/项目已有账单记录时,删除会被阻止并返回对应错误信息。
// </summary>
public async Task<(bool Success, string Message)> SafeDeleteChannelAsync(
ChannelService channelService, long channelId)
{
try
{
var result = await channelService.DeleteAsync(new EntityBaseId { Id = channelId });
if (result.Code == CodeStatus.OK)
return (true, result.Message ?? "删除成功");
return (false, result.Message ?? "删除失败");
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
// <summary>
// 同步写法 —— 查询所有可用收支途径(含系统默认途径)。
// </summary>
public List<BillChannel> GetAllChannels(ChannelService channelService)
{
return channelService.GetListAsync().GetAwaiter().GetResult().Data;
}
删除校验流程(以途径删除为例):
接收删除请求(EntityBaseId { Id })
│
├─ Id == 0 → 返回 i18n.fail(参数无效)
│
├─ BillRecord 中存在 ChannelId 关联 → 返回 i18n.channel_has_data
│
├─ 用户通道总数 <= 1 → 返回 i18n.last_record_cannot_delete
│
└─ 通过全部校验 → 执行 DeleteByIdAsync
│
├─ 成功 → 返回 i18n.delete_success
└─ 失败 → 返回 i18n.delete_fail
场景五:获取收支类型分类(从系统字典读取)
// <summary>
// 获取收支类型分类数据 —— 从系统字典 CASH_FLOWS 读取。
// ❗ 注意:需先在字典管理模块维护 CASH_FLOWS 数据字典,否则返回错误。
//(组件内部通过 GetDictTypeDropDown 查询,字典编码自动规范化为大写)
// </summary>
public async Task<List<object>> GetFlowsCategoriesAsync(RecordService recordService)
{
var result = await recordService.GetCategoryAsync();
if (result.Code == CodeStatus.OK)
return result.Data as List<object>;
// 错误提示:请先在字典管理维护 CASH_FLOWS 的数据字典
throw new InvalidOperationException(result.Message);
}
配置选项详解
数据库连接配置 (ConnectionConfigs)
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./ruovea.db",
// 可选配置
"EnableUnderLine": false,
"EnableDiffLog": false,
"IsEncrypt": false,
"DbSecurity": "",
"IsDeleteFilter": true,
"IsUserIdFilter": false,
"IsTenantIdFilter": false,
"CommandTimeOut": 30
}
]
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DbType |
string | 必填 | 数据库类型 |
ConnectionString |
string | 必填 | 连接字符串 |
EnableUnderLine |
bool | false |
驼峰转下划线 |
EnableDiffLog |
bool | false |
启用库表差异日志 |
IsEncrypt |
bool | false |
连接字符串是否加密 |
DbSecurity |
string | "" |
解密密钥(IsEncrypt=true 时使用) |
IsDeleteFilter |
bool | true |
⚠️ 全局软删除过滤(实体需继承 IDeletedEntity) |
IsUserIdFilter |
bool | false |
按创建者过滤(实体需继承 ICreatorFilter 或 EntityBase) |
IsTenantIdFilter |
bool | false |
按租户过滤(实体需继承 ITenantIdFilter) |
CommandTimeOut |
int | 30 |
SQL 命令超时时间(秒) |
表初始化配置 (DbInitConfig)
{
"DbInitConfig": {
"InitTable": true,
"InitSeedData": true
}
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
InitTable |
bool | false |
启动时自动检查并创建缺失的数据表(CodeFirst:BillBook、BillChannel、BillRecord、BillItemt) |
InitSeedData |
bool | false |
建表后是否写入种子数据(CASH_FLOWS 资金流动字典类型及 EXPENDITURE/INCOME 收支字典值) |
❗ 性能提醒:
AddLedgerInitSetup使用Task.Run在后台执行表检查(BillBook、BillChannel、BillRecord、BillItemt)和种子数据写入。生产环境首次启动后建议将InitTable设为false,避免每次启动都执行 4 张表的IsTableExists检查。
DI 注册配置
// <summary>
// AddOmiLedgerSetup —— 三种重载,适应不同配置来源。
// </summary>
// 重载 1:自动从全局 AppSettings.Configuration 读取
builder.Services.AddOmiLedgerSetup();
// 重载 2:传入自定义 IConfiguration
builder.Services.AddOmiLedgerSetup(configuration.GetSection("MyLedger"));
// 重载 3:通过 Action 委托配置
builder.Services.AddOmiLedgerSetup(options =>
{
options.InitTable = true;
});
// 自定义服务生命周期(可选项)
builder.Services.AddOmiLedgerSetup(ServiceLifetime.Transient);
// 带 IConfiguration + 自定义生命周期
builder.Services.AddOmiLedgerSetup(config, ServiceLifetime.Singleton);
// 带 Action + 自定义生命周期
builder.Services.AddOmiLedgerSetup(options => { options.InitTable = true; }, ServiceLifetime.Scoped);
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
serviceLifetime |
ServiceLifetime |
Scoped |
注册 5 个服务(AnalysService、BookService、ChannelService、ItemtService、RecordService)的生命周期 |
config |
IConfiguration |
— | 自定义配置节 |
config (Action) |
Action<DbInitConfig> |
— | 代码内配置 |
⚠️ 线程安全: 切换为
Singleton生命周期时,确保注入的SugarRepository<T>和ISqlSugarClient本身支持并发访问。SqlSugar 的SqlSugarClient是线程安全的,但仓储的某些操作依赖请求上下文(如Creator自动填充、ICurrentUser用户信息),单例模式下可能导致用户信息串扰。
Swagger API 版本配置
{
"Swagger": {
"ApiVersions": [
{
"Title": "记账管理",
"Version": "Ledger"
}
]
}
}
| 配置项 | 类型 | 说明 |
|---|---|---|
Title |
string | API 版本显示标题,用于 Swagger UI 中展示 |
Version |
string | API 版本标识,用于路由和文档区分 |
API 接口速览
所有接口自动归入 Swagger "bill" 分组,默认路由前缀由 DynamicWebApi 配置决定。
BookService —— 账本管理
| HTTP | 方法 | 说明 |
|---|---|---|
| POST | Init |
初始化账本数据(事务:账本 + 收支途径 + 收支项目 + 种子数据) |
| GET | GetPagesAsync |
分页查询账本列表(支持按名称模糊搜索) |
| GET | GetDetailAsync |
根据主键 ID 查询账本详情 |
| GET | GetListAsync |
查询当前用户所有可用账本列表(按 Sort 降序) |
| POST | AnyAsync |
检查当前用户是否已有账本 |
| POST | AddAsync |
添加新账本(同名去重校验) |
| PUT | UpdateAsync |
更新账本信息(不存在时返回错误) |
| DELETE | DeleteAsync |
删除账本(校验关联记录和至少保留一条) |
| POST | BatchExportAsync |
批量导出账本(当前用户全部账本,按 Sort 升序) |
| POST | BatchImportAsync |
批量导入账本(请求体传 List<BookAddDto>,自动生成 ID) |
RecordService —— 账单记录管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync |
分页查询账单记录(支持按收支类型/账本/项目/渠道/时间范围筛选,含 Books/Itemts/CashChannels 关联查询) |
| GET | GetDetailAsync |
根据主键 ID 查询账单记录详情 |
| POST | AddAsync |
添加新账单记录 |
| PUT | UpdateAsync |
更新账单记录信息 |
| DELETE | DeleteAsync |
删除账单记录 |
| POST | BatchExportAsync |
批量导出账单记录(按当前筛选条件返回全部数据,含账本/项目/途径名称) |
| POST | BatchImportAsync |
批量导入账单记录(请求体传 List<RecordAddDto>,自动生成 ID) |
| GET | GetCategoryAsync |
获取收支类型分类数据(从系统字典 CASH_FLOWS 读取) |
ChannelService —— 收支途径管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync |
分页查询收支途径列表(支持按名称模糊搜索) |
| GET | GetDetailAsync |
根据主键 ID 查询收支途径详情 |
| GET | GetListAsync |
查询所有可用收支途径列表(含系统默认途径,按 Sort 降序) |
| POST | AddAsync |
添加新收支途径(同名去重校验) |
| PUT | UpdateAsync |
更新收支途径信息 |
| DELETE | DeleteAsync |
删除收支途径(校验关联记录和至少保留一条) |
| POST | BatchExportAsync |
批量导出收支途径(当前用户全部途径) |
| POST | BatchImportAsync |
批量导入收支途径(请求体传 List<ChannelAddDto>,自动生成 ID) |
ItemtService —— 收支项目管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync |
分页查询收支项目列表(支持按收支类型和名称筛选) |
| GET | GetDetailAsync |
根据主键 ID 查询收支项目详情 |
| GET | GetListAsync |
查询所有可用收支项目列表(按收支类型、含系统默认项目,按 Sort 降序) |
| POST | AddAsync |
添加新收支项目(同名去重校验) |
| PUT | UpdateAsync |
更新收支项目信息 |
| DELETE | DeleteAsync |
删除收支项目(校验关联记录和至少保留一条) |
| POST | BatchExportAsync |
批量导出收支项目(当前用户全部项目) |
| POST | BatchImportAsync |
批量导入收支项目(请求体传 List<ItemtAddDto>,自动生成 ID) |
AnalysService —— 数据统计分析
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetYearlyStatistics |
获取当前年份按日/月/年/周四维度的汇总统计数据 |
| POST | PMonthly |
获取月度收支透视表(按月份 x 收支类型分组) |
| POST | MonthlyIncomeData |
获取月度收入数据(按年份和月份分组,10分钟缓存) |
| POST | MonthlydDisburseData |
获取月度支出数据(按年份和月份分组,10分钟缓存) |
| POST | MonthlydSurplusData |
获取月度结余数据(收入-支出,按年份和12个月展开,10分钟缓存) |
| POST | GroupAvenue |
按支付途径分组统计支出数据(饼图数据源,接受 ConcurQuery 时间范围) |
| POST | GroupItem |
按支出项目分组统计支出数据(饼图数据源,接受 ConcurQuery 时间范围) |
| POST | BillLineData |
获取日收支分析数据(按日期和筛选条件分组,折线图数据源) |
| POST | BillLineMonData |
获取月收支分析数据(按月份和筛选条件分组,折线图数据源) |
错误处理与日志
错误码速查
组件内部使用 i18n 国际化资源管理错误信息,支持 7 种语言自动切换:
| 错误 Key | 中文含义 | 触发场景 |
|---|---|---|
i18n.book_name_required |
账本名称不能为空 | Init 初始化时未提供 BookName |
i18n.channel_name_required |
收支途径名称不能为空 | Init 初始化时未提供 ChannelName |
i18n.out_item_name_required |
支出项目名称不能为空 | Init 初始化时未提供 OutItemName |
i18n.in_item_name_required |
收入项目名称不能为空 | Init 初始化时未提供 InItemName |
i18n.fail |
失败 | 通用操作失败(Id=0、参数无效等) |
i18n.title_duplicate |
标题重复 | 创建账本/途径/项目时名称已存在 |
i18n.add_fail |
添加失败 | 数据库插入操作返回 false |
i18n.not_exist |
不存在 | 更新时根据 ID 查询不到数据 |
i18n.modify_fail |
修改失败 | 数据库更新操作返回 false |
i18n.book_has_data |
此账本底下有数据,无法删除 | 删除账本前检测到关联账单记录 |
i18n.channel_has_data |
此收支途径底下有数据,无法删除 | 删除途径前检测到关联账单记录 |
i18n.itemt_has_data |
此收支项目底下有数据,无法删除 | 删除项目前检测到关联账单记录 |
i18n.last_record_cannot_delete |
只剩一行记录,无法删除 | 删除前检测用户仅剩最后一条数据 |
i18n.delete_success |
删除成功 | 删除操作成功 |
i18n.delete_fail |
删除失败 | 数据库删除操作返回 false |
i18n.cash_flows_dict_required |
请先在字典管理维护:cash_flows 的数据字典 | GetCategoryAsync 时未找到 CASH_FLOWS 字典 |
异常处理示例
// <summary>
// 安全记账 —— 捕获校验、业务和数据库异常。
// </summary>
public async Task<(bool Success, string Message)> SafeAddRecordAsync(
RecordService recordService, RecordAddDto dto)
{
try
{
var result = await recordService.AddAsync(dto);
return result.Code == CodeStatus.OK
? (true, "记账成功")
: (false, result.Message ?? "记账失败");
}
catch (ArgumentException ex)
{
// 业务校验失败(参数为空、数据不存在)
return (false, $"业务校验失败: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("transaction", StringComparison.OrdinalIgnoreCase))
{
// 事务执行失败(数据库层面错误,已自动回滚)
return (false, $"数据操作失败,已自动回滚: {ex.Message}");
}
catch (Exception ex)
{
// 未知错误
return (false, $"未知错误: {ex.Message}");
}
}
// <summary>
// 同步写法 —— 安全初始化账本。仅在非 ASP.NET 上下文使用。
// </summary>
public (bool Success, string Message) SafeInitLedger(BookService bookService, BciDto dto)
{
try
{
var result = bookService.Init(dto);
return result.Code == CodeStatus.OK
? (true, "初始化成功")
: (false, result.Message);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
日志集成
组件不直接输出日志,依赖调用方集成的日志框架。推荐在 Program.cs 中配置 Serilog 或 NLog 来捕获:
// SqlSugar 的 SQL 日志可通过 AOP 事件捕获
builder.Services.AddSqlSugarSetup(); // 内部配置了 SQL 执行日志
版本迁移指南
从 8.0.x 升级到 10.0.x
| 变更项 | 说明 |
|---|---|
| TFM 升级 | net8.0 → net10.0,需同步升级依赖包到 10.0.* 版本 |
| 包版本对齐 | RuoVea.OmiApi.Dict、RuoVea.DynamicWebApi、RuoVea.ExSugar 需升至对应 10.0.* |
| API 兼容 | 所有公开 API 向后兼容,无需修改业务代码 |
| 数据库 | 表结构无变更,无需执行迁移脚本 |
API 变更历史
| 版本 | 变更 |
|---|---|
8.0.1.5 |
新增账本/途径/项目/账单的批量导出(BatchExportAsync)与批量导入(BatchImportAsync)接口;建表逻辑统一,种子数据写入由 InitSeedData 控制 |
8.0.0.5 |
修复多字段查询时 SqlSugar 因重复参数 @value 键报错的问题 |
8.0.0.4 |
组件版本升级,图表数据缓存(月度收入/支出/结余 10 分钟缓存) |
8.0.0.3 |
表结构初始化处理、种子数据初始化 |
| 更早版本 | 初始发布 |
常见问题
Q: 如何切换数据库?
修改 appsettings.json 中的 DbType 和 ConnectionString,然后重新运行。AddLedgerInitSetup 会自动为新数据库创建 4 张表(BillBook、BillChannel、BillRecord、BillItemt)并写入种子数据。
// MySql 示例
{ "DbType": "MySql", "ConnectionString": "Server=localhost;Database=ruovea;Uid=root;Pwd=123456;" }
// SqlServer 示例
{ "DbType": "SqlServer", "ConnectionString": "Server=.;Database=ruovea;Trusted_Connection=True;" }
// PostgreSQL 示例
{ "DbType": "PostgreSQL", "ConnectionString": "Host=localhost;Database=ruovea;Username=postgres;Password=123456;" }
// Oracle 示例
{ "DbType": "Oracle", "ConnectionString": "Data Source=localhost/ORCL;User Id=ruovea;Password=123456;" }
Q: 如何自定义 API 路由前缀?
在 AddDynamicWebApi 中配置 DefaultApiPrefix:
// <summary>
// 自定义路由前缀示例
// </summary>
builder.Services.AddDynamicWebApi(options =>
{
options.DefaultApiPrefix = "/openapi/api";
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
Q: ⚠️ 数据删除前为何被阻止?
当账本、收支途径或收支项目已被账单记录引用时,组件会阻止删除并返回对应错误信息(i18n.book_has_data / i18n.channel_has_data / i18n.itemt_has_data)。这是为了防止数据孤岛。
解决方案:先将关联的账单记录全部删除或转移,然后再执行删除操作。
Q: ⚠️ 什么是"最后一条记录保护"?
当用户只有最后一条账本/途径/项目数据时,组件阻止删除(返回 i18n.last_record_cannot_delete)。这是防止用户误删导致系统无法正常使用的保护机制。如需强制删除最后一条,请直接操作数据库。
Q: ❗ 统计数据缓存什么时候刷新?
月度收入(MonthlyIncomeData)、月度支出(MonthlydDisburseData)、月度结余(MonthlydSurplusData)三个接口使用 10 分钟内存缓存。在添加/修改/删除账单记录后,统计缓存不会自动失效,后续 10 分钟内的请求将返回旧数据。
如需立即刷新,可重启应用或等待缓存过期(10分钟)。
Q: ❗ 为什么数据分析接口返回空数据?
可能原因:
- 当前用户尚未添加任何账单记录
ConcurQuery的时间范围设置不正确(StartTime/EndTime 为空或范围不匹配)- 涉及途径/项目分组统计时,检查关联表是否已有数据
Q: GetCategoryAsync 返回错误怎么办?
GetCategoryAsync 依赖系统字典模块的 CASH_FLOWS 字典类型。如果未维护,会返回 i18n.cash_flows_dict_required 错误。
解决方案:在字典管理(RuoVea.OmiApi.Dict)中创建 Code 为 CASH_FLOWS 的字典类型,并添加字典值(如"支出"/"收入"等)。编码不区分大小写,组件查询时会自动规范化为大写。
Q: 账本/途径/项目可以设置为默认项吗?
每个表都有 IsDefault 字段(YesOrNot 枚举)。系统默认项通过 SeedData/TypeDataSeed 在初始化时写入,且 GetListAsync 查询时会同时返回用户自定义项和系统默认项(Creat == UserId || IsDefault == Y)。
Q: 支持生产环境和开发环境使用不同的路由前缀吗?
推荐根据环境动态配置:
// <summary>
// 开发环境使用默认路由,生产环境添加 /openapi/api 前缀。
// </summary>
if (builder.Environment.IsProduction())
{
builder.Services.AddDynamicWebApi(options =>
{
options.DefaultApiPrefix = "/openapi/api";
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
}
else
{
builder.Services.AddDynamicWebApi(options =>
{
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
}
许可证
本项目基于 Apache 2.0 License 开源发布。
| 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
- RuoVea.DynamicWebApi (>= 10.0.0)
- RuoVea.ExSugar (>= 10.0.1.3)
- RuoVea.OmiApi.Dict (>= 10.0.1.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.1.5 | 99 | 8/20/2026 |
| 10.0.1.4 | 104 | 8/16/2026 |
| 10.0.1.3 | 95 | 8/15/2026 |
| 10.0.1.2 | 94 | 8/15/2026 |
| 10.0.1.1 | 101 | 8/15/2026 |
| 10.0.1 | 93 | 8/14/2026 |
| 10.0.0.11 | 103 | 7/17/2026 |
| 10.0.0.10 | 109 | 7/15/2026 |
| 10.0.0.9 | 111 | 7/7/2026 |
| 8.0.1.6 | 91 | 8/20/2026 |
| 8.0.1.5 | 93 | 8/18/2026 |
| 8.0.1.4 | 109 | 8/16/2026 |
| 8.0.1.3 | 94 | 8/15/2026 |
| 8.0.1.2 | 90 | 8/15/2026 |
| 8.0.1.1 | 112 | 8/15/2026 |
| 8.0.1 | 100 | 8/14/2026 |
| 8.0.0.14 | 101 | 7/17/2026 |
| 8.0.0.13 | 106 | 7/15/2026 |
| 8.0.0.12 | 116 | 7/7/2026 |
| 8.0.0.11 | 139 | 6/29/2026 |