# TBOSS OA 模块 — 数据库表结构设计

> 版本：v1.1 | 日期：2026-06-27 | 基于 PRD v1.0 + 需求讨论确认

---

## 1. 设计约定

### 1.1 技术栈

| 项目 | 规格 |
|------|------|
| 数据库 | Microsoft SQL Server 2019+ |
| 后端 | .NET Framework 4.8 + ADO.NET |
| 移动端 | Flutter 3.38.10（通过 HTTP API 交互，不直连数据库） |
| 字符集 | Chinese_PRC_CI_AS |

### 1.2 命名规范

| 范畴 | 规范 | 示例 |
|------|------|------|
| 表名 | PascalCase，单数 | `Expense`, `OaPermission` |
| 列名 | PascalCase | `Id`, `ApprovalInstanceId` |
| 主键 | `BIGINT IDENTITY(1,1)` | `Id BIGINT IDENTITY(1,1) PRIMARY KEY` |
| ERP 引用列 | 存储 ERP ID（NVARCHAR），非 FK 约束 | `ApplicantId NVARCHAR(30)`（注释标注来源 ERP） |
| 约束名 | `PK_表名` / `UX_表名_列名` | `PK_Expense`, `UX_Expense_ExpenseNo` |
| 索引名 | `IX_表名_用途` | `IX_Expense_List` |

### 1.3 全局字段

每张 OA 自管表必须包含：

| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `CreateTime` | `DATETIME` | `NOT NULL DEFAULT GETDATE()` | 服务器授时，行创建时间，不可修改 |
| `UpdateTime` | `DATETIME` | `NULL` | 最后修改时间，应用层 UPDATE 时显式赋 `GETDATE()` |
| `IsDeleted` | `BIT` | `NOT NULL DEFAULT 0` | 软删除标记，0=正常，1=已删除。例外：OaPermissionChangeLog（审计日志不可删除）、ExpenseApplyMapping（纯关联表，随主表级联清理）、AnnouncementTarget / AnnouncementReadLog（随公告级联清理） |

### 1.4 精度规范

| 类型 | 精度 | 示例 |
|------|------|------|
| 金额 | `DECIMAL(28,8)` | `TotalAmount` |
| 经纬度 | `DECIMAL(10,6)` | `CheckInLongitude` |
| 税率 | `DECIMAL(5,4)` | `TaxRate` |
| 工时 | `DECIMAL(4,1)` | `NetOtHours` |

### 1.5 状态枚举

所有状态/类型字段使用 `VARCHAR(20)` 存储英文标识，前端国际化映射。完整取值见第 6 节。

### 1.6 单据编号

格式 `{前缀}-{YYYYMMDD}-{序号}`，序号按天重置高位补零至 3 位。.NET 服务端使用 `sp_getapplock` 原子生成。

| 业务 | 前缀 | 示例 |
|------|------|------|
| 费用申请 | BXSQ | BXSQ-20260603-001 |
| 费用报销 | BX | BX-20260603-001 |
| 加班申请 | JB | JB-20260603-001 |
| 用车申请 | YC | YC-20260603-001 |
| 外勤日志 | VST | VST-20260603-001 |

---

## 2. 数据库 ERD

```
                        ┌─────────────────────────────┐
                        │     OA 权限体系（独立）        │
                        │  OaPermission                │
                        │  OaUserPermission            │
                        │  OaPermissionChangeLog       │
                        └─────────────────────────────┘

  [ERP 用户] ◄──── 所有业务表的 ApplicantId / SalespersonId
  [ERP 部门] ◄──── 所有业务表的 DeptId
  [ERP 客户] ◄──── OutingLog.CustomerId
  [ERP 审批] ◄──── ApprovalInstanceId（审批类主表）
  [ERP 预算] ◄──── BudgetService 适配器（余额/冻结/释放）
  [ERP 项目] ◄──── ProjectService 适配器（列表/级联）
  [ERP 科目] ◄──── SubjectService 适配器（预算科目）

  ┌──────────────────────────────────────────────────────┐
  │                    OA 自管业务表                        │
  │                                                       │
  │  ExpenseApply ──(1:N)── ExpenseApplyDetail        │
  │       │                  (明细行)                    │
  │       │                                             │
  │  ExpenseApplyMapping ── 申请↔报销多对多          │
  │       ├──(N:1)── Expense                              │
  │       │            (费用明细, 含币种)                   │
  │       │            └──(1:N)── ExpenseDetail            │
  │       └──(N:1)── ExpenseApply                   │
  │                                                       │
  │  Overtime                                             │
  │                                                       │
  │  Vehicle ──(1:N)── VehiclePassenger                   │
  │       │                                               │
  │       └──(N:1)── SysVehicle (OA 自管车辆池)            │
  │                                                       │
  │  OutingLog ──(1:N)── OutingLogComment                 │
  │                                                       │
  │  Announcement ──(1:N)── AnnouncementTarget            │
  │       └──(1:N)── AnnouncementReadLog                  │
  │                                                       │
  │  Attachment ── 统一附件表                              │
  │       BizType: expense_apply/expense/outing_log/announcement │
  └──────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────┐
  │                    OA 自管基础表                        │
  │  SysVehicle         — 车辆池                           │
  │  SysBanner          — 工作台轮播图                      │
  │  SysCostCategory    — 费用类别字典                      │
  └──────────────────────────────────────────────────────┘
```

---

## 3. 表结构定义

### 3.1 OA 权限体系

#### 3.1.1 OaPermission — 权限点字典

OA 独立权限系统的权限点定义表，存储所有可用权限编码及中文名称。权限点按模块分组，管理员通过快捷套餐或手动勾选为用户赋权。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **PermissionCode** | VARCHAR(80) | ✅ | 权限点唯一编码，如 `oa.expense.apply`。`UX_OaPermission_PermissionCode` UNIQUE 约束 |
| **PermissionName** | NVARCHAR(50) | ✅ | 权限点中文名称，如"发起报销"，用于管理界面展示 |
| **Module** | VARCHAR(30) | ✅ | 所属功能模块，用于分组展示：expense / expense_apply / overtime / vehicle / outing_log / announcement / report / admin |
| **SortOrder** | INT | ✅ | 管理界面排序权重，值越小越靠前，默认 0 |
| **IsActive** | BIT | ✅ | 权限点全局启用/停用标记，1=启用，0=停用，默认 1。停用后所有用户无法使用该权限 |

> 本表为字典表，不包含 CreateTime/UpdateTime/IsDeleted。权限点预置后一般不增删。

#### 3.1.2 OaUserPermission — 用户-权限关联（ACL）

用户与权限点的多对多关联表。每个用户可拥有多个权限点，通过 IsDeleted 软删除实现权限的授予与回收。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **UserId** | NVARCHAR(30) | ✅ | ERP 用户 ID（非 FK 约束，关联 ERP 用户主数据） |
| **PermissionId** | BIGINT | ✅ | 权限点 ID，FK → OaPermission.Id |
| **GrantedBy** | NVARCHAR(30) | ✅ | 授权操作人 ERP 用户 ID，用于审计追溯 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE()，授权时间 |
| **UpdateTime** | DATETIME | | 最后变更时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0。移除权限时设为 1（软删除，保留历史记录），重新授权时置回 0 |

