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
                    
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="RuoVea.OmiApi.Ledger" Version="10.0.1.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RuoVea.OmiApi.Ledger" Version="10.0.1.5" />
                    
Directory.Packages.props
<PackageReference Include="RuoVea.OmiApi.Ledger" />
                    
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 RuoVea.OmiApi.Ledger --version 10.0.1.5
                    
#r "nuget: RuoVea.OmiApi.Ledger, 10.0.1.5"
                    
#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 RuoVea.OmiApi.Ledger@10.0.1.5
                    
#: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=RuoVea.OmiApi.Ledger&version=10.0.1.5
                    
Install as a Cake Addin
#tool nuget:?package=RuoVea.OmiApi.Ledger&version=10.0.1.5
                    
Install as a Cake Tool

RuoVea.OmiApi.Ledger

账本管理模块 —— 基于 .NET 构建的轻量级、跨平台记账管理系统后端。

RuoVea.OmiApi.Ledger 是一个开箱即用的记账管理 NuGet 包,提供账本管理、账单记录、收支途径、收支项目、数据统计分析等完整能力。基于 SqlSugar ORMDynamicWebApi,注册即自动生成 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 值: MySqlSqlServerSqliteOraclePostgreSQLDmKdbndpOpenGaussClickHouseAccessGBase 等。

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 按创建者过滤(实体需继承 ICreatorFilterEntityBase
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.0net10.0,需同步升级依赖包到 10.0.* 版本
包版本对齐 RuoVea.OmiApi.DictRuoVea.DynamicWebApiRuoVea.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 中的 DbTypeConnectionString,然后重新运行。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: ❗ 为什么数据分析接口返回空数据?

可能原因:

  1. 当前用户尚未添加任何账单记录
  2. ConcurQuery 的时间范围设置不正确(StartTime/EndTime 为空或范围不匹配)
  3. 涉及途径/项目分组统计时,检查关联表是否已有数据

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 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

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
Loading failed