Finance 是”域中有域”。公共基础包只提供共享的领域异常 FinanceException,下面分发票(Invoice)、税务(Tax)、薪酬(Payroll)三个子域,每个子域各自完整地走三层包模式。总聚合包把三个子域全部拉入。
本页是完整手册:从嵌套结构到三个子域各自的接口契约、能力探针、异常体系和注册方式。
为什么嵌套
发票、税务、薪酬三个业务虽然都属于财务范畴,但它们的 API 能力、业务流程差异很大:
- 发票管开具、红冲、查验、交付;
- 税务管申报、风险查询、退税;
- 薪酬管算薪、个税、代发、社保。
把它们塞进一个扁平的三层结构,要么接口臃肿(单个接口塞 20+ 方法),要么厂商被迫实现一堆空方法。嵌套域让每个子域保持独立的接口契约和厂商实现,同时通过共享的 FinanceException 保持一致的错误格式。
域结构
Finance(公共基础)│ └── FinanceException.cs ← 共享领域异常(domain = "Finance")│├── Finance.Invoice ← 发票子域│ ├── Finance.Invoice.Baiwang ← 百望│ ├── Finance.Invoice.Kingdee ← 金蝶│ ├── Finance.Invoice.Nuonuo ← 诺诺│ └── Finance.Invoice.All ← 发票聚合(3 厂商)│├── Finance.Tax ← 税务子域│ └── Finance.Tax.QiXiangYun ← 企享云│├── Finance.Payroll ← 薪酬子域│ └── Finance.Payroll.All ← 薪酬聚合(暂无厂商实现)│└── Finance.All ← 总聚合(拉入全部子域)公共基础:FinanceException
FinanceException 继承 IntegrationException,domain 固定为 "Finance",是三个子域异常的共同基类:
public class FinanceException : IntegrationException{ public string ProviderName => Provider ?? string.Empty; public new string? ErrorCode => base.ErrorCode; public new string? ProviderMessage => base.ProviderMessage;
// 消息格式: "[{providerName}] {message}" public FinanceException(string providerName, string message, string? errorCode = null, string? providerMessage = null, Exception? innerException = null) : base(domain: "Finance", ...) { }}InvoiceException、PayrollException 都继承 FinanceException(补充本子域上下文)。TaxException 是例外——它直接继承 System.Exception,不经过 FinanceException/IntegrationException,因为税务子域有独立的错误码体系。
子域对照表
| 子域 | 抽象接口 | 异常 | 厂商实现 | 聚合包 |
|---|---|---|---|---|
| Invoice 发票 | IInvoiceProvider + 2 能力探针 | InvoiceException : FinanceException | 百望 / 金蝶 / 诺诺 | Finance.Invoice.All |
| Tax 税务 | 6 个细粒度接口 | TaxException : Exception | 企享云 | — |
| Payroll 薪酬 | IPayrollProvider | PayrollException : FinanceException | 暂无 | Finance.Payroll.All |
| Finance 总聚合 | — | — | — | Finance.All |
Invoice 发票子域
IInvoiceProvider 涵盖发票开具、红冲、作废、查询、查验、交付,共 7 个方法:
public interface IInvoiceProvider{ string ProviderName { get; }
// 开票 Task<InvoiceIssueResult> IssueAsync(InvoiceIssueRequest request, CancellationToken ct = default); Task<InvoiceIssueResult> RedFlushAsync(InvoiceRedFlushRequest request, CancellationToken ct = default); Task CancelAsync(string? invoiceCode, string invoiceNumber, CancellationToken ct = default);
// 查询与查验 Task<InvoiceQueryResult> QueryAsync(DateTime? startDate = null, DateTime? endDate = null, int pageIndex = 1, int pageSize = 20, CancellationToken ct = default); Task<InvoiceVerifyResult> VerifyAsync(InvoiceVerifyRequest request, CancellationToken ct = default); Task<InvoiceInfo> GetDetailAsync(string? invoiceCode, string invoiceNumber, CancellationToken ct = default);
// 交付 Task DeliverAsync(InvoiceDeliveryRequest request, CancellationToken ct = default);}invoiceCode 参数在数电票场景传 null(数电票没有发票代码,只有号码)。IssueAsync 返回的 InvoiceIssueResult 在异步开票模式下可能 Invoice 字段为 null,需通过 QueryAsync 查询最终结果。
开票请求模型
InvoiceIssueRequest 的核心字段:
| 属性 | 类型 | 说明 |
|---|---|---|
OrderNo | string | 业务订单号,每销方税号下唯一(幂等控制) |
InvoiceType | InvoiceType | 发票类型(数电票/专票/普票等 8 种) |
Buyer | BuyerInfo | 购方信息(名称、税号、邮箱、手机) |
SellerTaxNumber | string? | 销方税号(多税号企业指定) |
Items | IReadOnlyList<InvoiceItem> | 发票明细行 |
CallbackUrl | string? | 异步回调地址 |
PushMode | InvoiceDeliveryMethod? | 推送方式(邮件/短信/两者) |
能力探针接口
供应商特有能力通过独立接口提供,使用前用 is 运算符检查(能力探针模式),而不是塞进主接口让所有厂商实现空方法:
| 探针接口 | 方法 | 能力 |
|---|---|---|
IInvoiceOcrProvider | RecognizeInvoiceAsync / RecognizeAndVerifyAsync | 发票 OCR 识别与识别后查验 |
ITaxCodeMatchProvider | MatchTaxCodeAsync | 税收分类编码智能匹配 |
// 使用前检查能力if (invoice is IInvoiceOcrProvider ocr){ var result = await ocr.RecognizeInvoiceAsync(imageBytes); // 处理 OCR 结果...}发票状态
InvoiceStatus 枚举覆盖开票全流程:
| 值 | 名称 | 说明 |
|---|---|---|
| 0 | Pending | 待开票 |
| 1 | Issuing | 开票中 |
| 2 | Signing | 签章中 |
| 3 | Issued | 已开具 |
| 4 | Failed | 开票失败 |
| 5 | SignFailed | 签章失败 |
| 6 | Cancelled | 已作废 |
| 7 | Cancelling | 作废中 |
| 8 | RedFlushed | 已红冲 |
Invoice 厂商
| 厂商 | Provider ID | DI 方法 |
|---|---|---|
| 百望 | Baiwang | AddBitzsoftBaiwangInvoice() |
| 金蝶 | Kingdee | AddBitzsoftKingdeeInvoice() |
| 诺诺 | Nuonuo | AddBitzsoftNuonuoInvoice() |
三家是国内主流电子发票服务商,均支持数电票。InvoiceException : FinanceException,错误格式与其他财务子域一致。
Tax 税务子域
税务子域用细粒度多接口而非单一接口,按业务能力拆成 6 个独立接口。每个接口都只有 2-3 个方法,调用方按需注入:
| 接口 | 覆盖场景 | 方法数 |
|---|---|---|
ITaxLoginProvider | 税务局登录、短信验证码 | 2 |
ITaxDeclarationProvider | 申报目录、提交、结果查询 | 3 |
ITaxInvoiceProvider | 发票查验、OCR 识别、查询、详情 | 4 |
ITaxDataQueryProvider | 企业风险、企业 360 数据查询 | 2 |
ITaxCustomsProvider | 海关报关单、出口退税 | 2 |
ITaxRegulationProvider | 政策法规分类查询、详情 | 2 |
拆成 6 个接口的原因是税务系统的功能跨度太大——登录认证、申报、发票、风控、海关、法规查询完全是不同的业务域和 API 分组。单接口会让调用方和厂商都负担过重,细粒度接口让每个能力可独立注入和演进。
TaxApiResult<T>
所有税务接口方法统一返回 TaxApiResult<T>(注意:是可变属性 set,和其他域的 init 不同):
public class TaxApiResult<T>{ public bool Success { get; set; } public string? Code { get; set; } public string? Message { get; set; } public T? Data { get; set; } public long? Timestamp { get; set; }}异步任务还有配套的 AsyncTaskResult<T>(含 IsCompleted / IsSuccess / Status / ErrorCode / ErrorMessage),用于申报等需要轮询的场景。
TaxException
税务子域的异常体系独立于 FinanceException——TaxException 直接继承 System.Exception:
public class TaxException : Exception{ public string? ErrorCode { get; } // 错误码 public string? ErrorDetail { get; } // 错误详情
public TaxException(string message, string? errorCode = null, string? errorDetail = null) : base(message) { }}配套的 TaxErrorCode 枚举提供标准化错误码:
| 值 | 名称 | 说明 |
|---|---|---|
| 0 | Unknown | 未知错误 |
| 1001 | AuthenticationFailed | 认证失败 |
| 1002 | TokenExpired | 令牌过期 |
| 2001 | InvalidParameter | 无效参数 |
| 3000 | BusinessError | 业务错误 |
| 4001 | NetworkError | 网络错误 |
| 5001 | AsyncTaskTimeout | 异步任务超时 |
| 6001 | ConfigurationError | 配置错误 |
Tax 厂商
| 厂商 | Provider ID | DI 方法 | 实现接口 |
|---|---|---|---|
| 企享云 | QiXiangYun | AddBitzsoftQiXiangYunTax() | 全部 6 个 |
目前只有企享云实现了这套接口。
Payroll 薪酬子域
IPayrollProvider 涵盖薪资计算、个税计算、薪资代发、社保管理、工资单推送,共 8 个方法:
public interface IPayrollProvider{ string ProviderName { get; }
// 薪资计算 Task<PayrollCalculationResult> CalculateAsync(PayrollCalculationRequest request, CancellationToken ct = default); Task<TaxCalculationResult> CalculateTaxAsync(TaxCalculationRequest request, CancellationToken ct = default);
// 代发 Task<SalaryDisbursementResult> DisburseAsync(SalaryDisbursementRequest request, CancellationToken ct = default); Task<SalaryDisbursementResult> GetDisbursementStatusAsync(string batchId, CancellationToken ct = default);
// 社保 Task<SocialInsuranceResult> EnrollAsync(SocialInsuranceRequest request, CancellationToken ct = default); // 参保 Task<SocialInsuranceResult> SuspendAsync(SocialInsuranceRequest request, CancellationToken ct = default); // 停保 Task<SocialInsuranceResult> DeclareAsync(SocialInsuranceRequest request, CancellationToken ct = default); // 申报
// 工资单 Task SendPayslipAsync(string employeeId, string period, CancellationToken ct = default); // period 格式 yyyy-MM}社保的三个操作(EnrollAsync / SuspendAsync / DeclareAsync)共用同一个 SocialInsuranceRequest 入参,返回 SocialInsuranceResult。PayrollException : FinanceException,与发票子域错误格式一致。
目前只有抽象层和聚合包,还没有厂商实现,属于预留架构。
注册
Invoice 单厂商
// ① 发票——百望builder.Services.AddBitzsoftBaiwangInvoice(builder.Configuration.GetSection("Finance:Invoice:Baiwang"));
// ② 发票——金蝶builder.Services.AddBitzsoftKingdeeInvoice(builder.Configuration.GetSection("Finance:Invoice:Kingdee"));
// ③ 发票——诺诺builder.Services.AddBitzsoftNuonuoInvoice(builder.Configuration.GetSection("Finance:Invoice:Nuonuo"));Tax 单厂商
// 税务——企享云builder.Services.AddBitzsoftQiXiangYunTax(builder.Configuration.GetSection("Finance:Tax:QiXiangYun"));子域聚合
// ① 发票全厂商聚合(百望/金蝶/诺诺按配置注册)builder.Services.AddBitzsoftInvoiceAll(builder.Configuration, "Finance:Invoice");
// ② 薪酬聚合(当前无厂商,仅注册抽象层)builder.Services.AddBitzsoftPayrollAll(builder.Configuration, "Finance:Payroll");总聚合
// 拉入发票 + 税务 + 薪酬全部子域builder.Services.AddBitzsoftFinanceAll(builder.Configuration, "Finance");配置按子域分节:
{ "Finance": { "Invoice": { "Baiwang": { "AppKey": "...", "AppSecret": "...", "TaxNumber": "..." }, "Nuonuo": { "AccessToken": "...", "UserTax": "..." } }, "Tax": { "QiXiangYun": { "ApiKey": "...", "ApiSecret": "..." } } }}消费
发票开具
public class InvoiceService(IInvoiceProvider invoice){ public async Task<InvoiceIssueResult> IssueAsync() { return await invoice.IssueAsync(new InvoiceIssueRequest { OrderNo = "ORD-20260801-001", InvoiceType = InvoiceType.DigitalNormal, Buyer = new BuyerInfo { Name = "买方公司", TaxNumber = "91...", Email = "finance@example.com" }, Items = [new InvoiceItem { GoodsName = "咨询服务", Price = 10000m, Quantity = 1, TaxRate = 0.06m }], PushMode = InvoiceDeliveryMethod.Email, }); }}发票能力探针
public class InvoiceOcrService(IInvoiceProvider invoice){ public async Task<OCRResult?> RecognizeAsync(byte[] image) { // 使用前检查能力探针 if (invoice is not IInvoiceOcrProvider ocr) return null; return await ocr.RecognizeInvoiceAsync(image); }}税务风险查询(企享云)
public class TaxRiskService(ITaxDataQueryProvider dataQuery){ public async Task<TaxApiResult<object>> CheckRiskAsync(string taxNumber) { var result = await dataQuery.QueryEnterpriseRiskAsync(taxNumber); if (!result.Success) throw new TaxException(result.Message ?? "查询失败", errorCode: result.Code); return result; }}薪资计算(预留)
public class PayrollService(IPayrollProvider payroll){ public async Task<PayrollCalculationResult> CalculateAsync(PayrollCalculationRequest request) { return await payroll.CalculateAsync(request); }}