**索引**：
- `UX_OaUserPermission_UserId_PermissionId` UNIQUE (UserId, PermissionId) WHERE IsDeleted = 0 — 防止同一权限重复授权
- `IX_UserPermission_UserId` (UserId, IsDeleted) INCLUDE (PermissionId) — 按用户查权限列表

#### 3.1.3 OaPermissionChangeLog — 权限变更审计

记录管理员每次修改用户权限的完整快照。本表仅追加不修改（无 UpdateTime），不可软删除。用于变更追溯和合规审计。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **TargetUserId** | NVARCHAR(30) | ✅ | 被操作的目标用户 ERP 用户 ID |
| **OperatorId** | NVARCHAR(30) | ✅ | 执行操作的管理员 ERP 用户 ID |
| **ChangeType** | VARCHAR(20) | ✅ | 变更类型：`assign`(赋权) / `revoke`(收权) / `toggle_active`(启停) |
| **BeforeSnapshot** | NVARCHAR(MAX) | ✅ | 变更前权限 JSON 快照，格式 `{"permissions": ["oa.expense.apply"], "isActive": true}` |
| **AfterSnapshot** | NVARCHAR(MAX) | ✅ | 变更后权限 JSON 快照，格式同上 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE()，审计时间戳 |

> 本表无 UpdateTime 和 IsDeleted —— 审计日志不可修改、不可删除。

**索引**：`IX_PermChangeLog_Target` (TargetUserId, CreateTime DESC) INCLUDE (OperatorId, ChangeType)

---

### 3.2 费用申请与费用报销

#### 3.2.1 ExpenseApply — 费用申请主表

员工在费用发生前提交的预估申请。支持多费用类型、关联项目与预算科目、多行预估明细。审批通过后可被多张报销单分批引用。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ExpenseApplyNo** | NVARCHAR(40) | ✅ | 单据唯一编号，格式 BXSQ-YYYYMMDD-XXX，提交时由 .NET 服务端原子生成 |
| **ExpenseApplyDate** | DATETIME | | 业务申请时间（提交时写入） |
| **ApplicantId** | NVARCHAR(30) | ✅ | 申请人 ERP 用户 ID |
| **ApplicantName** | NVARCHAR(100) | ✅ | 申请人姓名冗余，提交时从 ERP 用户主数据写入 |
| **DeptId** | NVARCHAR(30) | ✅ | 申请人所属部门 ERP 部门 ID，提交时写入 |
| **DeptName** | NVARCHAR(200) | ✅ | 部门名称冗余，提交时写入 |
| **EstimatedAmount** | DECIMAL(28,8) | ✅ | 预估申请总金额 = 所有明细行预估金额汇总，只读不手动修改，默认 0 |
| **Urgency** | VARCHAR(10) | ✅ | 紧急程度：normal(普通)/urgent(紧急)/critical(特急)，默认 normal。枚举见 §6.3 |
| **Purpose** | NVARCHAR(500) | ✅ | 费用申请事由，限制 500 字 |
| **Remark** | NVARCHAR(500) | | 备注，补充说明 |
| **EffectiveDate** | DATETIME | | 生效时间 |
| **AuditorId** | NVARCHAR(30) | | 审核人 ERP 用户 ID |
| **Status** | VARCHAR(20) | ✅ | 业务状态：draft(草稿)/pending(审批中)/approved(已通过)/rejected(已拒绝)/withdrawn(已撤回)/archived(已归档)。审批状态通过 .NET → ERP 实时查询。当 UsageStatus=fully_used 时自动流转为 archived |
| **UsageStatus** | VARCHAR(20) | ✅ | 被报销引用的状态：unused(未引用)/partially_used(部分引用)/fully_used(已用完)。由报销单提交/删除时自动重算，默认 unused。在非 approved/archived 状态下始终为 unused |
| **ValidUntil** | DATE | | 申请有效期截止日。到期后定时 Job 自动释放冻结预算，UsageStatus 标记为 fully_used，Status 流转为 archived，不可再被报销引用 |
| **ReferenceNo** | NVARCHAR(100) | | 关联的外部单号（合同号/询价单号），用于采购类申请追溯，非必填 |
| **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID，提交审批时由 .NET → ERP 创建后回写。撤回/驳回重新提交时覆盖为新值 |
| **PreviousInstanceIds** | VARCHAR(MAX) | | 历史审批实例 ID JSON 数组，撤回/驳回重新提交时追加旧 ApprovalInstanceId，用于追溯完整审批历史 |
| **Version** | BIGINT | ✅ | 乐观锁并发控制字段，每次更新时 +1，默认 1 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：
- `IX_ExpenseApply_List` (ApplicantId, CreateTime DESC) INCLUDE (ExpenseApplyNo, EstimatedAmount, Purpose, UsageStatus)
- `IX_ExpenseApply_Import` (ApplicantId, UsageStatus) WHERE UsageStatus IN ('unused', 'partially_used') — 报销导入时快速过滤可用申请

#### 3.2.2 ExpenseApplyDetail — 费用申请预估明细

