tboss-oa-architecture.md 28 KB

TBOSS OA 模块 — 系统架构设计

版本:v1.0 | 日期:2026-06-04 | 基于 PRD v1.0


1. 整体分层架构

graph TB
    subgraph 客户端层["客户端层 — Flutter 3.38.10"]
        direction LR
        A1[Android 宿主<br/>Java/Kotlin]
        A2[iOS 宿主<br/>OC / Xcode 14.2]
        A3[Flutter OA Module<br/>业务表单 / 公告 / 报表 / 权限管理]
        A1 --- A3
        A2 --- A3
    end

    subgraph 通道层["Platform Channel"]
        B1[MethodChannel<br/>相机 / 相册 / 通讯录<br/>地图选点 / GPS 定位]
    end

    subgraph 服务端层[".NET Framework 4.8 服务端"]
        direction TB
        C1["OA API Controller<br/>RESTful HTTP API"]

        subgraph 业务服务层
            C2["权限服务<br/>OaPermission / ACL"]
            C3["单据服务<br/>报销 / 事前 / 加班 / 用车 / 外勤"]
            C4["公告服务<br/>发布 / 触达审计 / DING"]
            C5["报表服务<br/>5 大明细报表 / 导出"]
        end

        subgraph 集成适配层
            C6["审批适配器<br/>ErpAAdapter / ErpBAdapter"]
            C7["消息适配器<br/>App Push / 站内消息"]
            C8["预算适配器<br/>BudgetService"]
            C9["项目/科目适配器<br/>ProjectService / SubjectService"]
            C10["成本中心适配器<br/>CostCenterService"]
            C11["汇率适配器<br/>ExchangeRateService"]
            C12["客户适配器<br/>CustomerService"]
        end

        C1 --> C2
        C1 --> C3
        C1 --> C4
        C1 --> C5
        C2 --> C12
        C3 --> C6
        C3 --> C8
        C3 --> C9
        C3 --> C10
        C3 --> C11
        C4 --> C7
    end

    subgraph 数据层["数据层"]
        D1[("SQL Server 2019+<br/>OA 业务库<br/>单据 / 公告 / 权限 / 车辆")]
    end

    subgraph 外部系统["外部系统"]
        direction LR
        E1["ERP-A<br/>审批引擎 + 主数据"]
        E2["ERP-B<br/>审批引擎 + 主数据"]
        E3["ERP-C<br/>审批引擎 + 主数据"]
    end

    A3 -->|"HTTP REST"| C1
    A3 -.->|"MethodChannel"| B1
    C3 --> D1
    C4 --> D1
    C2 --> D1
    C6 -->|"审批 API<br/>(签名不同)"| E1
    C6 --> E2
    C6 --> E3
    C8 -->|"主数据查询"| E1
    C8 --> E2
    C8 --> E3
    C7 -->|"App Push"| A3

分层职责

职责 不负责
客户端层 UI 渲染、表单校验、本地缓存、原生能力调用 审批逻辑、权限判定、数据聚合
通道层 桥接 Flutter 与原生 SDK(相机/GPS/通讯录/地图) 业务逻辑
服务端层 REST API、业务逻辑、多 ERP 适配、消息推送触发 最终审批执行(由 ERP 完成)
数据层 OA 业务数据持久化 用户/组织/客户/审批数据存储
外部系统 审批引擎、用户主数据、客户主数据 OA 特有的业务表单

2. 技术选型

技术 版本 选型理由
移动端框架 Flutter 3.38.10 跨平台代码复用;已有 Flutter Module 嵌入 Android/iOS 宿主
Android 宿主 Java/Kotlin 现有 ERP App 技术栈,MethodChannel 互调已验证
iOS 宿主 Objective-C Xcode 14.2 现有 ERP App 技术栈;Flutter 3.38.10 兼容已验证
后端框架 .NET Framework 4.8 4.8 现有服务端技术栈;团队熟悉;已有消息/审批代理模块可复用
数据访问 Dapper 轻量高效;复杂 SQL 手写场景优于 EF;与现有服务端一致
数据库 SQL Server 2019+ 2019+ 现有基础设施;与 ERP 同生态
HTTP 客户端 Dio Flutter 端标准 HTTP 库;拦截器/超时/重试
路由 GoRouter 声明式路由;Deep Link 支持
状态管理 Provider / Riverpod 轻量;全局角色/权限状态注入
UI 组件库 TDesign Flutter 企业级组件库;项目已统一采用
图表 fl_chart 报表趋势图/柱状图
原生互调 MethodChannel Flutter ↔ 原生双向通信

