# TBOSS OA 模块 — 系统架构设计

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

---

## 1. 整体分层架构

```mermaid
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_apply/
│   │   ├── expense_apply_api.dart
│   │   ├── expense_apply_model.dart
│   │   ├── expense_apply_apply_page.dart
│   │   ├── expense_apply_list_page.dart
│   │   ├── expense_apply_list_controller.dart
│   │   └── expense_apply_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 文件（各模块目录占位）
```

### 模块依赖关系

```mermaid
graph LR
    subgraph features["features/"]
        home["home"]
        expense["expense"]
        expense_apply["expense_apply"]
        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. 部署架构

```mermaid
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 不直接对接 |

### 配置项

```xml
<!-- .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

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

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

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

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

```csharp
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 模式

```csharp
// 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 费用申请提交流程

```mermaid
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 费用报销提交流程

```mermaid
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: 写 ExpenseApplyMapping
    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