费用申请的逐行预估明细。每行包含费用类别及预估金额，汇总金额写入主表 EstimatedAmount。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ExpenseApplyId** | BIGINT | ✅ | 关联主表，FK → ExpenseApply.Id（主表软删除时级联软删除） |
| **ExpenseCategory** | VARCHAR(30) | ✅ | 费用类型编码，值来源于 SysCostCategory.CategoryCode |
| **Purpose** | NVARCHAR(500) | ✅ | 费用事由，限制 500 字 |
| **ProjectId** | NVARCHAR(60) | | 关联 ERP 项目代号 |
| **ProjectName** | NVARCHAR(200) | | 关联 ERP 项目名称，提交时写入 |
| **CostDeptId** | NVARCHAR(30) | | 费用承担部门 ERP 部门 ID，V1 中自动等于申请人所在 DeptId |
| **CostDeptName** | NVARCHAR(200) | | 费用承担部门名称冗余，V1 中自动等于申请人所在 DeptName |
| **AcctSubjectId** | NVARCHAR(50) | | 关联 ERP 会计科目代号，选择费用类型时会自动带出，值来源于 SysCostCategory.AcctSubjectId |
| **AcctSubjectName** | NVARCHAR(400) | | 关联 ERP 会计科目名称冗余，选择费用类型时会自动带出，值来源于 SysCostCategory.AcctSubjectName |
| **EstimatedStartDate** | DATE | | 预计开始日期，所有费用类型可见。选填 |
| **EstimatedEndDate** | DATE | | 预计结束日期，所有费用类型可见。选填。校验：必须 ≥ StartDate |
| **EstimatedAmount** | DECIMAL(28,8) | ✅ | 单项预估金额 |
| **Remark** | NVARCHAR(500) | | 明细项说明备注 |
| **SortOrder** | INT | ✅ | 排序号，默认 1，控制移动端渲染顺序 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_ExpenseApplyDetail_AppId` (ExpenseApplyId, SortOrder) INCLUDE (ExpenseCategory, EstimatedAmount, Remark)

#### 3.2.3 ExpenseApplyMapping — 申请↔报销多对多关联

一张费用报销单可导入多张费用申请，一张费用申请也可被多张费用报销单分批引用。每行记录一次导入关联，保存导入金额用于 UsageStatus 计算。**应用层约束**：单次导入的多张申请必须属于同一项目和同一预算科目（ProjectId + AcctSubjectId 一致），以保证预算扣减的准确性。`ImportedAmount` 总和 ≤ 对应申请 `EstimatedAmount - 已报总额`。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ExpenseId** | BIGINT | ✅ | 报销单 ID，FK → Expense.Id |
| **ExpenseApplyId** | BIGINT | ✅ | 费用申请 ID，FK → ExpenseApply.Id |
| **ImportedAmount** | DECIMAL(28,8) | ✅ | 本次报销从该申请导入的金额。∑ImportedAmount 不能超过申请 EstimatedAmount。仅 `Status IN ('pending', 'approved', 'archived')` 的报销单计入占用总额，`withdrawn` 和 `IsDeleted=1` 的报销单不计入 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |

**约束**：`UX_ExpenseApplyMapping_ExpenseId_ExpenseApplyId` UNIQUE (ExpenseId, ExpenseApplyId) — 同一张报销单不能重复导入同一张申请
**索引**：`IX_ExpenseApplyMapping_AppId` (ExpenseApplyId) INCLUDE (ExpenseId, ImportedAmount) — 查某申请的被引用情况

#### 3.2.4 Expense — 费用报销主表

员工费用报销的核心主表。支持从费用申请拼单导入或直接新建，包含收款银行信息、财务核销标记。审批通过后由财务核销归档。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ExpenseNo** | VARCHAR(40) | ✅ | 单据唯一编号，格式 BX-YYYYMMDD-XXX，`UX_Expense_ExpenseNo` UNIQUE 约束 |
| **ExpenseDate** | DATE | | 费用报销日期（提交时写入） |
| **ApplicantId** | NVARCHAR(30) | ✅ | 报销人 ERP 用户 ID |
| **ApplicantName** | NVARCHAR(100) | ✅ | 报销人姓名冗余，提交时从 ERP 用户主数据写入 |
| **DeptId** | NVARCHAR(30) | ✅ | 报销人所属部门 ERP 部门 ID，提交时写入 |
| **DeptName** | NVARCHAR(200) | ✅ | 部门名称冗余，提交时写入 |
| **CurrencyCode** | VARCHAR(10) | | 币别代码，默认 CNY。选外币时从 .NET → ERP ExchangeRateService 获取汇率填入明细行 |
| **IsGenerateVoucher** | BIT | ✅ | 是否产生会计凭证：0=不产生，1=产生，DEFAULT 0 |
| **VoucherNo** | VARCHAR(50) | | 财务核销时录入的金蝶/财务系统记账凭证号 |
| **ApprovedAmount** | DECIMAL(28,8) | ✅ | 核准金额合计 = 所有明细行 ApprovedAmount 汇总，只读不手动修改，默认 0 |
| **TotalAmount** | DECIMAL(28,8) | ✅ | 报销总金额 = 所有明细行 TotalAmount 汇总，只读不手动修改，默认 0 |
| **Purpose** | NVARCHAR(500) | ✅ | 报销事由说明 |
| **Remark** | NVARCHAR(500) | | 明细项说明备注 |
| **PaymentMethod** | NVARCHAR(20) | | 支付方式，员工填单时选择：bankTransfer(银行转账)/cash(现金)/alipay(支付宝)/wechat(微信) |
| **IsInvoiceVerified** | BIT | ✅ | 财务核销标记一：发票已在全国增值税发票查验平台验真，默认 0 |
| **IsTaxIdMatched** | BIT | ✅ | 财务核销标记二：发票抬头与公司税号一致，默认 0 |
| **IsCategoryCompliant** | BIT | ✅ | 财务核销标记三：报销类目与发票项目匹配合规，默认 0 |
| **BankTransferNo** | VARCHAR(50) | | 财务核销时录入的银行电汇流水号 |
| **PaymentStatus** | VARCHAR(20) | ✅ | 付款状态：unpaid(待付款)/paid(已付款)。仅 Status=approved 时允许流转，默认 unpaid |
| **Status** | VARCHAR(20) | ✅ | 业务状态：draft/pending/approved/rejected/withdrawn/archived(已归档)。审批状态通过 .NET → ERP 实时查询。PaymentStatus=paid 且业务完结后自动流转为 archived |
| **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID，提交审批时由 .NET → ERP 创建后回写 |
| **PreviousInstanceIds** | VARCHAR(MAX) | | 历史审批实例 ID JSON 数组 |
| **EffectiveDate** | DATETIME | | 生效时间 |
| **AuditorId** | NVARCHAR(30) | | 审核人 ERP 用户 ID |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_Expense_List` (ApplicantId, CreateTime DESC) INCLUDE (TotalAmount, ExpenseNo, PaymentStatus)