不选型的理由

未选 理由
独立用户体系 ERP 已有用户/组织数据,避免双写和同步
自建审批引擎 ERP 审批引擎成熟且多套在用,OA 只做消费端
自建消息推送 .NET 服务端已有完整推送链路
Flutter 独立 App 必须是 App 内嵌模块,不能是独立应用
Entity Framework 团队偏好 Dapper;现有服务端已是 Dapper

3. 模块划分

tboss_oa_module/lib/
├── main.dart                          # 入口
├── app.dart                           # MaterialApp + GoRouter + Provider 注入
│
├── core/
│   ├── router/app_router.dart         # 路由表 + Deep Link 处理
│   ├── network/
│   │   ├── api_client.dart            # Dio 实例(base URL / 拦截器 / 超时)
│   │   ├── api_response.dart          # 统一响应模型
│   │   └── api_exception.dart         # 业务异常
│   ├── auth/
│   │   └── auth_service.dart          # Token 管理 + 401 处理
│   ├── theme/
│   │   ├── app_colors.dart            # 色彩 Token
│   │   └── app_theme.dart             # ThemeData
│   ├── utils/
│   │   ├── date_utils.dart
│   │   ├── responsive.dart            # 横竖屏适配
│   │   └── validators.dart            # 表单校验
│   └── i18n/
│       ├── app_localizations.dart
│       └── locale_provider.dart
│
├── shared/
│   ├── models/
│   │   ├── user_model.dart            # 用户模型(从 ERP API 映射)
│   │   ├── approval_status.dart       # 审批状态枚举
│   │   └── pagination_model.dart      # 分页模型
│   └── widgets/
│       ├── app_card.dart
│       ├── status_tag.dart            # 状态标签组件
│       ├── status_banner.dart         # 详情页状态横幅
│       ├── list_card.dart             # 列表卡片
│       ├── action_bar.dart            # 底部操作栏
│       ├── filter_tabs.dart           # Chip 筛选条
│       ├── form_section.dart          # 表单分组
│       ├── form_field_row.dart        # 表单行
│       ├── section_card.dart          # 分区卡片
│       ├── empty_state.dart           # 空状态
│       ├── loading_widget.dart        # 骨架屏/菊花
│       ├── approval_actions.dart      # 审批操作栏(经理版)
│       ├── approval_timeline.dart     # 审批时间线
│       ├── report_filter_bar.dart     # 报表筛选
│       ├── message_item.dart          # 消息卡片
│       ├── profile_menu_item.dart     # 个人中心菜单项
│       └── pencil_nav_bar.dart        # 底部导航栏
│
├── features/
│   ├── shell/
│   │   ├── app_shell.dart             # Appshell(底部 Tab)
│   │   └── nav_bar_config.dart
│   ├── home/
│   │   ├── home_page.dart             # 工作台
│   │   └── home_controller.dart
│   ├── messages/
│   │   ├── message_list_page.dart
│   │   ├── message_controller.dart
│   │   └── message_model.dart
│   ├── profile/
│   │   └── profile_page.dart
│   ├── expense_application/
│   │   ├── expense_application_api.dart
│   │   ├── expense_application_model.dart
│   │   ├── expense_application_apply_page.dart
│   │   ├── expense_application_list_page.dart
│   │   ├── expense_application_list_controller.dart
│   │   └── expense_application_detail_page.dart
│   ├── expense/
│   │   ├── expense_api.dart
│   │   ├── expense_model.dart
│   │   ├── expense_apply_page.dart
│   │   ├── expense_list_page.dart
│   │   ├── expense_list_controller.dart
│   │   └── expense_detail_page.dart
│   ├── overtime/
│   │   ├── overtime_api.dart
│   │   ├── overtime_model.dart
│   │   ├── overtime_apply_page.dart
│   │   ├── overtime_list_page.dart
│   │   ├── overtime_list_controller.dart
│   │   └── overtime_detail_page.dart
│   ├── vehicle/
│   │   ├── vehicle_api.dart
│   │   ├── vehicle_model.dart
│   │   ├── vehicle_apply_page.dart
│   │   ├── vehicle_list_page.dart
│   │   ├── vehicle_list_controller.dart
│   │   └── vehicle_detail_page.dart
│   ├── outing_log/
│   │   ├── outing_log_api.dart
│   │   ├── outing_log_model.dart
│   │   ├── outing_log_create_page.dart
│   │   ├── outing_log_list_page.dart
│   │   ├── outing_log_list_controller.dart
│   │   └── outing_log_detail_page.dart
│   ├── announcement/
│   │   ├── announcement_api.dart
│   │   ├── announcement_model.dart
│   │   ├── announcement_list_page.dart
│   │   ├── announcement_list_controller.dart
│   │   ├── announcement_detail_page.dart
│   │   └── announcement_create_page.dart
│   ├── report/
│   │   ├── expense_apply_detail_report_page.dart
│   │   ├── expense_detail_report_page.dart
│   │   ├── overtime_detail_report_page.dart
│   │   ├── vehicle_detail_report_page.dart
│   │   └── outing_log_report_page.dart
│   └── admin/
│       └── admin_permissions_page.dart
│
└── .gitkeep 文件(各模块目录占位)

