# 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