#### 3.2.5 ExpenseDetail — 费用报销明细

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ExpenseId** | BIGINT | ✅ | 关联报销主表，FK → Expense.Id（主表软删除时级联软删除） |
| **ExpenseApplyId** | BIGINT | | 关联费用申请，FK → ExpenseApply.Id。导入申请时写入，直接新建报销时为 NULL |
| **ExpenseApplyNo** | NVARCHAR(40) | | 费用申请单号冗余，便于列表展示免 JOIN |
| **ExpenseApplyDate** | DATETIME | | 费用申请日期冗余 |
| **ExpenseCategory** | VARCHAR(30) | ✅ | 费用类型编码，值来源于 SysCostCategory.CategoryCode |
| **Purpose** | NVARCHAR(500) | ✅ | 费用事由，限制 500 字 |
| **Remark** | NVARCHAR(500) | | 明细项说明备注 |
| **ProjectId** | NVARCHAR(60) | | 关联 ERP 项目代号 |
| **ProjectName** | NVARCHAR(200) | | 关联 ERP 项目名称，提交时写入 |
| **CostDeptId** | NVARCHAR(30) | | 费用承担部门 ERP 部门 ID，V1 中自动等于申请人所在 DeptId |
| **CostDeptName** | NVARCHAR(200) | | 费用承担部门名称冗余，V1 中自动等于申请人所在 DeptName |
| **AcctSubjectId** | NVARCHAR(50) | | 关联 ERP 会计科目代号，选择费用类型时会自动带出，值来源于 SysCostCategory.AcctSubjectId |
| **AcctSubjectName** | NVARCHAR(400) | | 关联 ERP 会计科目名称冗余，选择费用类型时会自动带出，值来源于 SysCostCategory.AcctSubjectName |
| **Amount** | DECIMAL(28,8) | ✅ | 原币不含税金额。多币种时为原币金额 |
| **TaxRate** | DECIMAL(5,4) | | 税率，如 0.0600 / 0.0900 / 0.1300 |
| **TaxAmount** | DECIMAL(28,8) | ✅ | 进项税额，无发票时为 0，默认 0 |
| **TotalAmount** | DECIMAL(28,8) | ✅ | 原币价税合计 = Amount + TaxAmount |
| **CurrencyCode** | VARCHAR(10) | | 原币币种代码，默认 CNY。选外币（如 USD/EUR/JPY）时从 .NET → ERP ExchangeRateService 获取当日汇率自动填入 ExchangeRate |
| **ExchangeRate** | DECIMAL(10,4) | | 原币→本币汇率，CNY 时为 1.0000。由 .NET 服务端从 ERP 汇率表查询后填入 |
| **BaseAmount** | DECIMAL(28,8) | | 折算后的本币金额 = TotalAmount × ExchangeRate。预算校验和报表统计均以本币为准 |
| **ApprovedAmount** | DECIMAL(28,8) | ✅ | 该行核准金额，默认=TotalAmount（审批直接通过时），审批人可修改。当 ApprovedAmount 发生变化时，应以 ApprovedAmount 为基准重新计算本币金额：BaseAmount = ApprovedAmount × ExchangeRate。财务核销时只读不可修改 |
| **CustomerVendorId** | NVARCHAR(30) | | 客户或供应商 ID，用于采购/招待类费用追溯。客户和供应商均可使用 |
| **CustomerVendorName** | NVARCHAR(300) | | 客户或供应商名称，用于采购/招待类费用追溯 |
| **OffsetAmount** | DECIMAL(28,8) | ✅ | 已冲账金额（如预付/借款冲抵），V1 暂不使用，值始终为 0，默认 0。预留 V2 扩展 |
| **BankName** | NVARCHAR(100) | ✅ | 收款银行全称，前端支持下拉联想（数据源 .NET 字典 API），也可自由输入 |
| **BankAccountName** | NVARCHAR(50) | ✅ | 收款开户户名，默认填入当前用户姓名，可修改 |
| **BankAccount** | VARCHAR(50) | ✅ | 收款银行账号，前端校验 16-19 位数字格式 |
| **SortOrder** | INT | ✅ | 排序号，默认 1 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_ExpenseDetail_ExpenseId` (ExpenseId, SortOrder) INCLUDE (Amount, TotalAmount, ApprovedAmount, CustomerVendorName, OffsetAmount)

---

### 3.3 加班申请

#### 3.3.1 Overtime — 加班申请单

员工加班申请，支持三种加班类型和三种补偿方式。后端自动扣除午休/晚餐盲区计算净工时。审批通过后可用于调休或加班费结算。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ApplicationNo** | VARCHAR(30) | ✅ | 单据唯一编号，格式 JB-YYYYMMDD-XXX，`UX_Overtime_ApplicationNo` UNIQUE 约束 |
| **ApplicantId** | NVARCHAR(30) | ✅ | 申请人 ERP 用户 ID |
| **DeptId** | NVARCHAR(30) | ✅ | 所属部门 ERP 部门 ID |
| **OtType** | VARCHAR(10) | ✅ | 加班类型：workday(工作日加班)/weekend(休息日加班)/holiday(法定节假日加班) |
| **CompensationType** | VARCHAR(20) | ✅ | 补偿方式：overtime_pay(全部结算加班费)/comp_leave(全部转为调休)/mixed(部分调休+部分加班费) |
| **CompLeaveRatio** | DECIMAL(3,2) | | 混合模式下转为调休的比例，范围 0.10~0.90，仅 CompensationType=mixed 时生效。如 0.30=30%调休+70%加班费 |
| **StartTime** | DATETIME | | 加班开始时间，提交时必填。校验：必须早于 EndTime |
| **EndTime** | DATETIME | | 加班结束时间，提交时必填 |
| **NetOtHours** | DECIMAL(4,1) | ✅ | 实际净工时。由 .NET 服务端自动扣除午餐(12:00-13:00)和晚餐(18:00-18:30)盲区后计算，前端只读展示。≤0 时提交按钮置灰 |
| **Reason** | NVARCHAR(500) | ✅ | 加班原因说明 |
| **Status** | VARCHAR(20) | ✅ | 业务状态：draft/pending/approved/rejected/withdrawn/archived(已归档)。审批状态通过 .NET → ERP 实时查询。业务完结（加班费已发放或调休已确认）后自动流转为 archived |
| **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID |
| **PreviousInstanceIds** | VARCHAR(MAX) | | 历史审批实例 ID JSON 数组 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_Overtime_List` (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType)

---

### 3.4 用车申请

#### 3.4.1 Vehicle — 用车申请与还车核销

覆盖公车申请、审批、还车登记全生命周期。车辆池 OA 自管（SysVehicle），排期冲突检测、里程表记录和实际费用报账均在 OA 完成。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ApplicationNo** | VARCHAR(30) | ✅ | 单据唯一编号，格式 YC-YYYYMMDD-XXX，`UX_Vehicle_ApplicationNo` UNIQUE 约束 |
| **ApplicantId** | NVARCHAR(30) | ✅ | 申请人 ERP 用户 ID |
| **DeptId** | NVARCHAR(30) | ✅ | 所属部门 ERP 部门 ID |
| **VehicleId** | BIGINT | | 申请调配的公车 ID，FK → SysVehicle.Id（OA 自管车辆池）。草稿时可空，提交时必填 |
| **Purpose** | VARCHAR(20) | ✅ | 用车目的：reception(客户接待)/business(商务出行)/official(公务) |
| **Reason** | NVARCHAR(500) | ✅ | 用车事由详细说明 |
| **Origin** | NVARCHAR(200) | ✅ | 行程预计出发地地址 |
| **OriginLongitude** | DECIMAL(10,6) | | 始发地经度（地图选点回填） |
| **OriginLatitude** | DECIMAL(10,6) | | 始发地纬度 |
| **Destination** | NVARCHAR(200) | ✅ | 行程预计目的地地址 |
| **DestLongitude** | DECIMAL(10,6) | | 目的地经度 |
| **DestLatitude** | DECIMAL(10,6) | | 目的地纬度 |
| **PassengerCount** | INT | | 同行总人数，默认 1 |
| **StartTime** | DATETIME | | 预计出车时间，提交时必填 |
| **EndTime** | DATETIME | | 预计还车时间，提交时必填。校验：必须晚于 StartTime |
| — | — | — | **以下为还车登记字段（审批通过后填写）** |
| **ActualReturnTime** | DATETIME | | 还车登记：实际归还时间 |
| **StartOdometer** | DECIMAL(10,2) | | 还车登记：出车前里程表读数 |
| **EndOdometer** | DECIMAL(10,2) | | 还车登记：还车后里程表读数。校验：必须 > StartOdometer |
| **ActualCost** | DECIMAL(28,8) | | 还车登记：路桥费/停车费等实际费用总额 |
| **CostRemark** | NVARCHAR(500) | | 还车登记：费用明细备注 |
| **Status** | VARCHAR(20) | ✅ | 业务状态：draft/pending/approved/rejected/withdrawn/returned(已还车)/archived(已归档)。审批状态通过 .NET → ERP 实时查询。还车登记完成（returned）后自动流转为 archived |
| **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID |
| **PreviousInstanceIds** | VARCHAR(MAX) | | 历史审批实例 ID JSON 数组 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：
- `IX_Vehicle_List` (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, VehicleId, Purpose, Origin, Destination, StartTime)
- `IX_Vehicle_Collision` (VehicleId, StartTime, EndTime) WHERE Status IN ('pending', 'approved') — 排期冲突检测