模块依赖关系

graph LR
    subgraph features["features/"]
        home["home"]
        expense["expense"]
        expense_apply["expense_application"]
        overtime["overtime"]
        vehicle["vehicle"]
        outing_log["outing_log"]
        announcement["announcement"]
        report["report"]
        admin["admin"]
        messages["messages"]
        profile["profile"]
        shell["shell"]
    end

    subgraph shared["shared/"]
        widgets["widgets"]
        models["models"]
    end

    subgraph core["core/"]
        router["router"]
        network["network"]
        auth["auth"]
        theme["theme"]
        utils["utils"]
        i18n["i18n"]
    end

    home --> shared
    expense --> shared
    announcement --> shared
    report --> shared
    admin --> shared
    shell --> router
    shared --> core

所有 feature 模块依赖 shared/core/shell 依赖 router 管理 Tab 切换。模块之间不直接相互依赖,页面跳转通过 GoRouter 完成。


4. .NET 服务端模块划分

Tboss.OA.Server/
├── Controllers/
│   ├── OaPermissionController.cs      # 权限管理 API
│   ├── OaExpenseApplyController.cs     # 事前申请 API
│   ├── OaExpenseController.cs          # 费用报销 API
│   ├── OaOvertimeController.cs         # 加班申请 API
│   ├── OaVehicleController.cs          # 用车申请 API
│   ├── OaOutingLogController.cs        # 外勤日志 API
│   ├── OaAnnouncementController.cs     # 公告管理 API
│   ├── OaReportController.cs          # 报表 API
│   └── OaApprovalController.cs        # 审批代理 API
│
├── Services/
│   ├── PermissionService.cs           # 权限校验 + 套餐管理
│   ├── ExpenseApplyService.cs
│   ├── ExpenseService.cs
│   ├── OvertimeService.cs
│   ├── VehicleService.cs
│   ├── OutingLogService.cs
│   ├── AnnouncementService.cs
│   ├── ReportService.cs
│   └── ApplicationNoGenerator.cs      # 单据编号原子生成
│
├── ErpIntegration/                     # ERP 集成适配器(V1 共 7 个)
│   ├── IApprovalAdapter.cs            # 审批:创建实例/查询状态/执行动作/撤回
│   ├── IBudgetAdapter.cs              # 预算:余额查询/冻结/扣减/释放
│   ├── IProjectAdapter.cs             # 项目:列表/级联查询
│   ├── ISubjectAdapter.cs             # 预算科目:按项目查科目列表
│   ├── ICostCenterAdapter.cs          # 成本中心:列表查询
│   ├── IExchangeRateAdapter.cs        # 汇率:币种→汇率查询
│   ├── ICustomerAdapter.cs            # 客户:搜索/创建
│   ├── NullAdapters.cs                # 空实现集合(ERP 无能力时返回空/不限)
│   └── ErpAdapterFactory.cs           # 按配置选取实现
│
├── Data/
│   ├── Repositories/                  # Dapper 数据访问
│   └── Sql/                           # SQL 脚本 / 迁移
│
├── Models/
│   ├── Dto/                           # 请求/响应 DTO
│   └── Entity/                        # 数据库实体映射
│
└── Infrastructure/
    ├── ErpClient.cs                   # ERP HTTP 客户端封装
    ├── MessageClient.cs               # 消息推送客户端
    └── CacheManager.cs                # 报表数据缓存

