2026-05-30-tboss-oa-database.md 64 KB

TBOSS OA 模块 — 工业级数据库表结构设计说明书(优化完备版)

本设计说明书基于 SQL Server 数据库与 .NET Framework 4.8 后端架构进行深度重构与全面补强。设计核心旨在全量支撑移动端前端所有微观组件交互、多角色权限动态变体、项目科目级预算强管控、发票合规自查校验、考勤盲区扣除、用车冲突拦截、外勤防作弊定位及“一键 DING”强催办等全闭环业务流。


一、 数据库全局设计约定

  1. 主键与标识:所有表主键统一使用 VARCHAR(36) 存储标准 GUID。
  2. 精度规范:所有涉及金额、预算的字段统一使用 DECIMAL(18, 2);涉及GPS经纬度字段统一使用 DECIMAL(10, 6)
  3. 全局必备字段:每张表必须包含 CreateTime DATETIME NOT NULL DEFAULT GETDATE()(创建时间)与 UpdateTime DATETIME NULL(更新时间)。例外SysRoleChangeLog 为不可变审计日志,仅追加不修改,无需 UpdateTime
  4. 软删除机制:核心业务表与基础数据表统一挂载 IsDeleted BIT NOT NULL DEFAULT 0,用于逻辑删除。例外SysRoleChangeLog(审计日志不可删除)、SysProjectBudget(预算记录由 Version 乐观锁控制,不做软删除)。
  5. 状态机规范:所有状态、类型字段统一使用 VARCHAR(20)VARCHAR(30) 存储英文标识字符串,由前端负责本地化国际化语言包映射渲染。
  6. 单据编号生成ApplicationNo / ReportNo / VisitNo 等单号统一格式为 {前缀}-{YYYYMMDD}-{序号},序号按天重置、高位补零至 3 位(如 BX-20260530-001)。后端使用 SQL Server sp_getapplock + SELECT MAX(…) FOR UPDATE 原子获取当日最大序号并 +1,确保高并发下无重复。

二、 基础架构与多角色安全权限表

为了满足管理员端权限配置页面(页面 27)中通过复选框矩阵(TDCheckboxGroup)为员工自由分配复合角色的业务闭环,彻底废弃初版设计中的单一 Role 字符串字段,升级为标准 RBAC 权限模型。