#### 3.4.2 VehiclePassenger — 随行同行人员

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **VehicleId** | BIGINT | ✅ | 关联用车单主表，FK → Vehicle.Id |
| **UserId** | NVARCHAR(30) | | 内部同事 ERP 用户 ID（可空，添加外部人员时为空） |
| **PassengerName** | NVARCHAR(50) | ✅ | 同行人姓名文本（ERP 用户取 RealName，外部人员自由输入） |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_VehiclePassenger_VehicleId` (VehicleId, IsDeleted) INCLUDE (UserId, PassengerName)

#### 3.4.3 SysVehicle — 企业车辆资产池（OA 自管）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **LicensePlate** | NVARCHAR(20) | ✅ | 车牌号（含中文字符如粤B12345），`UX_SysVehicle_LicensePlate` UNIQUE 约束 |
| **VehicleType** | VARCHAR(20) | ✅ | 车辆类型：sedan(轿车)/suv(SUV)/mpv(商务车)/van(面包车) |
| **Brand** | NVARCHAR(50) | | 品牌型号 |
| **Seats** | INT | | 核定座位数 |
| **DriverName** | NVARCHAR(50) | | 默认驾驶员姓名 |
| **Status** | VARCHAR(20) | ✅ | 车辆状态：idle(空闲可用)/in_use(使用中)/maintenance(维修中)。用车申请审批通过自动变更为 in_use，还车登记完成（Vehicle.Status=returned/archived）后恢复 idle。maintenance 状态下不可被申请 |
| **IsActive** | BIT | ✅ | 启用/停用标记，默认 1。停用的车辆不可被申请 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

---

### 3.5 外勤日志

#### 3.5.1 OutingLog — 外勤拜访日志

业务员外出拜访客户的工作记录。GPS 定位只读防篡改，照片强制拍摄并加水印。不走审批流，提交即完成。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **VisitNo** | VARCHAR(30) | ✅ | 单据唯一编号，格式 VST-YYYYMMDD-XXX，`UX_OutingLog_VisitNo` UNIQUE 约束 |
| **SalespersonId** | NVARCHAR(30) | ✅ | 业务员 ERP 用户 ID（外勤日志使用 SalespersonId 而非 ApplicantId，强调销售角色属性） |
| **DeptId** | NVARCHAR(30) | ✅ | 所属部门 ERP 部门 ID |
| **CustomerId** | NVARCHAR(30) | | 拜访客户 ERP 客户 ID（非 FK）。输入新客户名时，提交后由 .NET → ERP CustomerService 创建新客户并回填此字段 |
| **CustomerName** | NVARCHAR(300) | ✅ | 客户公司全称冗余字段，前端输入时调 ERP 联想匹配，也支持自由输入 |
| **ContactId** | NVARCHAR(30) | | 客户联系人 ERP ID |
| **CheckInLongitude** | DECIMAL(10,6) | | GPS 硬件定位经度，提交时必填。前端设为只读，不可手动修改 |
| **CheckInLatitude** | DECIMAL(10,6) | | GPS 硬件定位纬度 |
| **CheckInAddress** | NVARCHAR(500) | | 逆地理编码出的街道地址，前端设为只读，不可手动修改 |
| **VisitSummary** | NVARCHAR(2000) | ✅ | 今日工作核心总结 |
| **NextPlan** | NVARCHAR(500) | | 后续推进计划 |
| **Status** | VARCHAR(20) | ✅ | 状态：draft(草稿)/completed(已提交)。不走审批流，提交即完成，提交后锁定不可再编辑 |
| **LastViewedTime** | DATETIME | | 员工最后一次查看详情页的时间（仅打开详情页时更新），用于判断是否有新点评（红点逻辑：MAX(CreateTime) > LastViewedTime） |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE()，服务器授时（用于防伪水印） |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0（仅管理员可软删除已提交日志） |

**索引**：
- `IX_OutingLog_Sales` (SalespersonId, CreateTime DESC) INCLUDE (CustomerId, CustomerName, CheckInAddress)
- `IX_OutingLog_Dept` (DeptId, CreateTime DESC) INCLUDE (SalespersonId, CustomerId, CustomerName, CheckInAddress, Status)

#### 3.5.2 OutingLogComment — 主管考评点评

经理在外勤日志详情页的星级评分和文字点评，以气泡样式展示。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **LogId** | BIGINT | ✅ | 关联外勤日志，FK → OutingLog.Id（主表软删除时级联软删除） |
| **CommenterId** | NVARCHAR(30) | ✅ | 点评人（经理）ERP 用户 ID |
| **RatingStars** | INT | | 星级评分 1-5，CHECK 约束 (RatingStars IS NULL OR RatingStars >= 1 AND RatingStars <= 5) |
| **CommentText** | NVARCHAR(1000) | ✅ | 点评指导意见文字内容 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE()（红点判定基于此字段） |
| **UpdateTime** | DATETIME | |（修改点评不更新 CreateTime，不触发新红点） |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_OutingLogComment_LogId` (LogId, IsDeleted) INCLUDE (CommenterId, RatingStars, CommentText, CreateTime)

---

### 3.6 公告管理

#### 3.6.1 Announcement — 行政公告主表