5. 核心接口清单

5.1 OA 权限

方法 URL 说明
GET /api/oa/permissions 获取权限点列表
GET /api/oa/user-permissions?userId= 获取用户权限
PUT /api/oa/user-permissions 保存用户权限
GET /api/oa/permission-changelog?userId=&page= 权限变更审计日志

5.2 业务单据

方法 URL 说明
POST /api/oa/expense-apply/submit 提交事前申请
POST /api/oa/expense/submit 提交费用报销
POST /api/oa/overtime/submit 提交加班申请
POST /api/oa/vehicle/submit 提交用车申请
PUT /api/oa/{bizType}/draft 存草稿
GET /api/oa/{bizType}/list?status=&page= 列表查询
GET /api/oa/{bizType}/detail/:id 详情查询
DELETE /api/oa/{bizType}/:id 软删除单据

5.3 外勤日志

方法 URL 说明
PUT /api/oa/outing-log/submit 提交外勤日志
PUT /api/oa/outing-log/:id/view 标记已查看
POST /api/oa/outing-log/:id/comment 主管点评

5.4 审批代理

方法 URL 说明
GET /api/oa/approval/pending?userId=&bizType=&page= 待审批列表
GET /api/oa/approval/pending-count?userId= 待审批数量
GET /api/oa/approval/timeline?bizType=&bizId= 审批时间线
POST /api/oa/approval/action 审批动作(同意/拒绝)
POST /api/oa/approval/withdraw 撤回申请
GET /api/oa/approval/my-history?userId=&page= 我的审批历史
GET /api/oa/approval/subordinates?approverId=&bizType=&page= 下属单据列表

5.5 公告

方法 URL 说明
POST /api/oa/announcement/publish 发布/更新公告
POST /api/oa/announcement/:id/read 标记已读
POST /api/oa/announcement/:id/ding DING 催办
GET /api/oa/announcement/read-stats/:id 已读/未读统计

5.6 报表

方法 URL 说明
GET /api/oa/report/expense-apply?range=&userId= 事前申请报表
GET /api/oa/report/expense?range=&userId= 费用报销报表
GET /api/oa/report/overtime?range=&userId= 加班报表
GET /api/oa/report/vehicle?range=&userId= 用车报表
GET /api/oa/report/outing-log?range=&userId= 外勤日志报表
POST /api/oa/report/expense/export 导出费用报销 Excel

5.7 基础数据

方法 URL 说明
GET /api/oa/vehicles 车辆池列表
POST /api/oa/vehicles 添加车辆
PUT /api/oa/vehicles/:id 修改车辆
GET /api/oa/banners 轮播图列表

5.8 复用已有 API

方法 URL 说明
GET /api/user/{id} 用户信息
GET /api/user/search?q=&page= 用户搜索
GET /api/dept/tree 部门树
PUT /api/user/avatar 上传头像
GET /api/customer/search?q= 客户联想搜索
GET /api/messages?page= 消息列表
GET /api/messages/unread-count 未读消息数
PUT /api/messages/:id/read 标记已读
POST /api/messages/read-all 全部已读
DELETE /api/messages/:id 删除消息
GET /api/dict/banks 银行列表
GET /api/dict/cost-categories 费用类别

6. 第三方服务集成方案

