# 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/Word/Excel ≤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 | | **CurrencyCode** | VARCHAR(10) | | 币别代码,默认 CNY。选外币时从 .NET → ERP ExchangeRateService 获取汇率填入明细行 | | **ApprovedAmount** | DECIMAL(18,2) | ✅ | 核准金额合计 = 所有明细行 ApprovedAmount 汇总,只读不手动修改,默认 0 | | **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 | | **ApprovedAmount** | DECIMAL(18,2) | ✅ | 该行核准金额,默认=TotalAmount(审批直接通过时),审批人可修改。默认 0 | | **CustomerVendorName** | NVARCHAR(200) | | 客户或供应商名称,用于采购/招待类费用追溯 | | **ProjectCode** | VARCHAR(30) | | 项目代号(ERP 项目编码),与 ProjectId 配对冗余存储,方便查询 | | **SubjectCode** | VARCHAR(30) | | 科目代号(ERP 科目编码),与 BudgetSubjectId 配对冗余存储 | | **ProjectCategory** | VARCHAR(20) | | 项目类别(ERP 项目分类字段),冗余存储 | | **OffsetAmount** | DECIMAL(18,2) | ✅ | 已冲账金额(如预付/借款冲抵),默认 0 | | **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, ApprovedAmount, CustomerVendorName, ProjectCode, SubjectCode, ProjectCategory, OffsetAmount) --- ### 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, ApprovedAmount, CustomerVendorName, ProjectCode, SubjectCode, ProjectCategory, OffsetAmount); 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.ApprovedAmount, e.CurrencyCode, 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.ApprovedAmount, ed.CustomerVendorName, ed.ProjectCode, ed.SubjectCode, ed.ProjectCategory, ed.OffsetAmount, 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