管理员发布的公司公告。支持按全员/部门/指定用户三种范围发布，可置顶、设有效期。管理员可查看已读/未读名单并 DING 催办。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **Title** | NVARCHAR(200) | ✅ | 公告标题 |
| **Content** | NVARCHAR(MAX) | ✅ | 公告正文，支持 HTML/Markdown 格式 |
| **Type** | VARCHAR(20) | ✅ | 公告分类：notice(通知公告)/policy(人事与制度)/activity(放假与活动) |
| **Status** | VARCHAR(20) | ✅ | 发布状态：draft(草稿，仅创建者和管理员可见)/published(已发布，全员可见) |
| **PublisherId** | NVARCHAR(30) | ✅ | 发布管理员 ERP 用户 ID |
| **PublishTime** | DATETIME | | 实际发布时间，草稿时为 NULL，发布时写入 GETDATE() |
| **IsTop** | BIT | ✅ | 是否全局置顶：1=置顶优先展示，0=正常排序，默认 0 |
| **PrivateLevel** | INT | ✅ | 可见范围级别：0=全员，1=按部门，2=按指定用户。默认 0 |
| **ExpiryDate** | DATETIME | | 自动下架失效时间，不填则永不过期。过期后列表置灰标注"已过期" |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_Announcement_List` (Type, IsTop DESC, PublishTime DESC) INCLUDE (Title, PublisherId, Status, ExpiryDate) WHERE IsDeleted = 0 AND Status = 'published'

#### 3.6.2 AnnouncementTarget — 公告可见范围

当公告按部门或按指定用户发布时，记录目标范围。多态外键设计（SQL Server 不支持多态 FK，由应用层校验引用完整性）。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **AnnouncementId** | BIGINT | ✅ | 关联公告主表，FK → Announcement.Id |
| **TargetType** | VARCHAR(10) | ✅ | 目标实体类型：all(全员)/dept(按部门)/user(按指定个人)。PrivateLevel=0 时写入一条 all 记录 |
| **TargetId** | NVARCHAR(30) | ✅ | 多态外键：TargetType=all 时为 'ALL'；TargetType=dept 时指向 ERP 部门 ID；TargetType=user 时指向 ERP 用户 ID |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |

#### 3.6.3 AnnouncementReadLog — 触达率审计

记录每个员工对每条公告的已读/未读状态。发布时根据 PrivateLevel 批量初始化 IsRead=0 记录，停留 ≥2 秒标记已读。管理员通过此表查看触达率并 DING 催办。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **AnnouncementId** | BIGINT | ✅ | 关联公告，FK → Announcement.Id |
| **UserId** | NVARCHAR(30) | ✅ | 被触达员工 ERP 用户 ID |
| **IsRead** | BIT | ✅ | 已读标记：0=未读，1=已读。停留 ≥2 秒自动标记，默认 0 |
| **ReadTime** | DATETIME | | 员工实际阅读时间 |
| **IsUrged** | BIT | ✅ | 是否已被管理员 DING 催办过，默认 0（催办后仍可再次催办，更新 LastUrgeTime） |
| **LastUrgeTime** | DATETIME | | 最后一次 DING 催办时间 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |

**约束**：`UX_AnnouncementReadLog_AnnouncementId_UserId` UNIQUE (AnnouncementId, UserId) — 每个员工对每条公告仅有一条触达记录
**索引**：`IX_Announcement_Ding` (AnnouncementId, IsRead, IsUrged) INCLUDE (UserId, ReadTime)

---

### 3.7 Attachment — 统一附件表

费用申请（支撑材料）、费用报销（发票影像）、外勤日志（现场照片）、公告（附件）四种业务附件合并为一张聚合表。通过 `BizType` 区分业务类型，`DetailId` 可绑定报销明细行（仅报销使用），`SortOrder` 用于外勤照片墙排序。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **BizType** | VARCHAR(30) | ✅ | 业务类型：expense_apply(费用申请)/expense(费用报销)/outing_log(外勤日志)/announcement(公告) |
| **BizId** | BIGINT | ✅ | 父表 ID（多态，与 BizType 联合路由到具体业务表） |
| **DetailId** | BIGINT | | 仅 BizType='expense' 使用，绑定 ExpenseDetail.Id，实现发票与明细行的关联 |
| **FileName** | NVARCHAR(200) | ✅ | 原始文件名 |
| **FileUrl** | VARCHAR(500) | ✅ | 云端对象存储（OSS）绝对 URL |
| **FileType** | VARCHAR(20) | ✅ | 文件类型：image(通用图片)/pdf/doc/xls/sign_in_photo(签到照)/visit_photo(现场照)/other |
| **FileSize** | BIGINT | | 文件字节数，用于上传大小校验 |
| **SortOrder** | INT | | 排序号，默认 0。外勤照片墙按此排序展示 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**应用层约束**（前端 + .NET 校验）：
- expense_apply：≤9 个，图片 ≤20MB / PDF/Word/Excel ≤20MB
- expense：≤9 个，图片 ≤20MB / PDF/Word/Excel ≤20MB
- outing_log：1~9 张，仅支持 image/sign_in_photo/visit_photo 类型，≤20MB
- announcement：≤9 个，PDF/图片/Word/Excel ≤20MB

**索引**：`IX_Attachment_Biz` (BizType, BizId, IsDeleted) INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder)

---

### 3.8 基础字典

#### 3.8.1 SysCostCategory — 费用类别字典（OA 自管）

费用报销和费用申请表单中"费用类别"下拉的数据源。通过 `BizScope` 控制适用范围（事前/报销/通用）。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **CategoryCode** | VARCHAR(30) | ✅ | 类别编码，`UX_SysCostCategory_CategoryCode` UNIQUE 约束 |
| **CategoryName** | NVARCHAR(200) | ✅ | 类别中文名称，如"交通费" |
| **ParentId** | BIGINT | | 上级类别 ID，FK → SysCostCategory.Id（自引用）。NULL 为一级分类，非 NULL 为叶子节点。仅叶子节点可用于明细录入 |
| **AcctSubjectId** | NVARCHAR(50) | | 关联 ERP 会计科目代号 |
| **AcctSubjectName** | NVARCHAR(400) | | 关联 ERP 会计科目名称冗余 |
| **Remark** | NVARCHAR(500) | | 说明备注 |
| **SortOrder** | INT | ✅ | 排序权重，默认 0 |
| **IsActive** | BIT | ✅ | 启用/停用标记：1=启用，0=停用 |
| **DisableDate** | DATETIME | | 停用日期，到期后该类别不可被新单据引用 |
| **IsAttachmentRequired** | BIT | ✅ | 是否需要附件：0=不需要，1=必须上传附件，DEFAULT 0 |
| **BizScope** | VARCHAR(20) | ✅ | 适用业务范围：expense_apply(仅费用申请)/expense(仅报销)/both(通用)，默认 both。费用申请下拉仅显示 expense_apply 和 both；报销下拉仅显示 expense 和 both；导入报销时也不豁免 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：
- `IX_CostCategory_Parent` (ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1
- `IX_CostCategory_Scope` (BizScope, ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 AND IsDeleted = 0

#### 3.8.2 SysBanner — 工作台轮播图（OA 自管）

工作台顶部轮播图组件的数据源。管理员可维护图片、标题、跳转链接和排序。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| **Id** | BIGINT | ✅ | 主键，IDENTITY(1,1) |
| **ImageUrl** | VARCHAR(500) | ✅ | 轮播图片云端存储绝对 URL |
| **Title** | NVARCHAR(100) | | 图片标题/alt 文本 |
| **LinkUrl** | VARCHAR(500) | | 点击跳转链接。为空时不可点击，仅全屏预览图片 |
| **SortOrder** | INT | ✅ | 排序权重，值越小越靠前，默认 0 |
| **IsActive** | BIT | ✅ | 启用/停用标记：1=启用，0=停用 |
| **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() |
| **UpdateTime** | DATETIME | | |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 |

**索引**：`IX_Banner_Active` (IsActive, SortOrder) INCLUDE (ImageUrl, Title, LinkUrl) WHERE IsDeleted = 0

---

## 4. 业务逻辑与约束

### 4.1 软删除

所有 OA 自管表统一使用 `IsDeleted` 软删除。SELECT/JOIN 必须加 `WHERE IsDeleted = 0`。
例外：`OaPermissionChangeLog`（审计日志不可修改不可删除）、`ExpenseApplyMapping`（纯关联表，随主表级联清理）、`AnnouncementTarget` / `AnnouncementReadLog`（随公告级联清理）。

### 4.2 审批状态管理

审批数据不在 OA 本地存储。审批状态、审批时间线、待办列表均通过 .NET → ERP 实时查询。业务表仅存两个字段用于关联和追溯：

| 字段 | 作用 | 更新时机 |
|------|------|---------|
| `ApprovalInstanceId` | 关联 ERP 审批实例 | 提交审批时写入；撤回/驳回重新提交时覆盖 |
| `PreviousInstanceIds` | 追溯历史审批 | 撤回/驳回重新提交时追加旧 instanceId |

**状态同步流程**：
```
列表页 → 实时调 .NET → ERP 查询审批状态
详情页 → 实时调 .NET → ERP 查询审批状态和审批时间线
审批操作 → OA 调 .NET → ERP 执行审批动作
消息通知 → .NET 服务端消息模块在 ERP 审批事件后触发推送
```

**ERP 创建审批实例失败处理**：保持 `draft` 状态，提示"提交审批失败，请稍后重试"，用户可重新提交。

**ERP 创建成功但 OA 回写失败处理**：采用异步补偿机制，后台定时任务扫描"创建成功但未回写"的事务日志，补偿回写 `ApprovalInstanceId`。

### 4.3 费用申请-报销联动

`UsageStatus` 由 .NET 服务端在报销单状态变更时自动重算：

| 已报总额（仅计入 Status IN ('pending', 'approved', 'archived') 的报销单） | UsageStatus | Status 联动 |
|----------|-------------|-------------|
| SUM(ExpenseApplyMapping.ImportedAmount) = 0 | unused | 保持当前状态 |
| > 0 且 < EstimatedAmount | partially_used | 保持当前状态 |
| ≥ EstimatedAmount | fully_used | 自动流转为 `archived` |

> **注意**：`Status='withdrawn'` 和 `IsDeleted=1` 的报销单不计入已报总额。

### 4.4 级联删除（软删除）

| 主表 | 子表 |
|------|------|
| ExpenseApply | ExpenseApplyDetail, Attachment (BizType='expense_apply') |
| Expense | ExpenseDetail, ExpenseApplyMapping, Attachment (BizType='expense') |
| Vehicle | VehiclePassenger |
| OutingLog | OutingLogComment, Attachment (BizType='outing_log') |
| Announcement | AnnouncementTarget, AnnouncementReadLog, Attachment (BizType='announcement') |

### 4.5 单据编号生成

.NET 服务端使用 `sp_getapplock` + `SELECT MAX(ApplicationNo)` 原子生成，保证高并发下按天不重复。

---

## 5. 索引汇总

```sql
-- 费用申请列表
CREATE NONCLUSTERED INDEX IX_ExpenseApply_List 
ON ExpenseApply (ApplicantId, CreateTime DESC) 
INCLUDE (ExpenseApplyNo, EstimatedAmount, Purpose, UsageStatus);