6.1 ERP 审批引擎

集成方式:.NET 服务端 HTTP 调用
认证:ERP 内部 Token / Session(由 .NET 服务端维护)
容错:单套 ERP 超时 15s,失败不影响其他 ERP 查询
      重试 3 次,间隔 2s/4s/8s 递增

6.2 消息推送

集成方式:复用 .NET 服务端已有推送模块
通道:App Push(原生通知栏)+ 站内消息(消息列表)
触发点:审批创建/完成/拒绝、公告发布、DING 催办
OA 职责:调用 .NET 消息 API 构造消息体,推送执行由消息模块完成

6.3 原生能力(MethodChannel)

能力 平台 用途
相机 Android/iOS 外勤拍照(防伪水印)、发票拍照、头像拍摄
相册 Android/iOS 附件上传(图片/PDF/文件选择)
GPS 定位 Android/iOS 外勤签到、用车始发地
通讯录 Android/iOS 用车同行人选人、审批转交选人
地图选点 Android/iOS 用车目的地经纬度选点
文件分享 Android/iOS Excel 导出 → 系统分享面板
PDF 查看 Android/iOS 附件 PDF 原生预览

6.4 支付

不涉及。OA 不做在线支付,财务核销是线下打款后录入电汇流水号和记账凭证号。

7. 部署架构

graph TB
    subgraph 用户设备["用户设备"]
        direction LR
        M1["Android App<br/>含 Flutter OA Module"]
        M2["iOS App<br/>含 Flutter OA Module"]
    end

    subgraph 内网["公司内网"]
        direction TB
        S1["IIS / Windows Server<br/>.NET 4.8 服务端"]
        DB1[("SQL Server 2019+<br/>OA 业务库")]

        subgraph ERP群["ERP 系统群"]
            ERP1["ERP-A<br/>审批 + 主数据"]
            ERP2["ERP-B<br/>审批 + 主数据"]
            ERP3["ERP-C<br/>审批 + 主数据"]
        end
    end

    M1 -->|"HTTPS"| S1
    M2 -->|"HTTPS"| S1
    S1 --> DB1
    S1 -->|"内网 HTTP"| ERP1
    S1 -->|"内网 HTTP"| ERP2
    S1 -->|"内网 HTTP"| ERP3

    S1 -.->|"App Push<br/>FCM / APNs"| M1
    S1 -.->|"App Push<br/>FCM / APNs"| M2

部署要点

组件 部署位置 说明
Flutter OA Module 嵌入 Android/iOS App 随 App 发版更新
.NET 4.8 服务端 Windows Server + IIS 新增 OA Controller 部署到现有服务端
SQL Server 现有数据库实例 新增 OA 库或扩展现有库(新建 OA 表)
ERP 系统 内网已有 OA 通过内网 HTTP 调用
推送服务 FCM (Android) / APNs (iOS) 已有消息模块管理,OA 不直接对接

配置项

<!-- .NET 服务端 web.config 新增 -->
<appSettings>
  <!-- ERP 适配器配置:每项可设为 ErpA/ErpB/Null/OaLocal -->
  <add key="Oa:Adapter:Approval" value="ErpA" />
  <add key="Oa:Adapter:Budget" value="ErpA" />
  <add key="Oa:Adapter:Project" value="ErpA" />
  <add key="Oa:Adapter:Subject" value="ErpA" />
  <add key="Oa:Adapter:CostCenter" value="ErpA" />
  <add key="Oa:Adapter:ExchangeRate" value="ErpA" />
  <add key="Oa:Adapter:Customer" value="ErpA" />
  
  <!-- ERP API 地址 -->
  <add key="ErpA:BaseUrl" value="http://erp-a.internal/api/" />
  <add key="ErpB:BaseUrl" value="http://erp-b.internal/api/" />
  
  <!-- 调用超时(毫秒) -->
  <add key="Oa:ErpTimeout" value="15000" />
  
  <!-- 报表缓存时间(分钟) -->
  <add key="Oa:ReportCacheMinutes" value="5" />
</appSettings>

8. 安全设计

