# TBOSS OA 模块 — 数据库表结构设计 > 版本:v1.0 | 日期:2026-06-04 | 基于 PRD v1.0 --- ## 1. 设计约定 ### 1.1 技术栈 | 项目 | 规格 | |------|------| | 数据库 | Microsoft SQL Server 2019+ | | 后端 | .NET Framework 4.8 + Dapper / 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,非 FK 约束 | `ApplicantId BIGINT`(注释标注来源 ERP) | | 约束名 | `PK_表名` / `UX_表名_列名` | `PK_Expense`, `UX_Expense_ReportNo` | | 索引名 | `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 审计日志不可删除 | ### 1.4 精度规范 | 类型 | 精度 | 示例 | |------|------|------| | 金额 | `DECIMAL(18,2)` | `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 适配器(预算科目) [ERP 成本] ◄──── CostCenterService 适配器(成本中心) ┌──────────────────────────────────────────────────────┐ │ OA 自管业务表 │ │ │ │ ExpenseApplication ──(1:N)── ExpenseAppDetail │ │ │ (明细行) │ │ │ │ ExpenseApplicationMapping ── 申请↔报销多对多 │ │ ├──(N:1)── Expense │ │ │ (费用明细, 含币种) │ │ │ └──(1:N)── ExpenseDetail │ │ └──(N:1)── ExpenseApplication │ │ │ │ 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 — 费用类别字典 │ └──────────────────────────────────────────────────────┘ ``` > **关键变更**: > - 审批/预算/项目/科目/成本中心 → 全部通过 .NET 适配器对接 ERP,OA 本地不存储 > - `Expense.SourceApplicationId` → `ExpenseApplicationMapping` 多对多关联 > - 4 张附件表合并为 1 张 Attachment 聚合表 --- ## 3. 表结构定义 ### 3.1 OA 权限体系 #### 3.1.1 OaPermission — 权限点字典 OA 独立权限系统的权限点定义表,存储所有可用权限编码及中文名称。权限点按模块分组,管理员通过快捷套餐或手动勾选为用户赋权。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **PermissionCode** | VARCHAR(80) | ✅ | 权限点唯一编码,如 `oa.expense.apply`。UNIQUE 约束 | | **PermissionName** | NVARCHAR(50) | ✅ | 权限点中文名称,如"发起报销",用于管理界面展示 | | **Module** | VARCHAR(30) | ✅ | 所属功能模块,用于分组展示:expense / expense_apply / overtime / vehicle / outing_log / announcement / report / admin | | **SortOrder** | INT | ✅ | 管理界面排序权重,值越小越靠前,默认 0 | > 本表为字典表,不包含 CreateTime/UpdateTime/IsDeleted。权限点预置后一般不增删。 #### 3.1.2 OaUserPermission — 用户-权限关联(ACL) 用户与权限点的多对多关联表。每个用户可拥有多个权限点,通过 IsDeleted 软删除实现权限的授予与回收。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **UserId** | BIGINT | ✅ | ERP 用户 ID(非 FK 约束,关联 ERP 用户主数据) | | **PermissionId** | BIGINT | ✅ | 权限点 ID,FK → OaPermission.Id | | **GrantedBy** | BIGINT | ✅ | 授权操作人 ERP 用户 ID,用于审计追溯 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE(),授权时间 | | **UpdateTime** | DATETIME | | 最后变更时间 | | **IsDeleted** | BIT | ✅ | DEFAULT 0。移除权限时设为 1(软删除,保留历史记录),重新授权时置回 0 | **索引**: - `UX_UserPermission` UNIQUE (UserId, PermissionId) WHERE IsDeleted = 0 — 防止同一权限重复授权 - `IX_UserPermission_UserId` (UserId, IsDeleted) INCLUDE (PermissionId) — 按用户查权限列表 #### 3.1.3 OaPermissionChangeLog — 权限变更审计 记录管理员每次修改用户权限的完整快照。本表仅追加不修改(无 UpdateTime),不可软删除。用于变更追溯和合规审计。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **TargetUserId** | BIGINT | ✅ | 被操作的目标用户 ERP 用户 ID | | **OperatorId** | BIGINT | ✅ | 执行操作的管理员 ERP 用户 ID | | **ChangeType** | VARCHAR(20) | ✅ | 变更类型:`assign`(赋权) / `revoke`(收权) / `toggle_active`(启停) | | **BeforeSnapshot** | NVARCHAR(MAX) | ✅ | 变更前权限 JSON 快照(含权限列表 + IsActive 状态) | | **AfterSnapshot** | NVARCHAR(MAX) | ✅ | 变更后权限 JSON 快照 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE(),审计时间戳 | > 本表无 UpdateTime 和 IsDeleted —— 审计日志不可修改、不可删除。 **索引**:`IX_PermChangeLog_Target` (TargetUserId, CreateTime DESC) INCLUDE (OperatorId, ChangeType) --- ### 3.2 事前申请与费用报销 #### 3.2.1 ExpenseApplication — 事前申请主表 员工在费用发生前提交的预估申请。支持多费用类型、关联项目与预算科目、多行预估明细。审批通过后可被多张报销单分批引用。 **变更说明**:`ApplicantId`、`DeptId`、`ProjectId`、`BudgetSubjectId` 为 ERP 对应实体 ID(非 FK)。`ExpenseType` → `ExpenseTypes`(逗号分隔多选)。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **ApplicationNo** | VARCHAR(30) | ✅ | 单据唯一编号,格式 BXSQ-YYYYMMDD-XXX,提交时由 .NET 服务端原子生成 | | **ApplicantId** | BIGINT | ✅ | 申请人 ERP 用户 ID | | **DeptId** | BIGINT | ✅ | 申请人所属部门 ERP 部门 ID,提交时写入 | | **ApplicationDate** | DATE | | 业务申请日期(提交时写入当日,与 CreateTime 服务器时间区分) | | **ProjectId** | BIGINT | | 关联项目,对应 ERP 项目主数据 ID。通过 .NET → ERP ProjectService 下拉选择,选定后联动查询该项目下的预算科目 | | **BudgetSubjectId** | BIGINT | | 关联预算科目,对应 ERP 科目主数据 ID。通过 .NET → ERP SubjectService 加载。选定后联合 ProjectId 查询可用预算余额 | | **EstimatedAmount** | DECIMAL(18,2) | ✅ | 预估申请总金额 = 所有明细行 Quantity × UnitPrice 汇总,只读不手动修改,默认 0 | | **Urgency** | VARCHAR(10) | ✅ | 紧急程度:normal(普通)/urgent(紧急)/critical(特急),默认 normal | | **ExpenseTypes** | VARCHAR(200) | ✅ | 申请涵盖的费用**大类**,逗号分隔支持多选。如 "travel,office" 表示同时申请差旅费和办公费。选定的值会联动过滤明细行的 SysCostCategory 下拉选项(只展示绑定了该大类的费用小类)。值来自枚举 §6.3 | | **Purpose** | NVARCHAR(500) | ✅ | 费用事由详细说明,限制 200 字 | | **Status** | VARCHAR(20) | ✅ | 业务状态:draft(草稿)/pending(审批中)/approved(已通过)/rejected(已拒绝)/withdrawn(已撤回)。审批状态通过 .NET → ERP 实时查询 | | **UsageStatus** | VARCHAR(20) | ✅ | 被报销引用的状态:unused(未引用)/partially_used(部分引用)/fully_used(已用完)。由报销单提交/删除时自动重算,默认 unused | | **IsTaxIncluded** | BIT | ✅ | 预估金额是否含税:0=不含税,1=含税。影响后续报销核销时的金额对比 | | **ValidUntil** | DATE | | 申请有效期截止日。到期后定时 Job 自动释放冻结预算,UsageStatus 标记为 fully_used,不可再被报销引用 | | **ReferenceNo** | VARCHAR(50) | | 关联的外部单号(合同号/询价单号),用于采购类申请追溯,非必填 | | **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID,提交审批时由 .NET → ERP 创建后回写。撤回/驳回重新提交时覆盖为新值 | | **PreviousInstanceIds** | VARCHAR(MAX) | | 历史审批实例 ID JSON 数组,撤回/驳回重新提交时追加旧 ApprovalInstanceId,用于追溯完整审批历史 | | — | — | — | **以下为费用类型专用字段(数据库全部允许 NULL,应用层按 ExpenseTypes 校验)** | | **EstimatedStartDate** | DATE | | 预计行程开始日期,仅 ExpenseTypes 含 travel 或 meeting 时前端展示并必填 | | **EstimatedEndDate** | DATE | | 预计行程结束日期,仅 ExpenseTypes 含 travel 或 meeting 时必填。校验:必须 ≥ StartDate | | **IsOvernight** | BIT | | 是否过夜:1=需住宿,0=当天来回。仅 travel 时生效,决定住宿天数 | | **TransportType** | VARCHAR(20) | | 交通工具:plane(飞机)/high_speed_rail(高铁)/train(普速火车)/self_drive(自驾)。仅 travel 时必填 | | **EntertainmentTarget** | NVARCHAR(200) | | 招待对象单位名称,仅 ExpenseTypes 含 entertainment 时必填,用于税务合规 | | **EntertainmentLevel** | VARCHAR(20) | | 招待对象层级:normal(普通)/important(重要)/vip(VIP)。影响人均消费标准 | | **GuestCount** | INT | | 外部招待人数,仅 entertainment 时必填 | | **CompanionCount** | INT | | 内部陪同人数,仅 entertainment 时必填。应用层校验:必须 ≤ GuestCount | | **Venue** | NVARCHAR(200) | | 招待或会议举办地点,entertainment 或 meeting 时必填 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**: - `IX_ExpenseApp_List` (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseTypes, UsageStatus) - `IX_ExpenseApp_Import` (ApplicantId, UsageStatus) WHERE UsageStatus IN ('unused', 'partially_used') — 报销导入时快速过滤可用申请 #### 3.2.2 ExpenseAppDetail — 事前申请预估明细 事前申请的逐行预估明细。每行包含费用类别、数量、单价,汇总金额写入主表 EstimatedAmount。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **ApplicationId** | BIGINT | ✅ | 关联主表,FK → ExpenseApplication.Id(主表软删除时级联软删除) | | **ExpenseCategory** | VARCHAR(20) | ✅ | 费用细分类别编码,值来源于 SysCostCategory.CategoryCode | | **Quantity** | INT | ✅ | 数量,默认 1 | | **UnitPrice** | DECIMAL(18,2) | ✅ | 单价 | | **Unit** | VARCHAR(10) | | 计量单位(张/间/人/天/套/个),辅助展示 | | **EstimatedAmount** | DECIMAL(18,2) | ✅ | 单项预估金额 = Quantity × UnitPrice,前端自动计算只读展示 | | **Remark** | NVARCHAR(200) | | 明细项说明备注 | | **SortOrder** | INT | ✅ | 排序号,默认 1,控制移动端渲染顺序 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_ExpenseAppDetail_AppId` (ApplicationId, SortOrder) INCLUDE (ExpenseCategory, EstimatedAmount, Remark) #### 3.2.3 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 张,图片 ≤10MB / PDF ≤20MB - expense:≤9 张,图片 ≤10MB - outing_log:1~9 张,仅支持 image 类型,≤10MB - announcement:≤5 个,PDF/图片/Word/Excel ≤20MB **索引**:`IX_Attachment_Biz` (BizType, BizId, IsDeleted) INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder) #### 3.2.4 ExpenseApplicationMapping — 申请↔报销多对多关联 一张报销单可导入多张事前申请,一张事前申请也可被多张报销单分批引用。每行记录一次导入关联,保存导入金额用于 UsageStatus 计算。**应用层约束**:单次导入的多张申请必须属于同一项目和同一预算科目(ProjectId + BudgetSubjectId 一致),以保证预算扣减的准确性。`ImportedAmount` 总和 ≤ 对应申请 `EstimatedAmount - 已报总额`。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **ExpenseId** | BIGINT | ✅ | 报销单 ID,FK → Expense.Id | | **ApplicationId** | BIGINT | ✅ | 事前申请 ID,FK → ExpenseApplication.Id | | **ImportedAmount** | DECIMAL(18,2) | ✅ | 本次报销从该申请导入的金额。∑ImportedAmount 不能超过申请 EstimatedAmount | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | **约束**:UNIQUE (ExpenseId, ApplicationId) — 同一张报销单不能重复导入同一张申请 **索引**:`IX_AppMapping_AppId` (ApplicationId) INCLUDE (ExpenseId, ImportedAmount) — 查某申请的被引用情况 #### 3.2.5 Expense — 费用报销主表 员工费用报销的核心主表。支持从事前申请拼单导入或直接新建,包含收款银行信息、财务核销标记。审批通过后由财务核销归档。 **变更说明**:删除 `SourceApplicationId`(改为 ExpenseApplicationMapping 多对多)。审批状态实时查询不缓存。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **ReportNo** | VARCHAR(30) | ✅ | 单据唯一编号,格式 BX-YYYYMMDD-XXX,UNIQUE 约束 | | **ApplicantId** | BIGINT | ✅ | 报销人 ERP 用户 ID | | **DeptId** | BIGINT | ✅ | 报销人所属部门 ERP 部门 ID | | **ApplicationDate** | DATE | | 报销申请日期(提交时写入) | | **CostCenterId** | BIGINT | | 成本中心 ERP ID,通过 .NET → ERP CostCenterService 下拉选择。可选,ERP 无数据时隐藏 | | **ProjectId** | BIGINT | | 关联项目 ERP ID(直接新建时手动选,导入事前申请时自动带入) | | **BudgetSubjectId** | BIGINT | | 关联预算科目 ERP ID | | **TotalAmount** | DECIMAL(18,2) | ✅ | 报销总金额 = 所有明细行 TotalAmount 汇总,只读不手动修改,默认 0 | | **Purpose** | NVARCHAR(500) | ✅ | 报销事由说明 | | **BankName** | NVARCHAR(100) | ✅ | 收款银行全称,前端支持下拉联想(数据源 .NET 字典 API),也可自由输入 | | **AccountName** | NVARCHAR(50) | ✅ | 收款开户户名,默认填入当前用户姓名,可修改 | | **BankAccount** | VARCHAR(50) | ✅ | 收款银行账号,前端校验 16-19 位数字格式 | | **IsInvoiceVerified** | BIT | ✅ | 财务核销标记一:发票已在全国增值税发票查验平台验真,默认 0 | | **IsTaxIdMatched** | BIT | ✅ | 财务核销标记二:发票抬头与公司税号一致,默认 0 | | **IsCategoryCompliant** | BIT | ✅ | 财务核销标记三:报销类目与发票项目匹配合规,默认 0 | | **BankTransferNo** | VARCHAR(50) | | 财务核销时录入的银行电汇流水号 | | **VoucherNo** | VARCHAR(50) | | 财务核销时录入的金蝶/财务系统记账凭证号 | | **PaymentStatus** | VARCHAR(20) | ✅ | 付款状态:unpaid(待付款)/paid(已付款)。仅 Status=approved 时允许流转,默认 unpaid | | **Status** | VARCHAR(20) | ✅ | 业务状态:draft/pending/approved/rejected/withdrawn。审批状态通过 .NET → ERP 实时查询 | | **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID,提交审批时由 .NET → ERP 创建后回写 | | **PreviousInstanceIds** | VARCHAR(MAX) | | 历史审批实例 ID JSON 数组 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_Expense_List` (ApplicantId, CreateTime DESC) INCLUDE (TotalAmount, ReportNo, PaymentStatus) #### 3.2.6 ExpenseDetail — 费用报销明细 报销单的逐行费用明细。每行对应一张发票或一笔无发票费用。支持多币种(原币+汇率+本币),币种从 ERP 汇率表获取。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **ExpenseId** | BIGINT | ✅ | 关联报销主表,FK → Expense.Id(主表软删除时级联软删除) | | **ExpenseDate** | DATE | ✅ | 费用实际发生日期 | | **ExpenseType** | VARCHAR(20) | ✅ | 费用**细分类别**编码,值来源于 SysCostCategory.CategoryCode 叶子节点。前端下拉选项受主表 ExpenseTypes(大类)和 SysCostCategory.ExpenseType 绑定关系联动过滤 | | **ExpenseDesc** | NVARCHAR(200) | ✅ | 费用摘要描述 | | **Amount** | DECIMAL(18,2) | ✅ | 原币不含税金额。多币种时为原币金额 | | **TaxAmount** | DECIMAL(18,2) | ✅ | 进项税额,无发票时为 0,默认 0 | | **TotalAmount** | DECIMAL(18,2) | ✅ | 原币价税合计 = Amount + TaxAmount | | **CurrencyCode** | VARCHAR(10) | | 原币币种代码,默认 CNY。选外币(如 USD/EUR/JPY)时从 .NET → ERP ExchangeRateService 获取当日汇率自动填入 ExchangeRate | | **ExchangeRate** | DECIMAL(10,4) | | 原币→本币汇率,CNY 时为 1.0000。由 .NET 服务端从 ERP 汇率表查询后填入 | | **BaseAmount** | DECIMAL(18,2) | | 折算后的本币金额 = TotalAmount × ExchangeRate。预算校验和报表统计均以本币为准 | | **InvoiceNo** | VARCHAR(50) | | 发票号码,InvoiceType=none 时允许 NULL | | **InvoiceCode** | VARCHAR(50) | | 发票代码 | | **InvoiceType** | VARCHAR(20) | ✅ | 发票类型:special(专票)/general(普票)/none(无发票)。无发票场景(单笔 ≤200 元小额零星)时选 none | | **TaxRate** | DECIMAL(5,4) | | 税率,如 0.0600 / 0.0900 / 0.1300 | | **SortOrder** | INT | ✅ | 排序号,默认 1 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_ExpenseDetail_ExpenseId` (ExpenseId, SortOrder) INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType) --- ### 3.3 加班申请 #### 3.3.1 Overtime — 加班申请单 员工加班申请,支持三种加班类型和三种补偿方式。后端自动扣除午休/晚餐盲区计算净工时。审批通过后可用于调休或加班费结算。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **ApplicationNo** | VARCHAR(30) | ✅ | 单据唯一编号,格式 JB-YYYYMMDD-XXX,UNIQUE 约束 | | **ApplicantId** | BIGINT | ✅ | 申请人 ERP 用户 ID | | **DeptId** | BIGINT | ✅ | 所属部门 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。审批状态通过 .NET → ERP 实时查询 | | **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,UNIQUE 约束 | | **ApplicantId** | BIGINT | ✅ | 申请人 ERP 用户 ID | | **DeptId** | BIGINT | ✅ | 所属部门 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(18,2) | | 还车登记:路桥费/停车费等实际费用总额 | | **CostRemark** | NVARCHAR(500) | | 还车登记:费用明细备注 | | **Status** | VARCHAR(20) | ✅ | 业务状态:draft/pending/approved/rejected/withdrawn/returned(已还车)。审批状态通过 .NET → ERP 实时查询 | | **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) | | **ApplicationId** | BIGINT | ✅ | 关联用车单主表,FK → Vehicle.Id | | **UserId** | BIGINT | | 内部同事 ERP 用户 ID(可空,添加外部人员时为空) | | **PassengerName** | NVARCHAR(50) | ✅ | 同行人姓名文本(ERP 用户取 RealName,外部人员自由输入) | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_VehiclePassenger_AppId` (ApplicationId, IsDeleted) INCLUDE (UserId, PassengerName) #### 3.4.3 SysVehicle — 企业车辆资产池(OA 自管) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **LicensePlate** | NVARCHAR(20) | ✅ | 车牌号(含中文字符如粤B12345),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,还车后恢复 idle | | **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,UNIQUE 约束 | | **SalespersonId** | BIGINT | ✅ | 业务员 ERP 用户 ID | | **DeptId** | BIGINT | ✅ | 所属部门 ERP 部门 ID | | **CustomerId** | BIGINT | | 拜访客户 ERP 客户 ID(非 FK)。输入新客户名时,提交后由 .NET → ERP CustomerService 创建新客户并回填此字段 | | **CustomerName** | NVARCHAR(200) | ✅ | 客户公司全称冗余字段,前端输入时调 ERP 联想匹配,也支持自由输入 | | **ContactId** | BIGINT | | 客户联系人 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 | | 员工最后一次查看详情页的时间,用于判断是否有新点评(红点逻辑) | | **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** | BIGINT | ✅ | 点评人(经理)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 | | | | **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** | BIGINT | ✅ | 发布管理员 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) | ✅ | 目标实体类型:dept(按部门)/user(按指定个人) | | **TargetId** | BIGINT | ✅ | 多态外键: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** | BIGINT | ✅ | 被触达员工 ERP 用户 ID | | **IsRead** | BIT | ✅ | 已读标记:0=未读,1=已读。停留 ≥2 秒自动标记,默认 0 | | **ReadTime** | DATETIME | | 员工实际阅读时间 | | **IsUrged** | BIT | ✅ | 是否已被管理员 DING 催办过,默认 0 | | **LastUrgeTime** | DATETIME | | 最后一次 DING 催办时间 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | **约束**:UNIQUE (AnnouncementId, UserId) — 每个员工对每条公告仅有一条触达记录 **索引**:`IX_Announcement_Ding` (AnnouncementId, IsRead, IsUrged) INCLUDE (UserId, ReadTime) --- ### 3.7 基础字典 #### 3.7.1 SysCostCategory — 费用类别字典(OA 自管) 费用报销和事前申请表单中"费用类别"下拉的数据源,支持二级分类。通过 `BizScope` 控制适用范围(事前/报销/通用),`ExpenseType` 可绑定费用大类以进一步过滤。与 ExpenseApplication.ExpenseTypes 的关系:后者是大类(如 travel),选定后联动过滤此表中 `ExpenseType` 匹配的叶子节点作为下拉选项。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | 主键,IDENTITY(1,1) | | **CategoryName** | NVARCHAR(50) | ✅ | 类别中文名称,如"交通费" | | **CategoryCode** | VARCHAR(30) | ✅ | 类别编码,UNIQUE 约束 | | **ParentId** | BIGINT | | 上级类别 ID,FK → SysCostCategory.Id(自引用)。NULL 为一级分类,非 NULL 为叶子节点。仅叶子节点可用于明细录入 | | **BizScope** | VARCHAR(20) | ✅ | 适用业务范围:expense_apply(仅事前申请)/expense(仅报销)/both(通用),默认 both | | **ExpenseType** | VARCHAR(20) | | 绑定费用大类(如 travel/office),进一步细化过滤。NULL 表示通用,不限定大类 | | **SortOrder** | INT | ✅ | 排序权重,默认 0 | | **IsActive** | BIT | ✅ | 启用/停用标记:1=启用,0=停用 | | **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, ExpenseType, ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 AND IsDeleted = 0 #### 3.7.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`(审计日志不可修改不可删除)。 ### 4.2 审批状态管理 审批数据不在 OA 本地存储。审批状态、审批时间线、待办列表均通过 .NET → ERP 实时查询。业务表仅存两个字段用于关联和追溯: | 字段 | 作用 | 更新时机 | |------|------|---------| | `ApprovalInstanceId` | 关联 ERP 审批实例 | 提交审批时写入;撤回/驳回重新提交时覆盖 | | `PreviousInstanceIds` | 追溯历史审批 | 撤回/驳回重新提交时追加旧 instanceId | **状态同步流程**: ``` 列表页 → 实时调 .NET → ERP 查询审批状态 详情页 → 实时调 .NET → ERP 查询审批状态和审批时间线 审批操作 → OA 调 .NET → ERP 执行审批动作 消息通知 → .NET 服务端消息模块在 ERP 审批事件后触发推送 ``` ### 4.3 事前申请-报销联动 `UsageStatus` 由 .NET 服务端在报销单状态变更时自动重算: | 已报总额 | UsageStatus | |----------|-------------| | SUM(ExpenseApplicationMapping.ImportedAmount) = 0 | unused | | > 0 且 < EstimatedAmount | partially_used | | ≥ EstimatedAmount | fully_used | ### 4.4 级联删除(软删除) | 主表 | 子表 | |------|------| | ExpenseApplication | ExpenseAppDetail, Attachment (BizType='expense_apply') | | Expense | ExpenseDetail, ExpenseApplicationMapping, Attachment (BizType='expense') | | 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_ExpenseApp_List ON ExpenseApplication (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseTypes, UsageStatus); -- 事前申请一键导入(仅已通过且尚未用完的) CREATE NONCLUSTERED INDEX IX_ExpenseApp_Import ON ExpenseApplication (ApplicantId, UsageStatus) WHERE UsageStatus IN ('unused', 'partially_used'); -- 费用报销列表 CREATE NONCLUSTERED INDEX IX_Expense_List ON Expense (ApplicantId, CreateTime DESC) INCLUDE (TotalAmount, ReportNo, PaymentStatus); -- 申请↔报销多对多 CREATE NONCLUSTERED INDEX IX_AppMapping_AppId ON ExpenseApplicationMapping (ApplicationId) 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_UserPermission 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, ExpenseType, 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_AppId ON VehiclePassenger (ApplicationId, IsDeleted) INCLUDE (UserId, PassengerName); CREATE NONCLUSTERED INDEX IX_ExpenseDetail_ExpenseId ON ExpenseDetail (ExpenseId, SortOrder) INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType); CREATE NONCLUSTERED INDEX IX_ExpenseAppDetail_AppId ON ExpenseAppDetail (ApplicationId, SortOrder) INCLUDE (ExpenseCategory, EstimatedAmount, Remark); ``` --- ## 6. 枚举取值 ### 6.1 单据业务状态 审批状态(draft/pending/approved/rejected/withdrawn)仅 draft 由 OA 本地直接写入,其余状态由 ERP 审批引擎管理,OA 通过 .NET → ERP 实时查询。 | 值 | 含义 | 适用表 | 写入方 | |----|------|--------|--------| | `draft` | 草稿 | 全部业务表 | OA 本地 | | `pending` | 待审批 | ExpenseApplication, Expense, Overtime, Vehicle | OA 写入后 ERP 接管 | | `approved` | 已通过 | 同上 | ERP 管理 | | `rejected` | 已拒绝 | 同上 | ERP 管理 | | `withdrawn` | 已撤回 | 同上 | OA 本地 | | `returned` | 已还车 | Vehicle(独有) | OA 本地 | | `completed` | 已提交 | OutingLog(不走审批流) | OA 本地 | | `published` | 已发布 | Announcement(不走审批流) | OA 本地 | ### 6.2 付款状态(PaymentStatus) | 值 | 含义 | |----|------| | `unpaid` | 待付款 | | `paid` | 已付款 | ### 6.3 费用类型(ExpenseTypes) | 值 | 含义 | |----|------| | `travel` | 差旅费(含交通/住宿/补助) | | `entertainment` | 业务招待费(宴请/礼品) | | `procurement` | 日常采购(含报价单/合同) | | `activity` | 活动经费(团建/庆典) | | `office` | 办公费(文具/耗材/快递) | | `meeting` | 会议费(内/外部会议) | | `training` | 培训费(外部培训/认证) | ### 6.4 紧急程度(Urgency) | 值 | 含义 | |----|------| | `normal` | 普通 | | `urgent` | 紧急 | | `critical` | 特急 | ### 6.5 交通工具(TransportType) | 值 | 含义 | |----|------| | `plane` | 飞机 | | `high_speed_rail` | 高铁/动车 | | `train` | 火车(普速) | | `self_drive` | 自驾 | ### 6.6 招待对象层级(EntertainmentLevel) | 值 | 含义 | |----|------| | `normal` | 普通人员 | | `important` | 重要人员 | | `vip` | VIP | ### 6.7 申请使用状态(UsageStatus) | 值 | 含义 | |----|------| | `unused` | 未被报销引用 | | `partially_used` | 部分金额已报销 | | `fully_used` | 全部金额已报销,不可再被引用 | ### 6.8 发票类型(InvoiceType) | 值 | 含义 | InvoiceNo/InvoiceCode | |----|------|----------------------| | `special` | 增值税专用发票 | 必填 | | `general` | 增值税普通发票 | 必填 | | `none` | 无发票(小额零星 ≤200 元) | 允许 NULL | ### 6.9 加班类型(OtType) | 值 | 含义 | |----|------| | `workday` | 工作日加班 | | `weekend` | 休息日加班 | | `holiday` | 法定节假日加班 | ### 6.10 补偿方式(CompensationType) | 值 | 含义 | |----|------| | `overtime_pay` | 全部结算加班费 | | `comp_leave` | 全部转为调休 | | `mixed` | 混合模式(按 CompLeaveRatio 分配) | ### 6.11 用车目的(Purpose — Vehicle) | 值 | 含义 | |----|------| | `reception` | 客户接待 | | `business` | 商务出行 | | `official` | 公务 | ### 6.12 车辆类型(VehicleType) | 值 | 含义 | |----|------| | `sedan` | 轿车 | | `suv` | SUV | | `mpv` | 商务车 | | `van` | 面包车 | ### 6.13 车辆状态(SysVehicle.Status) | 值 | 含义 | |----|------| | `idle` | 空闲可用 | | `in_use` | 使用中 | | `maintenance` | 维修中(不可申请) | ### 6.14 公告分类(Announcement.Type) | 值 | 含义 | |----|------| | `notice` | 通知公告 | | `policy` | 人事与制度 | | `activity` | 放假与活动 | ### 6.15 公告范围(PrivateLevel / TargetType) | PrivateLevel | 含义 | TargetType | 含义 | |-------------|------|-----------|------| | `0` | 全员 | — | — | | `1` | 按部门 | `dept` | TargetId → ERP 部门 ID | | `2` | 按用户 | `user` | TargetId → ERP 用户 ID | ### 6.16 费用类别适用范围(BizScope) | 值 | 含义 | |----|------| | `expense_apply` | 仅事前申请端使用 | | `expense` | 仅费用报销端使用 | | `both` | 两端通用 | ### 6.17 附件文件类型(Attachment.FileType) | 值 | 含义 | 适用 BizType | |----|------|------------| | `image` | 图片(JPG/PNG/WebP) | 通用 | | `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.18 BOOLEAN 语义 | 字段 | 0 的含义 | 1 的含义 | |------|---------|---------| | `IsDeleted` | 正常 | 已软删除 | | `IsActive` | 停用/禁用 | 启用 | | `IsRead` | 未读 | 已读 | | `IsTop` | 普通 | 置顶 | | `IsUrged` | 未催办 | 已催办 | | `IsInvoiceVerified` | 未查验 | 已查验 | | `IsTaxIdMatched` | 未匹配 | 已匹配 | | `IsCategoryCompliant` | 不合规 | 合规 | ### 6.19 权限模块(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.20 权限变更类型(OaPermissionChangeLog.ChangeType) | 值 | 含义 | |----|------| | `assign` | 赋予权限 | | `revoke` | 移除权限 | | `toggle_active` | 启用/禁用 | --- ## 7. 常用查询 / 代码示例 > 以下示例基于 Dapper + ADO.NET(.NET Framework 4.8 服务端)。 ### 7.1 报销单详情查询 ```csharp // 主表 + 关联的申请 const string sql = @" SELECT e.Id, e.ReportNo, e.TotalAmount, e.Purpose, e.BankName, e.AccountName, e.BankAccount, e.IsInvoiceVerified, e.IsTaxIdMatched, e.IsCategoryCompliant, e.BankTransferNo, e.VoucherNo, e.Status, e.PaymentStatus, e.ApplicationDate, e.CreateTime, e.ApprovalInstanceId, e.PreviousInstanceIds FROM Expense e WHERE e.Id = @expenseId AND e.IsDeleted = 0; -- 关联的申请 SELECT eam.ApplicationId, eam.ImportedAmount, ea.ApplicationNo, ea.EstimatedAmount, ea.ExpenseTypes FROM ExpenseApplicationMapping eam INNER JOIN ExpenseApplication ea ON ea.Id = eam.ApplicationId AND ea.IsDeleted = 0 WHERE eam.ExpenseId = @expenseId; -- 明细行 SELECT ed.Id, ed.ExpenseDate, ed.ExpenseType, ed.ExpenseDesc, ed.Amount, ed.TaxAmount, ed.TotalAmount, ed.CurrencyCode, ed.ExchangeRate, ed.BaseAmount, ed.InvoiceNo, ed.InvoiceCode, ed.InvoiceType, ed.TaxRate, ed.SortOrder, cc.CategoryName AS ExpenseTypeName FROM ExpenseDetail ed LEFT JOIN SysCostCategory cc ON cc.CategoryCode = ed.ExpenseType AND cc.IsDeleted = 0 WHERE ed.ExpenseId = @expenseId ORDER BY ed.SortOrder; -- 附件 SELECT Id, FileName, FileUrl, FileType, FileSize, DetailId, SortOrder FROM Attachment WHERE BizType = 'expense' AND BizId = @expenseId AND IsDeleted = 0 ORDER BY SortOrder, CreateTime; "; // 申请人姓名、部门通过 .NET API GET /api/user/{ApplicantId} 获取 // 审批时间线通过 .NET API GET /api/oa/approval/timeline?bizType=expense&bizId={id} 获取 ``` ### 7.2 UsageStatus 联动更新 ```csharp public async Task UpdateUsageStatusAsync( IDbConnection conn, IDbTransaction tx, long applicationId) { const string sql = @" UPDATE ea SET ea.UsageStatus = CASE WHEN ISNULL(SUM(eam.ImportedAmount), 0) = 0 THEN 'unused' WHEN ISNULL(SUM(eam.ImportedAmount), 0) >= ea.EstimatedAmount THEN 'fully_used' ELSE 'partially_used' END, ea.UpdateTime = GETDATE() FROM ExpenseApplication ea LEFT JOIN ExpenseApplicationMapping eam ON eam.ApplicationId = ea.Id LEFT JOIN Expense e ON e.Id = eam.ExpenseId AND e.IsDeleted = 0 AND e.Status != 'draft' WHERE ea.Id = @applicationId GROUP BY ea.Id, ea.EstimatedAmount;"; await conn.ExecuteAsync(sql, new { applicationId }, tx); } ``` ### 7.3 单据编号原子生成 ```csharp public async Task GenerateNoAsync( IDbConnection conn, IDbTransaction tx, string prefix, string tableName, DateTime date) { var dateStr = date.ToString("yyyyMMdd"); var pattern = $"{prefix}-{dateStr}-%"; var lockKey = $"AppNo_{prefix}_{dateStr}"; await conn.ExecuteAsync("sp_getapplock @Resource, @LockMode, @LockOwner", new { Resource = lockKey, LockMode = "Exclusive", LockOwner = "Transaction" }, tx); var sql = $@" SELECT MAX(ApplicationNo) FROM {tableName} WITH (UPDLOCK, ROWLOCK) WHERE ApplicationNo LIKE @pattern"; var maxNo = await conn.QuerySingleOrDefaultAsync(sql, new { pattern }, tx); int seq = 1; if (maxNo != null) seq = int.Parse(maxNo.Substring(maxNo.Length - 3)) + 1; return $"{prefix}-{dateStr}-{seq:D3}"; } ``` --- > **文档版本**:v1.0 | 日期:2026-06-04