-- 费用申请一键导入（仅已通过且尚未用完的）
CREATE NONCLUSTERED INDEX IX_ExpenseApply_Import 
ON ExpenseApply (ApplicantId, UsageStatus) 
WHERE UsageStatus IN ('unused', 'partially_used');

-- 费用报销列表
CREATE NONCLUSTERED INDEX IX_Expense_List 
ON Expense (ApplicantId, CreateTime DESC) 
INCLUDE (TotalAmount, ExpenseNo, PaymentStatus);

-- 申请↔报销多对多
CREATE NONCLUSTERED INDEX IX_ExpenseApplyMapping_AppId 
ON ExpenseApplyMapping (ExpenseApplyId) 
INCLUDE (ExpenseId, ImportedAmount);

-- 加班列表
CREATE NONCLUSTERED INDEX IX_Overtime_List 
ON Overtime (ApplicantId, CreateTime DESC) 
INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType);

-- 用车列表 + 排期冲突检测
CREATE NONCLUSTERED INDEX IX_Vehicle_List 
ON Vehicle (ApplicantId, CreateTime DESC) 
INCLUDE (ApplicationNo, VehicleId, Purpose, Origin, Destination, StartTime);
CREATE NONCLUSTERED INDEX IX_Vehicle_Collision 
ON Vehicle (VehicleId, StartTime, EndTime) 
WHERE Status IN ('pending', 'approved');

-- 外勤日志-业务员
CREATE NONCLUSTERED INDEX IX_OutingLog_Sales 
ON OutingLog (SalespersonId, CreateTime DESC) 
INCLUDE (CustomerId, CustomerName, CheckInAddress);
-- 外勤日志-部门
CREATE NONCLUSTERED INDEX IX_OutingLog_Dept 
ON OutingLog (DeptId, CreateTime DESC) 
INCLUDE (SalespersonId, CustomerId, CustomerName, CheckInAddress, Status);

-- 公告列表 + 触达审计
CREATE NONCLUSTERED INDEX IX_Announcement_List 
ON Announcement (Type, IsTop DESC, PublishTime DESC) 
INCLUDE (Title, PublisherId, Status, ExpiryDate)
WHERE IsDeleted = 0 AND Status = 'published';
CREATE NONCLUSTERED INDEX IX_Announcement_Ding 
ON AnnouncementReadLog (AnnouncementId, IsRead, IsUrged) 
INCLUDE (UserId, ReadTime);

-- 统一附件
CREATE NONCLUSTERED INDEX IX_Attachment_Biz 
ON Attachment (BizType, BizId, IsDeleted) 
INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder);

-- OA 权限
CREATE UNIQUE NONCLUSTERED INDEX UX_OaUserPermission_UserId_PermissionId 
ON OaUserPermission (UserId, PermissionId) WHERE IsDeleted = 0;
CREATE NONCLUSTERED INDEX IX_UserPermission_UserId 
ON OaUserPermission (UserId, IsDeleted) INCLUDE (PermissionId);
CREATE NONCLUSTERED INDEX IX_PermChangeLog_Target 
ON OaPermissionChangeLog (TargetUserId, CreateTime DESC) 
INCLUDE (OperatorId, ChangeType);

-- 基础字典
CREATE NONCLUSTERED INDEX IX_CostCategory_Parent 
ON SysCostCategory (ParentId, SortOrder) 
INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1;
CREATE NONCLUSTERED INDEX IX_CostCategory_Scope 
ON SysCostCategory (BizScope, ParentId, SortOrder) 
INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 AND IsDeleted = 0;
CREATE NONCLUSTERED INDEX IX_Banner_Active 
ON SysBanner (IsActive, SortOrder) 
INCLUDE (ImageUrl, Title, LinkUrl) WHERE IsDeleted = 0;