维度 方案
传输安全 HTTPS(移动端 → 服务端);内网 HTTP(服务端 → ERP)
认证 复用宿主 App 的登录态;401 → 触发宿主重新登录
授权 OA 端:OaUserPermission ACL;服务端:每个 API 校验权限点
数据隔离 列表查询按 view_own/view_dept/view_all 权限控制数据范围
SQL 注入 Dapper 参数化查询
审计 OaPermissionChangeLog 记录所有权限变更
软删除 所有业务表 IsDeleted=1,禁止物理删除

9. ERP 适配器接口定义

9.1 IApprovalAdapter

public interface IApprovalAdapter
{
    // 创建审批实例,返回 ERP 实例 ID
    Task<string> SubmitAsync(string bizType, long bizId, object formData, List<string> tags = null);
    // tags: ["budget_exceeded", "standard_exceeded"] → ERP 自动插入特批节点

    // 查询审批状态
    Task<ApprovalStatus> GetStatusAsync(string instanceId);

    // 查询审批时间线
    Task<List<TimelineNode>> GetTimelineAsync(string bizType, long bizId, string instanceId);

    // 执行审批动作(action: approve / reject)
    Task<ActionResult> ExecuteActionAsync(string instanceId, string action, string opinion);

    // 撤回
    Task WithdrawAsync(string instanceId);

    // 查询待办列表
    Task<PendingList> GetPendingListAsync(long approverId, string bizType, int page, int pageSize);
}

9.2 IBudgetAdapter

public interface IBudgetAdapter
{
    // 查余额(含冻结金额)
    Task<BudgetBalance> GetBalanceAsync(long projectId, long subjectId);

    // 冻结(提交时),返回新 Version
    Task<long> FreezeAsync(long projectId, long subjectId, decimal amount);

    // 扣减(审批通过时),携带 Version 乐观锁
    Task DeductAsync(long projectId, long subjectId, decimal amount, long version);

    // 释放(拒绝/撤回时)
    Task ReleaseAsync(long projectId, long subjectId, decimal amount);
}

public class BudgetBalance
{
    public bool HasBudget { get; set; }           // false → OA 前端隐藏预算区块
    public decimal AllocatedAmount { get; set; }
    public decimal AvailableAmount { get; set; }
    public decimal FrozenAmount { get; set; }
    public decimal ActualAvailable => AvailableAmount - FrozenAmount;
}

9.3 IProjectAdapter / ISubjectAdapter / ICostCenterAdapter

public interface IProjectAdapter
{
    Task<List<IdName>> GetProjectsAsync();                    // 项目列表
    Task<List<IdName>> GetSubjectsAsync(long projectId);      // 某项目下的预算科目
}

public interface ISubjectAdapter
{
    Task<List<IdName>> GetSubjectsAsync(long? projectId);
}

public interface ICostCenterAdapter
{
    Task<List<IdName>> GetCostCentersAsync();
}

public class IdName { public long Id; public string Name; }

9.4 IStandardAdapter

public interface IStandardAdapter
{
    Task<StandardCheckResult> CheckAsync(string expenseType, string cityLevel, string employeeLevel, decimal amount);
}

public class StandardCheckResult
{
    public bool HasStandard { get; set; }     // false → OA 不校验
    public bool Pass { get; set; }
    public decimal MaxAmount { get; set; }
    public decimal ExceedAmount { get; set; }
}

9.5 IExchangeRateAdapter / ICustomerAdapter / ISupplierAdapter

public interface IExchangeRateAdapter
{
    Task<ExchangeRate> GetRateAsync(string currencyCode);
    // { CurrencyCode: "USD", Rate: 7.2456, Date: "2026-06-03" }
}

public interface ICustomerAdapter
{
    Task<List<IdName>> SearchAsync(string keyword);
    Task<long> CreateAsync(string customerName);  // 外勤日志新客户自动创建
}

public interface ISupplierAdapter
{
    Task<List<IdName>> SearchAsync(string keyword);
}

9.6 NullAdapter 模式