2.1 SysUser(用户表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 用户唯一标识 GUID
UserName VARCHAR(50) UNIQUE 登录账号 / 标准工号(全局唯一,不可重用。软删除用户的工号不释放,新员工不可占用)
RealName NVARCHAR(50) 员工真实姓名
PasswordHash VARCHAR(128) 密码加盐哈希值
DeptId VARCHAR(36) FK 归属部门,关联 SysDepartment.Id
Position NVARCHAR(50) 岗位/职称名称
Phone VARCHAR(20) 绑定手机号(用于接收原生短信网关通知)
Email VARCHAR(100) 企业邮箱地址
AvatarUrl VARCHAR(500) 头像绝对存储路径(用于各类卡片及评论头像渲染)
DefaultBankName NVARCHAR(100) 默认收款银行全称(报销单首次提交审批通过后自动回写)
DefaultAccountName NVARCHAR(50) 默认收款户名(报销单首次提交审批通过后自动回写)
DefaultBankAccount VARCHAR(50) 默认收款银行账号(报销单首次提交审批通过后自动回写)
IsActive BIT DEFAULT 1 账号启用/停用状态(1=启用,0=禁用)
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 最后修改时间
IsDeleted BIT DEFAULT 0 软删除标记

2.2 SysRole(系统角色表)[全新引入]

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 角色唯一标识 GUID
RoleCode VARCHAR(30) UNIQUE 角色编码(employee/approver/finance/admin
RoleName NVARCHAR(50) 角色中文名称(员工/经理/财务/系统管理员)
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

2.3 SysUserRole(用户角色多对多映射表)[全新引入]

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 映射唯一标识 GUID
UserId VARCHAR(36) FK 关联用户,连接 SysUser.Id
RoleId VARCHAR(36) FK 关联角色,连接 SysRole.Id
CreateTime DATETIME DEFAULT 授权时间
UpdateTime DATETIME 变更时间
IsDeleted BIT DEFAULT 0 软删除标记(移除角色时软删除,保留历史记录)

约束CREATE UNIQUE INDEX UX_UserRole_Active ON SysUserRole (UserId, RoleId) WHERE IsDeleted = 0,防止同一角色重复授权的同时,允许删除后重新授权。

2.4 SysRoleChangeLog(角色变更审计日志表)[全新引入]

记录页面 27 管理员每次修改用户角色和启用/禁用操作的完整审计轨迹。不可物理删除,不可修改。

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 日志唯一标识 GUID
TargetUserId VARCHAR(36) FK 被操作的目标用户,关联 SysUser.Id
OperatorId VARCHAR(36) FK 执行操作的管理员,关联 SysUser.Id
ChangeType VARCHAR(20) 变更类型(assign_role角色变更 / toggle_active启用禁用 / batch批量操作)
BeforeSnapshot NVARCHAR(MAX) 变更前 JSON 快照(含角色列表 + IsActive 状态)
AfterSnapshot NVARCHAR(MAX) 变更后 JSON 快照
CreateTime DATETIME DEFAULT 操作时间(不可修改,仅追加)

2.5 SysDepartment(企业组织架构部门表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 部门唯一标识 GUID
DeptName NVARCHAR(100) 部门完整名称
ParentId VARCHAR(36) FK 上级部门ID,自引用 SysDepartment.Id,用于生成树形组织架构(无上级则为NULL)
ManagerId VARCHAR(36) FK 部门负责人/主管,关联 SysUser.Id
SortOrder INT DEFAULT 0 排序权重,值越小在架构树中越靠前
IsActive BIT DEFAULT 1 部门启用状态
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记(部门裁撤/合并时标记)

2.6 SysBanner(工作台轮播图配置表)[全新引入]

支持页面 3 工作台顶部 TDRotation 轮播图组件的动态配置。

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 轮播项唯一标识 GUID
ImageUrl VARCHAR(500) 轮播图片云端存储绝对 URL
Title NVARCHAR(100) 轮播图片标题/alt 文本
LinkUrl VARCHAR(500) 点击跳转链接(可空,为空则不可点击)
SortOrder INT DEFAULT 0 排序权重,值越小越靠前
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

三、 财务控制、项目预算与成本中心表

为全方位支持普通员工发起单据时(页面 4),前端能够通过“项目+预算科目”级联选择器实时动态加载出【当前可用预算余额】,并在超支时强制触发高管特批流。

3.1 SysProject(企业项目表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 项目唯一标识 GUID
ProjectName NVARCHAR(200) 项目完整名称
ProjectCode VARCHAR(50) UNIQUE 项目标准控制编码(如: PROJ-2026-001)
DeptId VARCHAR(36) FK 归属主控部门,关联 SysDepartment.Id
TotalBudget DECIMAL(18,2) DEFAULT 0 项目总核定预算金额
SpentBudget DECIMAL(18,2) DEFAULT 0 项目已累计消耗支出总额(用于经理看板进度条计算)
StartDate DATE 项目合同/预定启动日期
EndDate DATE 项目预定结项日期
IsActive BIT DEFAULT 1 项目状态(1=进行中,0=已封账)
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

SpentBudget 与 SysProjectBudget 的关系SpentBudget 为项目级累计消耗汇总(= 该项目下所有 SysProjectBudget.AvailableAmount 扣减额之和),用于经理看板进度条快速读取。当 SysProjectBudget.AvailableAmount 扣减时,同步以相同金额累加到 SysProject.SpentBudget,两者在同一事务内更新。

TotalBudget 约束TotalBudget 为项目总预算上限。应用层需保证该项目下所有 SysProjectBudget.AllocatedAmount 之和 ≤ TotalBudget,超分配应在配置时拒绝。

3.2 SysBudgetSubject(预算科目字典表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 科目唯一标识 GUID
SubjectName NVARCHAR(100) 科目中文名称(如:差旅费/业务招待费/日常采购)
SubjectCode VARCHAR(50) UNIQUE 科目代码财务标准核算码(如:EXP-5001)
ParentId VARCHAR(36) FK 上级科目ID,自引用 SysBudgetSubject.Id,用于级联下拉多层解析(叶子节点才可绑定金额)
SortOrder INT DEFAULT 0 字典排序权重
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

3.3 SysCostCategory(费用类别字典表)[全新引入]

为页面 5 费用报销明细区"费用类别"下拉选择器提供数据源(交通费/住宿费/餐饮费/办公用品等),支持二级分类。

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 类别唯一标识 GUID
CategoryName NVARCHAR(50) 类别名称(如:交通费、住宿费、餐饮费)
CategoryCode VARCHAR(30) UNIQUE 类别编码(如:EXP-TRAVEL、EXP-HOTEL)
ParentId VARCHAR(36) FK 上级类别 ID,自引用 SysCostCategory.Id,支持二级分类(叶子节点才可用于明细录入)
SortOrder INT DEFAULT 0 排序权重
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

3.4 SysProjectBudget(项目-科目联合预算动态控制表)[全新引入]

本表为高并发预算强管控的核心,前端在录入明细金额时,后台通过此表的数据进行实时差额逻辑校验。

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 联合预算唯一标识 GUID
ProjectId VARCHAR(36) FK 关联项目,连接 SysProject.Id
SubjectId VARCHAR(36) FK 关联预算科目,连接 SysBudgetSubject.Id
ExpenseType VARCHAR(20) 费用类型维度(可空,NULL 表示适用于所有费用类型。非 NULL 时如 travel/entertainment/procurement/activity,用于页面 4 费用类型联动过滤可用科目)
AllocatedAmount DECIMAL(18,2) 该项目在此科目上被分配的初始总额度
AvailableAmount DECIMAL(18,2) 实时动态可用余额(前端组件读取此项进行超支强拦截)
Version ROWVERSION 乐观锁并发控制:每次扣减余额时 WHERE Version = @oldVersion,防止超扣
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 变更校准时间

并发控制策略:本表是高并发预算扣减的核心热点表。所有扣减 AvailableAmount 的 UPDATE 语句必须携带 WHERE Version = @oldVersion 做乐观锁校验。若 ROWCOUNT = 0(版本冲突),应用层重试最多 3 次。禁止在事务外进行"先读后写"操作。

扣减时机:预算余额在事前申请/报销单审批通过时扣减(即 Statuspending 变为 approved 时执行 AvailableAmount = AvailableAmount - 申请金额)。草稿和审批中状态不扣减。若单据被拒绝或撤回,已扣减的预算需回冲。

约束CHECK (AvailableAmount >= 0),禁止余额为负。CHECK (AllocatedAmount > 0),分配额度必须为正数。UNIQUE (ProjectId, SubjectId, ExpenseType),同一项目+科目+费用类型组合唯一。当 ExpenseType 为 NULL 时,使用过滤唯一索引:CREATE UNIQUE INDEX ... ON SysProjectBudget (ProjectId, SubjectId) WHERE ExpenseType IS NULL

ExpenseType NULL 与具体类型共存规则:同一 (ProjectId, SubjectId) 下,ExpenseType 为 NULL 的行作为"通用预算"(fallback),ExpenseType 为具体值的行作为"特定费用类型预算"。后端查询时优先精确匹配 ExpenseType 值,匹配不到时 fallback 到 ExpenseType IS NULL 的行。应用层禁止对同一 (ProjectId, SubjectId) 同时创建 NULL 行和多个特定类型行(只能选其一:全类型通用预算 OR 按类型分预算)。

3.5 SysCostCenter(企业成本中心表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 成本中心唯一标识 GUID
CenterName NVARCHAR(100) 成本中心名称(如:第一数字化研发中心)
CenterCode VARCHAR(50) UNIQUE 编码体系
DeptId VARCHAR(36) FK 归属核算部门,关联 SysDepartment.Id
IsActive BIT DEFAULT 1 是否启用
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

3.6 SysBank(银行字典表)[全新引入]

为页面 5 费用报销表单"开户行全称"下拉联想输入提供数据源。管理员可维护银行列表。

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 银行唯一标识 GUID
BankName NVARCHAR(100) 银行全称(如:中国工商银行深圳竹子林支行)
BankCode VARCHAR(20) UNIQUE 银行联行号
SortOrder INT DEFAULT 0 排序权重
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

四、 核心业务子系统一:事前申请与费用报销模块

完美匹配”事前申请发起”到”费用报销核销”、收款银行录入、财务”三字合规校验”强制解锁,以及最终金蝶/通用的财务凭证归档和电汇单据序列号记录。

4.1 ExpenseApplication(事前申请单主表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 事前单据唯一标识 GUID
ApplicationNo VARCHAR(30) UNIQUE 单号序列,自动生成(格式: BXSQ-YYYYMMDD-XXX)
ApplicantId VARCHAR(36) FK 发起员工 ID,关联 SysUser.Id
DeptId VARCHAR(36) FK 提单人当时所属部门,关联 SysDepartment.Id
ProjectId VARCHAR(36) FK 关联控制项目,关联 SysProject.Id(草稿时可空,提交时应用层校验非空)
BudgetSubjectId VARCHAR(36) FK 关联主控科目,关联 SysBudgetSubject.Id(草稿时可空,提交时应用层校验非空)
EstimatedAmount DECIMAL(18,2) DEFAULT 0 预估申请总金额(明细项之和,草稿无明细时为 0)
Urgency VARCHAR(10) DEFAULT 'normal' 紧急程度(normal/urgent/critical
ExpenseType VARCHAR(20) 费用类型(travel差旅费 / entertainment招待费 / procurement日常采购 / activity活动经费),用于列表筛选及报表聚合。明细行 ExpenseCategory 为更细粒度的费用子类
Purpose NVARCHAR(500) 费用事由详细大文本正文(限制200字,前端映射)
Status VARCHAR(20) DEFAULT 'draft' 状态机(draft/pending/approved/rejected/withdrawn
CurrentApproverId VARCHAR(36) FK 当前节点审批人 ID,关联 SysUser.Id。提交时写入首级审批人,每次审批动作完成后更新为下一级(或 NULL 表示流程结束)
CreateTime DATETIME DEFAULT 发起/存草稿时间
UpdateTime DATETIME 状态变动时间
IsDeleted BIT DEFAULT 0 逻辑删除标记

4.2 ExpenseAppDetail(事前费用申请预估明细子表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 明细项唯一标识 GUID
ApplicationId VARCHAR(36) FK 关联主表,连接 ExpenseApplication.Id(级联删除)
ExpenseCategory VARCHAR(20) 费用细分类别
EstimatedAmount DECIMAL(18,2) 单项明细预估金额
Remark NVARCHAR(200) 单项细目描述说明
SortOrder INT DEFAULT 1 移动端渲染序列权重
CreateTime DATETIME DEFAULT 明细添加时间
UpdateTime DATETIME 明细修改时间

4.3 Expense(费用报销单主表)

字段名 类型 必填 约束 说明
Id VARCHAR(36) PK 报销主表唯一标识 GUID
ReportNo VARCHAR(30) UNIQUE 报销单号序列,自动生成(格式: BX-YYYYMMDD-XXX)
SourceApplicationId VARCHAR(36) FK 关联导入的事前申请 ID(用于一键导入复制数据回填闭环)
ApplicantId VARCHAR(36) FK 报销发起人 ID,关联 SysUser.Id
DeptId VARCHAR(36) FK 报销部门,关联 SysDepartment.Id
CostCenterId VARCHAR(36) FK 成本中心,关联 SysCostCenter.Id
ProjectId VARCHAR(36) FK 关联项目,关联 SysProject.Id(草稿时可空,提交时应用层校验非空)
BudgetSubjectId VARCHAR(36) FK 关联预算科目,关联 SysBudgetSubject.Id(草稿时可空,提交时应用层校验非空)
TotalAmount DECIMAL(18,2) DEFAULT 0 实际报销总金额(系统自动计算累加明细,草稿无明细时为 0)
Purpose NVARCHAR(500) 报销事由(由事前申请导入时复制 ExpenseApplication.Purpose,或手工填写)
BankName NVARCHAR(100) 收款银行全称(值来源于 SysBank.BankName 下拉联想,非 FK 约束,用户可自由输入。如:中国工商银行深圳竹子林支行)
AccountName NVARCHAR(50) 收款银行开户户名(必须与报销人一致或支持特批)
BankAccount VARCHAR(50) 收款银行账号
IsInvoiceVerified BIT DEFAULT 0 财务核销自查标记一:发票系统已查验真实合法
IsTaxIdMatched BIT DEFAULT 0 财务核销自查标记二:发票抬头与公司税号一致
IsCategoryCompliant BIT DEFAULT 0 财务核销自查标记三:报销类目与发票项目合规
BankTransferNo VARCHAR(50) 财务线下付款标记:银行电子电汇流水号(转账唯一凭证)
VoucherNo VARCHAR(50) 财务归档凭证标记:金蝶/通用财务系统记账凭证号
PaymentStatus VARCHAR(20) DEFAULT 'unpaid' 付款状态机(unpaid/paying/paid
Status VARCHAR(20) DEFAULT 'draft' 审批状态机(draft/pending/approved/rejected/withdrawn
CurrentApproverId VARCHAR(36) FK 当前节点审批人 ID,关联 SysUser.Id
CreateTime DATETIME DEFAULT 报销单创建时间
UpdateTime DATETIME 财务核销归档付款时间
IsDeleted BIT DEFAULT 0 逻辑删除标记

双状态机转换规则Status(审批状态)与 PaymentStatus(付款状态)相互独立但存在依赖:

> Status:   draft → pending → approved → (财务打款后) → 归档
>                        ↘ rejected
>                        ↘ withdrawn
> 
> PaymentStatus: unpaid → paying → paid
>                (仅 Status='approved' 时允许流转,Status='rejected'/'withdrawn' 时强制回退到 unpaid)
> ```
> - 审批通过(`approved`)后 `PaymentStatus` 才解锁,允许财务操作 `unpaid → paying → paid`。
> - 审批被拒绝或撤回时,若 `PaymentStatus` 已进入 `paying`/`paid`,系统禁止撤回并告警。

### 4.4 ExpenseDetail(费用报销明细子表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 明细唯一标识 GUID |
| **ExpenseId** | VARCHAR(36) | ✅ | FK | 关联主表,连接 `Expense.Id`(级联删除) |
| **ExpenseDate** | DATE | ✅ |  | 费用发生具体日期(由前端 `TDDatePicker` 选择) |
| **ExpenseType** | VARCHAR(20) | ✅ |  | 细分费用类别(值来源于 `SysCostCategory.CategoryCode`,由应用层校验),对应发票项目分类 |
| **ExpenseDesc** | NVARCHAR(200) | ✅ |  | 对应费用的详细摘要描述 |
| **Amount** | DECIMAL(18,2) | ✅ |  | 金额(不含税净价金额) |
| **TaxAmount** | DECIMAL(18,2) | ✅ | DEFAULT 0 | 进项税额(无发票则录入0) |
| **TotalAmount** | DECIMAL(18,2) | ✅ |  | 价税合计金额(前端自动计算并反向累加到主表总额) |
| **InvoiceNo** | VARCHAR(50) |  |  | 发票号码(OCR自动扫描识别/手动录入) |
| **InvoiceCode** | VARCHAR(50) |  |  | 发票代码 |
| **InvoiceType** | VARCHAR(20) | ✅ |  | 发票类型(`special`专用发票 / `general`普通发票 / `none`无发票) |
| **TaxRate** | DECIMAL(5,4) |  |  | 税率(如:0.06,0.09,0.13) |
| **SortOrder** | INT | ✅ | DEFAULT 1 | 明细行排序号 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 明细录入时间 |
| **UpdateTime** | DATETIME |  |  | 明细修改时间 |

> **发票查验与 InvoiceNo 为空的处理规则**:当明细行 `InvoiceType = 'none'`(无发票场景,如小额零星采购、交通卡充值等),`InvoiceNo`/`InvoiceCode` 允许为 NULL。财务核销时三项合规自查(`IsInvoiceVerified`/`IsTaxIdMatched`/`IsCategoryCompliant`)的判定逻辑为:
> - 若该报销单全部明细行均为 `InvoiceType = 'none'`,三项自查标记自动置 1,跳过发票查验流程,财务仅审核费用合理性。
> - 若存在非 `none` 明细行但对应的 `InvoiceNo` 为空,前端发票查验区展示警告"明细行 X 缺少发票号码,请补充后重新查验",财务核销按钮保持锁定。

### 4.5 ExpenseAttachment(费用报销发票及附件表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 附件唯一标识 GUID |
| **ExpenseId** | VARCHAR(36) | ✅ | FK | 关联报销主表,连接 `Expense.Id` |
| **DetailId** | VARCHAR(36) |  | FK | 可空。若绑定则代表是属于某行明细的发票凭证 |
| **FileName** | NVARCHAR(200) | ✅ |  | 原始文件名称 |
| **FileUrl** | VARCHAR(500) | ✅ |  | 云端对象存储绝对 URL(前端发票格渲染直接调用读取) |
| **FileSize** | BIGINT |  |  | 文件体积大小(单位:字节) |
| **FileType** | VARCHAR(20) | ✅ |  | 格式划分(`image`/`pdf`/`doc`) |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 上传时间 |
| **UpdateTime** | DATETIME |  |  | 附件更换/删除时间 |

### 4.6 ExpenseApplicationAttachment(事前申请支撑材料附件表)`[全新引入]`

> 与 `ExpenseAttachment` 分工明确:本表存业务合理性支撑材料(报价单、合同、出差审批截图等),`ExpenseAttachment` 存事后报销的发票及凭证。事前申请无发票,附件性质不同,需独立建表。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 附件唯一标识 GUID |
| **ApplicationId** | VARCHAR(36) | ✅ | FK | 关联事前申请,连接 `ExpenseApplication.Id`(级联删除) |
| **FileName** | NVARCHAR(200) | ✅ |  | 原始文件名称 |
| **FileUrl** | VARCHAR(500) | ✅ |  | 云端对象存储绝对 URL |
| **FileType** | VARCHAR(20) | ✅ |  | 格式划分(`image`/`pdf`/`doc`) |
| **FileSize** | BIGINT |  |  | 文件体积大小(单位:字节,用于前端 ≤10MB/≤20MB 校验) |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 上传时间 |
| **UpdateTime** | DATETIME |  |  | 替换/删除时间 |

---

## 五、 核心业务子系统二:勤务考勤、公车调度与全反作弊外勤日志模块

全方位支持:① 加班净小时扣除核算;② 公车预约排期检测防御与还车登记核销闭环;③ 外勤日志只读定位地址及带有五星主管点评机制的穿透表设计。

### 5.1 Overtime(加班申请单业务表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 加班唯一标识 GUID |
| **ApplicationNo** | VARCHAR(30) | ✅ | UNIQUE | 格式序列(如:JB-YYYYMMDD-XXX) |
| **ApplicantId** | VARCHAR(36) | ✅ | FK | 加班员工,关联 `SysUser.Id` |
| **DeptId** | VARCHAR(36) | ✅ | FK | 关联部门 |
| **OtType** | VARCHAR(10) | ✅ |  | 加班类型(`workday`工作日/`weekend`休息日/`holiday`法定节假日) |
| **CompensationType** | VARCHAR(20) | ✅ |  | 补偿形式(`overtime_pay`结算加班费 / `comp_leave`转为调休时数 / `mixed`混合模式) |
| **CompLeaveRatio** | DECIMAL(3,2) |  |  | **混合模式下调休比例(0.00~1.00,仅 `CompensationType='mixed'` 时生效。如 0.3 表示 30% 转调休、70% 结算加班费)**。约束:`CHECK (CompLeaveRatio IS NULL OR (CompLeaveRatio > 0 AND CompLeaveRatio < 1))` |
| **StartTime** | DATETIME |  |  | 加班启动时间(草稿时可空,提交时应用层校验非空) |
| **EndTime** | DATETIME |  |  | 加班截止时间(草稿时可空,提交时应用层校验非空) |
| **NetOtHours** | DECIMAL(4,1) | ✅ | DEFAULT 0 | **本次申请加班净工时(后端匹配规则并扣除12:00等盲区后的结算净值,草稿未计算时为 0)** |
| **Reason** | NVARCHAR(500) | ✅ |  | 加班主因详细说明 |
| **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | 状态机(`draft`/`pending`/`approved`/`rejected`/`withdrawn`) |
| **CurrentApproverId** | VARCHAR(36) |  | FK | **当前节点审批人 ID,关联 `SysUser.Id`** |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 提单创建时间 |
| **UpdateTime** | DATETIME |  |  | 状态/审批变更时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 逻辑删除标记 |

> **约束**:`CHECK (StartTime IS NULL OR EndTime IS NULL OR StartTime < EndTime)`,当开始/结束时间均已填写时,开始时间必须早于结束时间。

### 5.2 Vehicle(用车申请与还车核销全生命周期表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 用车单据唯一标识 GUID |
| **ApplicationNo** | VARCHAR(30) | ✅ | UNIQUE | 格式序列(如:YC-YYYYMMDD-XXX) |
| **ApplicantId** | VARCHAR(36) | ✅ | FK | 用车申请人,关联 `SysUser.Id` |
| **DeptId** | VARCHAR(36) | ✅ | FK | 关联部门 |
| **VehicleId** | VARCHAR(36) |  | FK | 申请调配的公车,关联 `SysVehicle.Id`(后台用于排期重叠检测,草稿时可空,提交时应用层校验非空) |
| **Purpose** | VARCHAR(20) | ✅ |  | 用车目类别(`reception`客户接待/`business`商务出行/`official`公务) |
| **Reason** | NVARCHAR(500) | ✅ |  | **用车事由详细说明(页面 13"用车事由输入框"对应字段)** |
| **Origin** | NVARCHAR(200) | ✅ |  | 行程预计出发地点(前端定位获取或输入) |
| **OriginLongitude** | DECIMAL(10,6) |  |  | **始发地经度(地图选点回填,用于详情页微缩地图标记)** |
| **OriginLatitude** | DECIMAL(10,6) |  |  | **始发地纬度** |
| **Destination** | NVARCHAR(200) | ✅ |  | 行程预计最终目的地地点 |
| **DestLongitude** | DECIMAL(10,6) |  |  | **目的地经度(地图选点回填,用于详情页微缩地图标记)** |
| **DestLatitude** | DECIMAL(10,6) |  |  | **目的地纬度** |
| **PassengerCount** | INT |  | DEFAULT 1 | **同行总人数(页面 13"同行总人数"输入框对应字段)** |
| **StartTime** | DATETIME |  |  | 预计出车时间(草稿时可空,提交时应用层校验非空) |
| **EndTime** | DATETIME |  |  | 预计还车时间(草稿时可空,提交时应用层校验非空) |
| **ActualReturnTime** | DATETIME |  |  | **还车登记抽屉:资产实际归还还车时间** |
| **StartOdometer** | DECIMAL(10,2) |  |  | **还车登记抽屉:出车开走前里程表读数(核对车辆最新里程)** |
| **EndOdometer** | DECIMAL(10,2) |  |  | **还车登记抽屉:还车入库后里程表最终读数** |
| **ActualCost** | DECIMAL(18,2) |  |  | **还车登记抽屉:用车中途实际产生路桥费/停车费总报账** |
| **CostRemark** | NVARCHAR(500) |  |  | 用车报账明细备注说明 |
| **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | 复合状态机(`draft`/`pending`/`approved`/`rejected`/`withdrawn`/`returned`已还车归档)。还车登记在 `Approved` 状态下均可触发(不限制当前时间必须超过 EndTime),提前还车时前端标注提示文案 |
| **CurrentApproverId** | VARCHAR(36) |  | FK | **当前节点审批人 ID,关联 `SysUser.Id`** |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 提单时间 |
| **UpdateTime** | DATETIME |  |  | 状态/还车/核销变更时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 逻辑删除标记 |

> **约束**:`CHECK (StartTime IS NULL OR EndTime IS NULL OR StartTime < EndTime)`,当出车/还车时间均已填写时,出车时间必须早于还车时间。`CHECK (EndOdometer IS NULL OR StartOdometer IS NULL OR EndOdometer > StartOdometer)`,还车时里程表必须递增。

### 5.3 VehiclePassenger(随行同行人员映射表)

> 支持通过原生通讯录添加多个随行同事,同时兼容添加外部纯文本客户姓名。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 乘客关联标识 GUID |
| **ApplicationId** | VARCHAR(36) | ✅ | FK | 关联用车单主表,连接 `Vehicle.Id` |
| **UserId** | VARCHAR(36) |  | FK | 可空。如果是企业内部同事,绑定 `SysUser.Id` |
| **PassengerName** | NVARCHAR(50) | ✅ |  | 同行人员姓名文本(内部同事则冗余姓名,外部客商直接填文本) |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 添加时间 |
| **UpdateTime** | DATETIME |  |  | 变更时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 软删除标记(移除同行人时标记) |

### 5.4 OutingLog(外勤外务拜访日志主表)

> 本表核心专为防止业务员作弊设计,经纬度与逆地理编码地址在前端一律设为 `ReadOnly`,强制匹配现场防伪相机水印。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 日志唯一标识 GUID |
| **VisitNo** | VARCHAR(30) | ✅ | UNIQUE | 格式序列(如:VST-YYYYMMDD-XXX) |
| **SalespersonId** | VARCHAR(36) | ✅ | FK | 业务员 ID,关联 `SysUser.Id` |
| **DeptId** | VARCHAR(36) | ✅ | FK | 关联部门 |
| **CustomerId** | VARCHAR(36) |  | FK | 联动企业核心客户池,关联 `SysCustomer.Id`(草稿时可空,提交时应用层校验非空)。**当业务员输入不存在的客户名称时,提交时后端在同一事务内先 INSERT SysCustomer 获取新 Id,再 UPDATE OutingLog 回填** |
| **CustomerName** | NVARCHAR(200) | ✅ |  | 客户公司标准全称冗余字段 |
| **ContactId** | VARCHAR(36) |  | FK | 关联客户联系人,连接 `SysCustomerContact.Id` |
| **CheckInLongitude** | DECIMAL(10,6) |  |  | **GPS 硬件定位强制抓取精准经度(草稿时可空,提交时应用层校验非空)** |
| **CheckInLatitude** | DECIMAL(10,6) |  |  | **GPS 硬件定位强制抓取精准纬度(草稿时可空,提交时应用层校验非空)** |
| **CheckInAddress** | NVARCHAR(500) |  |  | **宿主底层逆地理编码出的具体街道绝对写实地址(禁止员工篡改,草稿时可空,提交时应用层校验非空)** |
| **VisitSummary** | NVARCHAR(2000) | ✅ |  | 今日外勤核心工作总结正文(大文本域) |
| **NextPlan** | NVARCHAR(500) |  |  | **后续推进计划(页面 16"后续推进计划大文本框"对应字段)** |
| **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | 状态(`draft`暂存草稿 / `completed`已提交) |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 服务器防伪授时创建时间 |
| **UpdateTime** | DATETIME |  |  | 日志修改/点评更新时间 |
| **LastViewedTime** | DATETIME |  |  | **员工最后一次查看该日志详情页的时间。用于列表页"新点评"红点:当 `OutingLogComment.CreateTime > COALESCE(LastViewedTime, '1900-01-01')` 且评论者非该员工本人时,显示未读标记** |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 逻辑删除标记 |

### 5.5 OutingLogComment(主管考评打分与互动多轮点评历史表)`[全新引入]`

> 支持经理在详情页底部通过评分组件(`TDRate`)进行打分,并支持团队内部以多气泡流水样式进行指导性文字互动。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 点评项唯一标识 GUID |
| **LogId** | VARCHAR(36) | ✅ | FK | 关联的外勤拜访日志,连接 `OutingLog.Id`(级联删除) |
| **CommenterId** | VARCHAR(36) | ✅ | FK | 发表点评的经理或协同人 ID,关联 `SysUser.Id` |
| **RatingStars** | INT |  |  | **主管点选的考评星级(1-5星,仅限主管首条考评记录生效)。约束:`CHECK (RatingStars IS NULL OR (RatingStars >= 1 AND RatingStars <= 5))`** |
| **CommentText** | NVARCHAR(1000) | ✅ |  | 批示及指导意见具体文字内容(前端渲染为气泡样式) |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 回复点评发布时间 |
| **UpdateTime** | DATETIME |  |  | 点评修改时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 软删除标记(经理撤回点评时标记) |

### 5.6 OutingLogAttachment(外勤日志防伪照片及附件表)`[全新引入]`

> 支持页面 16 的现场强制拍照上传及页面 18 的照片墙展示。照片由原生相机拍摄后在本地渲染防伪水印(服务器授时 + GPS 经纬度),上传至云端存储。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 附件唯一标识 GUID |
| **LogId** | VARCHAR(36) | ✅ | FK | 关联外勤日志,连接 `OutingLog.Id`(级联删除) |
| **FileName** | NVARCHAR(200) | ✅ |  | 原始文件名/水印照片描述 |
| **FileUrl** | VARCHAR(500) | ✅ |  | 云端对象存储绝对 URL(前端照片墙直接读取) |
| **FileType** | VARCHAR(20) | ✅ |  | 用途分类(`sign_in_photo`签到照 / `visit_photo`拜访现场照 / `other`其他) |
| **FileSize** | BIGINT |  |  | 文件体积大小(单位:字节,用于前端校验) |
| **SortOrder** | INT |  | DEFAULT 0 | 照片墙排序 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 上传时间 |
| **UpdateTime** | DATETIME |  |  | 替换/删除时间 |

### 5.7 SysVehicle(企业车辆资产表)

> Vehicle 表 `VehicleId` 关联本表,用于公车排期冲突检测及车辆状态管理。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 车辆唯一标识 GUID |
| **LicensePlate** | NVARCHAR(20) | ✅ | UNIQUE | 车牌号(含中文字符,如:粤B12345) |
| **VehicleType** | VARCHAR(20) | ✅ |  | 车辆类型(`sedan`轿车 / `suv`SUV / `mpv`商务车 / `van`面包车) |
| **Brand** | NVARCHAR(50) |  |  | 品牌型号 |
| **Seats** | INT |  |  | 核定座位数 |
| **DriverName** | NVARCHAR(50) |  |  | 默认驾驶员姓名 |
| **Status** | VARCHAR(20) | ✅ | DEFAULT 'idle' | 车辆状态(`idle`空闲 / `in_use`使用中 / `maintenance`维修中) |
| **IsActive** | BIT | ✅ | DEFAULT 1 | 启用/停用 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 创建时间 |
| **UpdateTime** | DATETIME |  |  | 修改时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 软删除标记(车辆报废/出售时标记) |

> **Status 与 Vehicle 申请同步规则**:`SysVehicle.Status` 由后端在审批动作时自动维护:
> - 用车申请审批通过 → `SysVehicle.Status` 置为 `in_use`
> - 用车单据变为 `rejected`/`withdrawn`/`returned` → 若无其他重叠时段的活跃申请,`SysVehicle.Status` 恢复为 `idle`
> - 车辆维修登记 → 人工置为 `maintenance`,维修期间所有申请校验拒绝

### 5.8 SysCustomer(客户基础资料表)

> 为外勤日志模块提供客户名称联想匹配。业务员在创建拜访日志时,输入客户名称即触发本表的模糊检索。

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 客户唯一标识 GUID |
| **CustomerName** | NVARCHAR(200) | ✅ |  | 客户公司全称 |
| **ShortName** | NVARCHAR(100) |  |  | 客户简称(快速检索用) |
| **Address** | NVARCHAR(500) |  |  | 客户办公/注册地址 |
| **Longitude** | DECIMAL(10,6) |  |  | 客户地址经度(用于外勤距离偏差计算) |
| **Latitude** | DECIMAL(10,6) |  |  | 客户地址纬度 |
| **SalespersonId** | VARCHAR(36) |  | FK | 默认负责业务员,关联 `SysUser.Id` |
| **IsActive** | BIT | ✅ | DEFAULT 1 | 启用/停用 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 创建时间 |
| **UpdateTime** | DATETIME |  |  | 修改时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 软删除标记 |

### 5.9 SysCustomerContact(客户联系人表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 联系人唯一标识 GUID |
| **CustomerId** | VARCHAR(36) | ✅ | FK | 关联客户,连接 `SysCustomer.Id` |
| **ContactName** | NVARCHAR(50) | ✅ |  | 联系人姓名 |
| **Position** | NVARCHAR(50) |  |  | 联系人职务 |
| **Phone** | VARCHAR(20) |  |  | 联系电话 |
| **Email** | VARCHAR(100) |  |  | 电子邮箱 |
| **SortOrder** | INT |  | DEFAULT 0 | 排序权重 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 创建时间 |
| **UpdateTime** | DATETIME |  |  | 修改时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 软删除标记 |

---

## 六、 核心业务子系统三:行政通告、已读审计与 DING 强催办模块

全方位支持:① 全员或指定部门范围的红头文件触达过滤;② 无感秒级停留已读状态日志置换;③ 管理员“一键 DING 强力催办”对未读人员执行系统级高优先级 Push。

### 6.1 Announcement(行政通告公告主表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 公告唯一标识 GUID |
| **Title** | NVARCHAR(200) | ✅ |  | 行政公告标题 |
| **Content** | NVARCHAR(MAX) | ✅ |  | 公告正文(支持 HTML 标记语言 / 标准 Markdown 排版正文存储) |
| **Type** | VARCHAR(20) | ✅ |  | 分类(`notice`通知公告 / `policy`人事与制度 / `activity`放假与活动安排) |
| **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | 发布状态(`draft`草稿仅创建者和管理员可见 / `published`已发布) |
| **PublisherId** | VARCHAR(36) | ✅ | FK | 发布管理员 ID,关联 `SysUser.Id` |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 首次创建时间(含草稿) |
| **PublishTime** | DATETIME |  |  | 设定公文发布生效时间(草稿时为 NULL) |
| **IsTop** | BIT | ✅ | DEFAULT 0 | 是否全局置顶(1=置顶,0=普通) |
| **PrivateLevel** | INT | ✅ | DEFAULT 0 | 可见域范围级(0=全员发布,1=按特定部门树,2=按指定特定用户) |
| **ExpiryDate** | DATETIME |  |  | 自动下架失效过期截止时间(可空,不填则永不过期) |
| **UpdateTime** | DATETIME |  |  | 最后修改/下架时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 逻辑删除标记 |

### 6.2 AnnouncementTarget(公告可见权限发布范围表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 范围标识 GUID |
| **AnnouncementId** | VARCHAR(36) | ✅ | FK | 关联公告主表,连接 `Announcement.Id` |
| **TargetType** | VARCHAR(10) | ✅ |  | 范围实体类别划分(`dept`按指定部门 / `user`按指定个人用户) |
| **TargetId** | VARCHAR(36) | ✅ |  | 多态外键:根据 `TargetType` 指向 `SysDepartment.Id`(`dept`)或 `SysUser.Id`(`user`)。SQL Server 不支持多态 FK 约束,由应用层校验 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 创建时间 |
| **UpdateTime** | DATETIME |  |  | 修改时间 |

### 6.3 AnnouncementReadLog(全员行政触达率审计与一键 DING 记录表)`[全面重构]`

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 审计行唯一标识 GUID |
| **AnnouncementId** | VARCHAR(36) | ✅ | FK | 关联公告,连接 `Announcement.Id` |
| **UserId** | VARCHAR(36) | ✅ | FK | 被下发触达的员工 ID,关联 `SysUser.Id` |
| **IsRead** | BIT | ✅ | DEFAULT 0 | **已读标记(员工进入页面无感无声停留2秒后异步流置换为1)** |
| **ReadTime** | DATETIME |  |  | 员工实际查阅阅读时间 |
| **IsUrged** | BIT | ✅ | DEFAULT 0 | **强催办标记:是否被管理员一键执行过强力 [DING 催办] 动作** |
| **LastUrgeTime** | DATETIME |  |  | **最后一次触发系统高级别强制锁屏 Push 或原生短信网关的时间** |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 触达记录创建时间(初始化 Job 写入时自动赋值) |
| **UpdateTime** | DATETIME |  |  | 已读/催办更新时间 |

> **初始化策略**:当管理员将公告 `Status` 从 `draft` 变为 `published`(发布动作)时,后台通过异步 Job 根据 `PrivateLevel` 写入触达记录:
> - `PrivateLevel=0`(全员):为所有在职员工(`SysUser.IsActive=1 AND IsDeleted=0`)批量插入 `IsRead=0` 记录。
> - `PrivateLevel=1`(按部门)/ `PrivateLevel=2`(按用户):依据 `AnnouncementTarget` 表的覆盖范围写入。
> 
> 草稿状态(`Status='draft'`)不触发任何触达记录写入。新员工入职时不回溯历史公告。管理员 DING 催办仅作用于已存在记录的未读人员(`IsRead=0`)。
> 
> **约束**:`UNIQUE (AnnouncementId, UserId)`,每个员工对每条公告仅有一条触达记录。

### 6.4 AnnouncementAttachment(公告公文红头附件链接表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 附件标识 |
| **AnnouncementId** | VARCHAR(36) | ✅ | FK | 连接 `Announcement.Id` |
| **FileName** | NVARCHAR(200) | ✅ |  | 附件名称 |
| **FileUrl** | VARCHAR(500) | ✅ |  | 绝对存储 URL 下载地址 |
| **FileType** | VARCHAR(20) | ✅ |  | 格式划分(`image`/`pdf`/`doc`/`xls`) |
| **FileSize** | BIGINT |  |  | 文件体积大小(单位:字节,用于前端 ≤20MB 校验) |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 上传时间 |
| **UpdateTime** | DATETIME |  |  | 替换/删除时间 |

### 6.5 Message(框架外壳左侧首 Tab 消息通知流水与动态路由寻址表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 消息行唯一标识 GUID |
| **UserId** | VARCHAR(36) | ✅ | FK | 接收消息通知的员工 ID,关联 `SysUser.Id` |
| **SenderId** | VARCHAR(36) | ✅ | FK | **消息发送人 ID(`announcement`→`Announcement.PublisherId` / `approval_notice`→单据 `ApplicantId` / `approval_result`→`ApprovalRecord.ApproverId`),消息创建时固化,后续不随源数据变更** |
| **Title** | NVARCHAR(200) | ✅ |  | 消息通知卡片大标题 |
| **Content** | NVARCHAR(500) |  |  | 卡片摘要文本(如:“您的单据已被王经理拒绝,请点击查看缘由”) |
| **MsgType** | VARCHAR(20) | ✅ |  | 分类(`approval_notice`待办 / `approval_result`结果 / `announcement`公告) |
| **BizType** | VARCHAR(30) | ✅ |  | **精准寻址路由标记(`announcement`公告 / `expense_apply`事前申请 / `expense`报销 / `overtime`加班 / `vehicle`用车 / `outing_log`外勤日志),配合前端 GoRouter** |
| **BizId** | VARCHAR(36) | ✅ |  | **关联单据的具体主键 ID(点击卡片携带此 ID 直接穿透切换详情页)** |
| **IsRead** | BIT | ✅ | DEFAULT 0 | **未读已读状态(决定 Appshell Tab 上的 TDBadge 红点是否淡出)** |
| **ReadTime** | DATETIME |  |  | 点击标记阅读时间 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 消息推送产生时间 |
| **UpdateTime** | DATETIME |  |  | 已读/删除时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | **软删除标记(前端左滑删除操作执行软删除,`IsDeleted=1` 且 `UpdateTime` 记录删除时间)** |

> **多态外键说明**:`BizId` 与 `BizType` 同为多态关联,约束策略与 `ApprovalRecord` 一致——业务表一律软删除,查询时通过 `BizType` 路由 + `IsDeleted` 过滤。消息表中 `MsgType = 'announcement'` 的记录,其 `BizId` 指向 `Announcement.Id`,`BizType` 对应值为 `announcement`。

---

## 七、 核心业务子系统四:工作流引擎与审批层级状态状态表

全方位支持经理端详情页底部”同意、拒绝、转交”三元动态控制栏。拒绝时强制写入不小于5字理由;转交时无缝重写审批链,将其移交给第三方特定领导。

> **审批事务边界**:每次审批动作(同意/拒绝/转交)必须在**同一数据库事务**内原子完成以下操作:
> 1. 更新业务主表 `Status` 和 `CurrentApproverId`
> 2. 写入/更新 `ApprovalRecord`(Action + Opinion + ApprovalTime)
> 3. 若审批通过:扣减 `SysProjectBudget.AvailableAmount` + 累加 `SysProject.SpentBudget`(携带 Version 乐观锁)
> 4. 发送 `Message` 通知至申请人(`MsgType='approval_result'`)
> 5. 若当前层级为最后一级:更新 `SysVehicle.Status`(仅用车单据)
> 
> 任一步骤失败则整体回滚,确保不会出现”状态已改但消息未发”或”预算已扣但状态未变”的不一致。

### 7.1 ApprovalChain(企业业务审批链流程配置表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 配置项唯一标识 GUID |
| **BizType** | VARCHAR(30) | ✅ |  | 业务单据类型标识(`expense_apply`事前申请 / `expense`报销 / `overtime`加班 / `vehicle`用车)。注意:`announcement`和`outing_log`不走审批链 |
| **DeptId** | VARCHAR(36) |  | FK | 可空。若绑定则代表仅适用于此特定部门,若为空代表属于企业全局通用流 |
| **LevelNo** | INT | ✅ |  | 审批层级序号阶梯(从1级开始,1代表一级审批,2代表二级高管,以此类推) |
| **ApproverId** | VARCHAR(36) |  | FK | 固定指定特定审批人,关联 `SysUser.Id` |
| **ApproverRole** | VARCHAR(20) |  |  | 按岗位角色动态寻人(`dept_manager`主管自动寻人 / `finance`财务专岗) |
| **ConditionType** | VARCHAR(20) |  |  | **条件触发类型(`budget_exceed`超预算高管特批 / `amount_threshold`金额门槛 / NULL 无条件)** |
| **ConditionThreshold** | DECIMAL(18,2) |  |  | **条件阈值(如超预算时触发此层级审批的金额临界值)** |
| **ConditionConfig** | NVARCHAR(500) |  |  | **条件扩展配置 JSON(如多条件组合、阈值范围等高级规则)** |
| **IsActive** | BIT | ✅ | DEFAULT 1 | 流程是否生效 |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 创建时间 |
| **UpdateTime** | DATETIME |  |  | 修改时间 |
| **IsDeleted** | BIT | ✅ | DEFAULT 0 | 软删除标记(审批链废弃时标记) |

> **互斥约束**:`ApproverId` 与 `ApproverRole` 至少填写一项:`CHECK (ApproverId IS NOT NULL OR ApproverRole IS NOT NULL)`。
> 
> **唯一约束**:`UNIQUE (BizType, DeptId, LevelNo)`(结合 `IsDeleted = 0` 过滤,见 Index 22/23)。
> 
> **条件分支**:当 `ConditionType IS NOT NULL` 时,该审批层级仅在满足条件时动态插入审批链。

### 7.2 ApprovalRecord(审批流全历史节点决策运行流转表)

| 字段名 | 类型 | 必填 | 约束 | 说明 |
| --- | --- | --- | --- | --- |
| **Id** | VARCHAR(36) | ✅ | PK | 审批记录节点唯一标识 GUID |
| **BizId** | VARCHAR(36) | ✅ |  | 关联的具体业务单据主键 ID(对应各类单据主表的 `Id`) |
| **BizType** | VARCHAR(30) | ✅ |  | 业务单据类型映射标志(`expense_apply`事前申请 / `expense`报销 / `overtime`加班 / `vehicle`用车) |
| **ApproverId** | VARCHAR(36) | ✅ | FK | 执行审批动作的领导/财务人员 ID,关联 `SysUser.Id` |
| **ApprovalLevel** | INT | ✅ |  | 当前所处的审批绝对层级(1,2,3级...) |
| **Action** | VARCHAR(20) | ✅ |  | **三元决策动作代码(`pending`处理中/`approve`同意/`reject`拒绝/`transfer`转交)** |
| **TransferToUserId** | VARCHAR(36) |  | FK | **当 Action='transfer' 时,通过原生组织通讯录勾选回传的被接管新领导 ID** |
| **Opinion** | NVARCHAR(500) |  |  | **审批意见备注(当 Action='reject' 时,后台强制限制必须输入 >=5 个汉字字数)** |
| **IsValid** | BIT | ✅ | DEFAULT 1 | **有效标记(1=有效,0=已失效)。当单据被拒绝后员工"重新编辑并发起"时,该 BizId 下所有旧审批记录的 IsValid 置为 0,新审批链重新从 Level 1 开始** |
| **CreateTime** | DATETIME | ✅ | DEFAULT | 单据到达该层级节点的时间 |
| **ApprovalTime** | DATETIME |  |  | 领导实际点击提交动作的执行时间 |
| **UpdateTime** | DATETIME |  |  | 审批记录变更时间 |

> **多态外键说明**:`BizId` 与 `BizType` 组成多态关联,根据 `BizType` 的值指向不同的业务主表(`expense_apply`→`ExpenseApplication.Id`,`expense`→`Expense.Id`,`overtime`→`Overtime.Id`,`vehicle`→`Vehicle.Id`)。SQL Server 不支持多态外键约束,无法在数据库层保证引用完整性。
> 
> **补偿措施**:
> 1. 业务表删除单据时执行**软删除**(`IsDeleted=1`),不物理删除行,确保审批记录的 `BizId` 始终有效。
> 2. 后端查询时始终通过 `BizType` 路由到正确的表,JOIN 时追加 `AND target.IsDeleted = 0`。
> 3. 定时 Job 扫描 `ApprovalRecord` 中 `BizId` 在对应业务表中不存在的记录,标记为异常并告警。
> 
> **审批链失效与重建规则**:当员工对 `rejected` 单据执行"重新编辑并发起"时:
> 1. 该 `BizId` 下所有 `ApprovalRecord` 的 `IsValid` 置为 0(标记旧审批链失效)。
> 2. 新的审批流程从 `ApprovalLevel = 1` 开始,重新匹配 `ApprovalChain` 配置写入新记录。
> 3. 前端时间线组件渲染时增加 `WHERE IsValid = 1` 过滤条件。

---

## 八、 企业全外键关系约束图 (ER Topology Mapping)

                   [SysRole] 
                       │ (1:N)
                 [SysUserRole] (复合岗位矩阵桥接表)
                       ▲
                       │ (N:1)
              [SysRoleChangeLog] (角色变更审计,关联 OperatorId + TargetUserId)

[SysDepartment] ──── SysUser ◄─────────────────────────────────┐

  │ (自引用)          │                                                           │
  │ ParentId          ├─(1:N)─► [SysCostCenter] (成本中心)                       │
  │                   │                                                           │
  │ (1:N)             ├─(1:N)─► [ExpenseApplication] ──(1:N)──► [ExpenseAppDetail]│
  ├─► [Expense]       ├─► (1:N)─► [ExpenseApplicationAttachment]                 │
  ├─► [Overtime]      ├─(1:N)─► [Expense] ───────────(1:N)──► [ExpenseDetail]    │
  ├─► [Vehicle]       │ (N:1)      │ (N:1)            (N:1)                     │
  └─► [OutingLog]     │ [SysCostCenter] └─(1:N)─► [ExpenseAttachment]            │
      (DeptId)        │                                                           │
                      │         [SysBank] (银行字典,供 Expense.BankName 下拉联想)  │
                      │                                                           │
                      ├─(1:N)─► [Overtime] (NetOtHours 净工时)                    │
                      ├─(1:N)─► [Vehicle] ──(1:N)──► [VehiclePassenger]           │
                      │            │            ┌─► (Odometer / ActualCost 还车核销)│
                      │            │ (N:1)      │                                  │
                      │       [SysVehicle] ◄────┘                                  │
                      ├─(1:N)─► [OutingLog] ──(1:N)──► [OutingLogComment]         │
                      │            │ (N:1)           └─► (CheckInAddress 强控防伪定位)│
                      │       [SysCustomer] ──(1:N)──► [SysCustomerContact]       │
                      │            │                                               │
                      │            └─(1:N)─► [OutingLogAttachment]                  │
                      ├─(1:N)─► [Announcement] (Publisher)                        │
                      │            ├─(1:N)─► [AnnouncementTarget]                   │
                      │            ├─(1:N)─► [AnnouncementAttachment]               │
                      │            └─(1:N)─► [AnnouncementReadLog] (DING 强力审计)  │
                      └─(1:N)─► [Message] (BizType & BizId 消息寻址核心组件)       │

[SysProject] ──┬──► SysProjectBudget │

           │ (N:1)                                                                 │
     [SysDepartment] (项目归属部门)                                                │
                                                                                   │

[SysBudgetSubject] ┘ (自引用 ParentId) │

SysCostCategory │

ApprovalChain │

ApprovalRecord ─────┘

SysBanner


---

## 九、 生产级高性能企业级索引布局规范 (SQL Server Optimized)

为了给 Flutter 前端应用提供极致流畅的下拉刷新、防抖模糊联想以及大数据穿透下钻多维分析,必须在 SQL Server 部署阶段直接建立以下高度优化复合非聚集索引:

```sql
-- 1. 员工/审批/财务端 在列表页进行高频五个状态 Chip 标签分类切流及时间流倒序展示索引
CREATE NONCLUSTERED INDEX IX_Expense_List_Path 
ON Expense (ApplicantId, Status, CreateTime DESC) 
INCLUDE (TotalAmount, ReportNo, PaymentStatus);

-- 2. 事前申请一键导入核销单据侧边抽屉展示(快速过滤已通过且未核销的单据)
CREATE NONCLUSTERED INDEX IX_ExpenseApp_Import_Drawer 
ON ExpenseApplication (ApplicantId, Status) 
WHERE Status = 'approved';

-- 3. 经理端待办列表”待我审批的单据”及一键向左滑动快速批量同意接口过滤路径
CREATE NONCLUSTERED INDEX IX_ApprovalRecord_Approver_Todo 
ON ApprovalRecord (ApproverId, Action, BizType, IsValid) 
INCLUDE (BizId, ApprovalLevel, CreateTime)
WHERE IsValid = 1;

-- 4. 高并发前台录入明细、键盘离焦事件触发预算可用余额校验的极端优化唯一索引
CREATE UNIQUE NONCLUSTERED INDEX UX_ProjectBudget_Concurrency_Control 
ON SysProjectBudget (ProjectId, SubjectId, ExpenseType) 
INCLUDE (AvailableAmount, AllocatedAmount);

-- 4.1 预算表 NULL ExpenseType 过滤唯一索引(防止同一项目+科目下多条通用预算记录)
CREATE UNIQUE NONCLUSTERED INDEX UX_ProjectBudget_NULL_Type 
ON SysProjectBudget (ProjectId, SubjectId) 
WHERE ExpenseType IS NULL;

-- 5. 车辆申请模块——防止同一辆公车在相同时间跨度产生排期重叠相撞的多条件防卫索引
CREATE NONCLUSTERED INDEX IX_Vehicle_Schedule_Collision_Defense 
ON Vehicle (VehicleId, StartTime, EndTime) 
WHERE Status IN ('pending', 'approved');

-- 6. 行政公告详情页——管理员一键“DING一下”进行审计时瞬间抓取并下发未读员工 UserId 的穿透索引
CREATE NONCLUSTERED INDEX IX_Announcement_Audit_Ding 
ON AnnouncementReadLog (AnnouncementId, IsRead, IsUrged) 
INCLUDE (UserId, ReadTime);

-- 7. 业务员外出拜访日志列表——按业务员进行时间倒序滚动的骨架屏加载优化索引
CREATE NONCLUSTERED INDEX IX_OutingLog_Sales_Timeline 
ON OutingLog (SalespersonId, CreateTime DESC) 
INCLUDE (CustomerId, CustomerName, CheckInAddress);

-- 8. 权限管理抽屉——按目标用户查询角色变更审计历史
CREATE NONCLUSTERED INDEX IX_RoleChangeLog_TargetUser 
ON SysRoleChangeLog (TargetUserId, CreateTime DESC) 
INCLUDE (OperatorId, ChangeType, BeforeSnapshot, AfterSnapshot);

-- 9. 消息列表查询——排除已软删除的消息
CREATE NONCLUSTERED INDEX IX_Message_List_Active 
ON Message (UserId, IsDeleted, IsRead, CreateTime DESC) 
INCLUDE (Title, MsgType, BizType, BizId, SenderId);

-- 10. 费用类别字典——级联下拉子类别加载
CREATE NONCLUSTERED INDEX IX_CostCategory_Parent 
ON SysCostCategory (ParentId, SortOrder) 
INCLUDE (CategoryName, CategoryCode)
WHERE IsActive = 1;

-- 11. 银行字典——下拉列表按排序加载
CREATE NONCLUSTERED INDEX IX_Bank_List 
ON SysBank (SortOrder, IsActive) 
INCLUDE (BankName, BankCode);

-- 12. 客户名称模糊搜索——外勤日志选客户时的即时联想
CREATE NONCLUSTERED INDEX IX_Customer_Name_Search 
ON SysCustomer (IsActive, CustomerName) 
INCLUDE (ShortName, SalespersonId);

-- 13. 按业务员筛选客户池
CREATE NONCLUSTERED INDEX IX_Customer_Salesperson 
ON SysCustomer (SalespersonId, IsActive) 
INCLUDE (CustomerName, ShortName);

-- 14. 经理端按部门查看下属外勤日志时间线
CREATE NONCLUSTERED INDEX IX_OutingLog_Dept_Timeline 
ON OutingLog (DeptId, CreateTime DESC) 
INCLUDE (SalespersonId, CustomerId, CustomerName, CheckInAddress, Status);

-- 15. 审批记录有效链查询——前端时间线组件按 BizId+BizType 过滤 IsValid=1
CREATE NONCLUSTERED INDEX IX_ApprovalRecord_Biz_Valid 
ON ApprovalRecord (BizId, BizType, IsValid, ApprovalLevel) 
INCLUDE (ApproverId, Action, Opinion, ApprovalTime);

-- 16. 事前申请列表——五状态 Chip 切换 + 时间倒序
CREATE NONCLUSTERED INDEX IX_ExpenseApp_List_Path 
ON ExpenseApplication (ApplicantId, Status, CreateTime DESC) 
INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseType);

-- 17. 加班记录列表——五状态 Chip 切换 + 时间倒序
CREATE NONCLUSTERED INDEX IX_Overtime_List_Path 
ON Overtime (ApplicantId, Status, CreateTime DESC) 
INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType);

-- 18. 用车记录列表——状态 Chip 切换 + 时间倒序
CREATE NONCLUSTERED INDEX IX_Vehicle_List_Path 
ON Vehicle (ApplicantId, Status, CreateTime DESC) 
INCLUDE (ApplicationNo, VehicleId, Purpose, Origin, Destination, StartTime);

-- 19. 按角色查用户——权限管理页 + 审批链动态寻人
CREATE NONCLUSTERED INDEX IX_UserRole_Role_Reverse 
ON SysUserRole (RoleId, IsDeleted) 
INCLUDE (UserId);

-- 20. 用户登录/搜索——用户名+姓名
CREATE NONCLUSTERED INDEX IX_User_Search 
ON SysUser (UserName, RealName) 
INCLUDE (DeptId, IsActive, IsDeleted);

-- 21. 用户角色映射——防止同一角色重复授权(过滤 IsDeleted=0)
CREATE UNIQUE NONCLUSTERED INDEX UX_UserRole_Active 
ON SysUserRole (UserId, RoleId) 
WHERE IsDeleted = 0;

-- 22. 审批链配置——全局配置唯一约束
CREATE UNIQUE NONCLUSTERED INDEX UX_ApprovalChain_Global 
ON ApprovalChain (BizType, LevelNo) 
WHERE DeptId IS NULL AND IsDeleted = 0;

-- 23. 审批链配置——部门级配置唯一约束
CREATE UNIQUE NONCLUSTERED INDEX UX_ApprovalChain_Dept 
ON ApprovalChain (BizType, DeptId, LevelNo) 
WHERE DeptId IS NOT NULL AND IsDeleted = 0;

-- 24. 公告列表——分类筛选 + 置顶优先 + 时间倒序
CREATE NONCLUSTERED INDEX IX_Announcement_List 
ON Announcement (Type, IsTop DESC, PublishTime DESC) 
INCLUDE (Title, PublisherId, Status, ExpiryDate)
WHERE IsDeleted = 0 AND Status = 'published';

-- 25. 部门树形架构——按上级部门加载子节点
CREATE NONCLUSTERED INDEX IX_Department_Tree 
ON SysDepartment (ParentId, SortOrder) 
INCLUDE (DeptName, ManagerId, IsActive)
WHERE IsDeleted = 0;

-- 26. 工作台轮播图——按排序加载启用项
CREATE NONCLUSTERED INDEX IX_Banner_Active 
ON SysBanner (IsActive, SortOrder) 
INCLUDE (ImageUrl, Title, LinkUrl)
WHERE IsDeleted = 0;

-- 27. 报销单关联事前申请——一键导入时排除已被引用的申请
CREATE NONCLUSTERED INDEX IX_Expense_SourceApp 
ON Expense (SourceApplicationId) 
WHERE SourceApplicationId IS NOT NULL;

-- 28. 子表 FK 查询——外勤日志点评列表
CREATE NONCLUSTERED INDEX IX_OutingLogComment_LogId 
ON OutingLogComment (LogId, IsDeleted) 
INCLUDE (CommenterId, RatingStars, CommentText, CreateTime);

-- 29. 子表 FK 查询——费用报销明细行
CREATE NONCLUSTERED INDEX IX_ExpenseDetail_ExpenseId 
ON ExpenseDetail (ExpenseId, SortOrder) 
INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType);

-- 30. 子表 FK 查询——事前申请明细行
CREATE NONCLUSTERED INDEX IX_ExpenseAppDetail_AppId 
ON ExpenseAppDetail (ApplicationId, SortOrder) 
INCLUDE (ExpenseCategory, EstimatedAmount, Remark);

-- 31. 子表 FK 查询——报销发票附件
CREATE NONCLUSTERED INDEX IX_ExpenseAttachment_ExpenseId 
ON ExpenseAttachment (ExpenseId) 
INCLUDE (FileName, FileUrl, FileType, FileSize);

-- 32. 子表 FK 查询——事前申请支撑材料附件
CREATE NONCLUSTERED INDEX IX_ExpenseAppAttachment_AppId 
ON ExpenseApplicationAttachment (ApplicationId) 
INCLUDE (FileName, FileUrl, FileType, FileSize);

-- 33. 子表 FK 查询——公告附件
CREATE NONCLUSTERED INDEX IX_AnnouncementAttachment_AnnId 
ON AnnouncementAttachment (AnnouncementId) 
INCLUDE (FileName, FileUrl, FileType, FileSize);

-- 34. 子表 FK 查询——外勤日志照片墙
CREATE NONCLUSTERED INDEX IX_OutingLogAttachment_LogId 
ON OutingLogAttachment (LogId, SortOrder) 
INCLUDE (FileName, FileUrl, FileType);

-- 35. 子表 FK 查询——用车同行人列表
CREATE NONCLUSTERED INDEX IX_VehiclePassenger_AppId 
ON VehiclePassenger (ApplicationId, IsDeleted) 
INCLUDE (UserId, PassengerName);