-- 外勤点评 + 用车同行人 + 报销明细 + 事前明细
CREATE NONCLUSTERED INDEX IX_OutingLogComment_LogId 
ON OutingLogComment (LogId, IsDeleted) 
INCLUDE (CommenterId, RatingStars, CommentText, CreateTime);
CREATE NONCLUSTERED INDEX IX_VehiclePassenger_VehicleId 
ON VehiclePassenger (VehicleId, IsDeleted) 
INCLUDE (UserId, PassengerName);
CREATE NONCLUSTERED INDEX IX_ExpenseDetail_ExpenseId 
ON ExpenseDetail (ExpenseId, SortOrder) 
INCLUDE (Amount, TotalAmount, ApprovedAmount, CustomerVendorName, OffsetAmount);
CREATE NONCLUSTERED INDEX IX_ExpenseApplyDetail_AppId 
ON ExpenseApplyDetail (ExpenseApplyId, SortOrder) 
INCLUDE (ExpenseCategory, EstimatedAmount, Remark);
```

---

## 6. 枚举取值

### 6.1 单据业务状态

审批状态（draft/pending/approved/rejected/withdrawn）仅 draft 由 OA 本地直接写入，其余状态由 ERP 审批引擎管理，OA 通过 .NET → ERP 实时查询。`archived` 为业务完结终态，由 OA 本地根据业务规则自动流转。

| 值 | 含义 | 适用表 | 写入方 | 触发条件 |
|----|------|--------|--------|---------|
| `draft` | 草稿 | 全部业务表 | OA 本地 | 用户存草稿 |
| `pending` | 待审批 | ExpenseApply, Expense, Overtime, Vehicle | OA 写入后 ERP 接管 | 提交审批 |
| `approved` | 已通过 | 同上 | ERP 管理 | 审批全部通过 |
| `rejected` | 已拒绝 | 同上 | ERP 管理 | 审批人拒绝 |
| `withdrawn` | 已撤回 | 同上 | OA 本地 | 员工撤回 |
| `returned` | 已还车 | Vehicle（独有） | OA 本地 | 还车登记完成 |
| `completed` | 已提交 | OutingLog（不走审批流） | OA 本地 | 提交日志 |
| `published` | 已发布 | Announcement（不走审批流） | OA 本地 | 发布公告 |
| `archived` | 已归档 | ExpenseApply, Expense, Overtime, Vehicle | OA 本地 | 业务完结：ExpenseApply.UsageStatus=fully_used；Expense.PaymentStatus=paid；Overtime 业务完结；Vehicle 还车登记完成 |

### 6.2 付款状态（PaymentStatus）

| 值 | 含义 |
|----|------|
| `unpaid` | 待付款 |
| `paid` | 已付款 |

### 6.3 紧急程度（Urgency）

| 值 | 含义 |
|----|------|
| `normal` | 普通 |
| `urgent` | 紧急 |
| `critical` | 特急 |

### 6.4 申请使用状态（UsageStatus）

| 值 | 含义 |
|----|------|
| `unused` | 未被报销引用 |
| `partially_used` | 部分金额已报销 |
| `fully_used` | 全部金额已报销，不可再被引用。触发 Status 流转为 archived |

### 6.5 加班类型（OtType）

| 值 | 含义 |
|----|------|
| `workday` | 工作日加班 |
| `weekend` | 休息日加班 |
| `holiday` | 法定节假日加班 |

### 6.6 补偿方式（CompensationType）

| 值 | 含义 |
|----|------|
| `overtime_pay` | 全部结算加班费 |
| `comp_leave` | 全部转为调休 |
| `mixed` | 混合模式（按 CompLeaveRatio 分配） |

### 6.7 用车目的（Purpose — Vehicle）

| 值 | 含义 |
|----|------|
| `reception` | 客户接待 |
| `business` | 商务出行 |
| `official` | 公务 |

### 6.8 车辆类型（VehicleType）

| 值 | 含义 |
|----|------|
| `sedan` | 轿车 |
| `suv` | SUV |
| `mpv` | 商务车 |
| `van` | 面包车 |

### 6.9 车辆状态（SysVehicle.Status）

| 值 | 含义 |
|----|------|
| `idle` | 空闲可用 |
| `in_use` | 使用中 |
| `maintenance` | 维修中（不可申请） |

### 6.10 公告分类（Announcement.Type）

| 值 | 含义 |
|----|------|
| `notice` | 通知公告 |
| `policy` | 人事与制度 |
| `activity` | 放假与活动 |

### 6.11 公告范围（PrivateLevel / TargetType）

| PrivateLevel | 含义 | TargetType | TargetId |
|-------------|------|-----------|----------|
| `0` | 全员 | `all` | `'ALL'` |
| `1` | 按部门 | `dept` | ERP 部门 ID |
| `2` | 按用户 | `user` | ERP 用户 ID |

### 6.12 费用类别适用范围（BizScope）

| 值 | 含义 |
|----|------|
| `expense_apply` | 仅费用申请端使用 |
| `expense` | 仅费用报销端使用 |
| `both` | 两端通用 |

### 6.13 附件文件类型（Attachment.FileType）

| 值 | 含义 | 适用 BizType |
|----|------|------------|
| `image` | 通用图片（JPG/PNG/WebP） | expense_apply / expense / announcement |
| `pdf` | PDF 文档 | expense_apply / expense / announcement |
| `doc` | Word 文档 | expense_apply / expense / announcement |
| `xls` | Excel 表格 | expense_apply / expense / announcement |
| `sign_in_photo` | 外勤签到照（水印含授时+GPS） | outing_log |
| `visit_photo` | 外勤拜访现场照 | outing_log |
| `other` | 其他附件 | outing_log |

### 6.14 BOOLEAN 语义

| 字段 | 0 的含义 | 1 的含义 |
|------|---------|---------|
| `IsDeleted` | 正常 | 已软删除 |
| `IsActive` | 停用/禁用 | 启用 |
| `IsRead` | 未读 | 已读 |
| `IsTop` | 普通 | 置顶 |
| `IsUrged` | 未催办 | 已催办 |
| `IsInvoiceVerified` | 未查验 | 已查验 |
| `IsTaxIdMatched` | 未匹配 | 已匹配 |
| `IsCategoryCompliant` | 不合规 | 合规 |

### 6.15 权限模块（OaPermission.Module）

| 值 | 对应权限点前缀 |
|----|-------------|
| `expense` | oa.expense.* |
| `expense_apply` | oa.expense_apply.* |
| `overtime` | oa.overtime.* |
| `vehicle` | oa.vehicle.* |
| `outing_log` | oa.outing_log.* |
| `announcement` | oa.announcement.* |
| `report` | oa.report.* |
| `admin` | oa.admin.* |

### 6.16 权限变更类型（OaPermissionChangeLog.ChangeType）

| 值 | 含义 |
|----|------|
| `assign` | 赋予权限 |
| `revoke` | 移除权限 |
| `toggle_active` | 启用/禁用（权限点全局） |

---

> **文档版本**：v1.1 | 日期：2026-06-27