// ERP 无某能力时,使用 Null 实现——OA 前端自动隐藏对应区块
public class NullBudgetAdapter : IBudgetAdapter
{
    public Task<BudgetBalance> GetBalanceAsync(long p, long s)
        => Task.FromResult(new BudgetBalance { HasBudget = false });
    public Task<long> FreezeAsync(long p, long s, decimal a) => Task.FromResult(0L);
    public Task DeductAsync(long p, long s, decimal a, long v) => Task.CompletedTask;
    public Task ReleaseAsync(long p, long s, decimal a) => Task.CompletedTask;
}

10. 事前申请→报销:适配器调用链

10.1 事前申请提交流程

sequenceDiagram
    participant OA as Flutter OA
    participant NET as .NET 服务端
    participant P as ProjectService
    participant S as SubjectService
    participant B as BudgetService
    participant A as ApprovalService

    OA->>NET: 打开表单
    NET->>P: 项目列表
    P-->>NET: [{id, name}]
    NET->>S: 某项目下科目
    S-->>NET: [{id, name}]
    NET-->>OA: 级联数据

    OA->>NET: 选完项目+科目
    NET->>B: 查可用余额
    B-->>NET: { hasBudget, available, frozen }
    NET-->>OA: 预算余额 ¥XX

    OA->>NET: 提交
    NET->>B: 冻结预算
    B-->>NET: { version }
    NET->>A: 创建审批实例(含 tags)
    A-->>NET: { instanceId }
    NET-->>OA: { approvalInstanceId, status }

10.2 费用报销提交流程

sequenceDiagram
    participant OA as Flutter OA
    participant NET as .NET 服务端
    participant C as CostCenterService
    participant E as ExchangeRateService
    participant B as BudgetService
    participant A as ApprovalService

    OA->>NET: 打开表单
    NET->>C: 成本中心列表
    C-->>NET: [{id, name}]
    NET-->>OA: 下拉数据

    OA->>NET: 明细行选外币 USD
    NET->>E: 查 USD 汇率
    E-->>NET: { rate: 7.2456 }
    NET-->>OA: 本币金额 = 原币 × 7.2456

    OA->>NET: 提交
    NET->>B: 扣减预算
    B-->>NET: ok
    NET->>A: 创建审批实例 { tags }
    A-->>NET: { instanceId }
    NET->>NET: 写 ExpenseApplicationMapping
    NET-->>OA: { instanceId, status }

10.3 适配器覆盖矩阵

环节 Approval Budget Project Subject CostCenter ExchRate Customer
事前-填单 余额
事前-提交 ✅ 创建 ✅ 冻结
事前-审批 ✅ 流转
事前-通过 ✅ 扣减
报销-填单
报销-提交 ✅ 创建 ✅ 扣减
报销-审批 ✅ 流转
报销-撤回 ✅ 撤回 ✅ 释放

11. ERP 能力缺位时的 OA 自治扩展

当 ERP 暂无某项能力,后续 OA 需要自建时,扩展路径标准化:

11.1 三步扩展法

步骤 1: 建表
  ERP 无预算 → OA 新建 SysProjectBudget 表(DDL 已就绪)
  ERP 无标准 → OA 新建 ExpenseStandard 表 + 管理页

步骤 2: 写实现
  新建 OaLocalBudgetAdapter : IBudgetAdapter
  新建 OaLocalStandardAdapter : IStandardAdapter

步骤 3: 切配置
  appSettings: Oa:Adapter:Budget 从 Null 切为 OaLocal
  重启 .NET 服务端生效

11.2 影响范围

层面 改动
数据库 +1 张表(迁移脚本)
.NET 服务端 +1 个 Adapter 实现类(~50 行)
web.config 改 1 行配置
Flutter 前端 零改动(API 契约不变)
OA API 零改动

11.3 预设可用性

能力 当前 NullAdapter 行为 如需自建
预算 取决于 ERP hasBudget=false → OA 隐藏余额 +SysProjectBudget 表
汇率 取决于 ERP 只返回 CNY (rate=1) +汇率表+维护页
项目 始终需要 空列表 → 无下拉选项 +SysProject 管理页
成本中心 可空 空列表 → 可空不强制选 +SysCostCenter 管理页

文档版本:v1.0 | 日期:2026-06-04