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

TBOSS OA 模块 — 工业级数据库表结构设计说明书


1. 基础元数据与技术栈 (Metadata & Tech Stack)

1.1 数据库类型与版本

项目 规格
数据库 Microsoft SQL Server 2019+ (兼容 SQL Server 2022)
宿主平台 Windows Server
字符集 默认排序规则(建议 Chinese_PRC_CI_AS
连接池规范 .NET SqlConnection 默认连接池(Min Pool Size=5, Max Pool Size=100),连接字符串中 Pooling=true
事务隔离级别 默认 READ COMMITTED;预算扣减操作使用 READ COMMITTED + 乐观锁(ROWVERSION)

1.2 ORM / 驱动工具

层级 方案
数据访问层 .NET Framework 4.8 + ADO.NET (System.Data.SqlClient)
微 ORM(推荐) Dapper(轻量高效,适合复杂 SQL 手写场景)
全功能 ORM(备选) Entity Framework 6.x(Code First 或 Database First)
移动端 Flutter 3.38.10,通过 HTTP API 与后端交互(不直连数据库)

1.3 命名规范

范畴 规范 示例
表名 PascalCase,单数形式 SysUser, ExpenseDetail
列名 PascalCase Id, UserName, CreateTime
主键 统一 IdBIGINT IDENTITY(1,1) Id BIGINT IDENTITY(1,1) PRIMARY KEY
外键列 {引用表名去掉Sys前缀}Id 或全名 DeptId(引用 SysDepartment.Id
约束名 PK_表名 / FK_表名_列名 / UX_表名_列名 PK_SysUser, UX_SysUser_UserName
索引名 IX_表名_用途 / UX_表名_用途 IX_Expense_List_Path
SQL 关键字 大写 SELECT, FROM, WHERE

2. 数据库全局设计约定

2.1 主键与标识

所有表主键统一使用 BIGINT IDENTITY(1,1),起始值 1,步长 1。不对外暴露主键值,API 层使用业务单号(ApplicationNo / ReportNo / VisitNo)作为资源标识。

选型理由BIGINT(64 位有符号整数,最大值 9,223,372,036,854,775,807)足以支撑企业级 OA 系统的数据规模。整型主键在 B+ 树索引中比 GUID 更紧凑(8 字节 vs 16 字节),页分裂更少,JOIN 性能更高。BIGINT 不对外暴露,避免了自增 ID 的可猜测性风险。

2.2 精度规范

  • 金额/预算字段:统一使用 DECIMAL(18,2)
  • GPS 经纬度字段:统一使用 DECIMAL(10,6)
  • 税率字段DECIMAL(5,4)(如 0.0600, 0.0900, 0.1300)
  • 比率字段DECIMAL(3,2)(如调休比例 0.30)
  • 工时字段DECIMAL(4,1)(如加班净工时 3.5h)

2.3 全局必备字段

每张表必须包含:

字段 类型 约束 说明
CreateTime DATETIME NOT NULL DEFAULT GETDATE() 行创建时间(服务器授时)
UpdateTime DATETIME NULL 最后修改时间(由应用层 UPDATE 时显式赋值为 GETDATE()

例外SysRoleChangeLog 为不可变审计日志,仅追加不修改,无需 UpdateTime

2.4 软删除机制

核心业务表与基础数据表统一挂载 IsDeleted BIT NOT NULL DEFAULT 0,用于逻辑删除。

例外

  • SysRoleChangeLog(审计日志不可删除)
  • SysProjectBudget(预算记录由 Version 乐观锁控制,不做软删除)
  • ApprovalRecord(审批记录本身不做软删除,通过 IsValid 标记有效性)

2.5 状态机规范

所有状态、类型字段统一使用 VARCHAR(20)VARCHAR(30) 存储英文标识字符串。前端负责本地化国际化语言包的映射与渲染。完整取值说明见第 6 节

2.6 单据编号生成规则

ApplicationNo / ReportNo / VisitNo 等单号统一格式为 {前缀}-{YYYYMMDD}-{序号}

业务模块 前缀 示例
事前申请 BXSQ BXSQ-20260530-001
费用报销 BX BX-20260530-001
加班申请 JB JB-20260530-001
用车申请 YC YC-20260530-001
外勤拜访日志 VST VST-20260530-001

序号按天重置、高位补零至 3 位。后端使用 SQL Server sp_getapplock + SELECT MAX(…) 原子获取当日最大序号并 +1,确保高并发下无重复。


3. 数据库关系图 (ERD — Entity Relationship Diagram)

以下为文本形式的实体关系拓扑。外键列均为 BIGINT,引用目标表 Id 列(BIGINT IDENTITY)。

                         [SysRole] 
                             │ (1:N)
                       [SysUserRole] (复合岗位矩阵桥接表)
                             ▲
                             │ (N:1)
                    [SysRoleChangeLog] (角色变更审计,关联 OperatorId + TargetUserId)
    [SysDepartment] ──── [SysUser] (全局统一核心枢纽) ◄─────────────────────────────────┐
        │ (自引用)          │                                                           │
        │ ParentId          ├─(1:N)─► [SysCostCenter] (成本中心)                       │
        │                   │                                                           │
        │ (1:N)             ├─(1:N)─► [ExpenseApplication] ──(1:N)──► [ExpenseAppDetail]│
        │                   ├─► (1:N)─► [ExpenseApplicationAttachment]                 │
        ├─► [Expense]       ├─(1:N)─► [Expense] ───────────(1:N)──► [ExpenseDetail]    │
        ├─► [Overtime]      │ (N:1)      │ (N:1)            (N:1)                     │
        ├─► [Vehicle]       │ [SysCostCenter] └─(1:N)─► [ExpenseAttachment]            │
        └─► [OutingLog]     │                                                           │
            (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] (AvailableAmount 核心动态预算强控制)           │
                 │ (N:1)                                                                 │
           [SysDepartment] (项目归属部门)                                                │
                                                                                         │
  [SysBudgetSubject] ┘ (自引用 ParentId)                                                 │

  [SysCostCategory] (费用类别字典,自引用 ParentId)                                        │

  [ApprovalChain] (BizType/DeptId 审批层级配置,关联 ApproverId/ApproverRole)              │

  [ApprovalRecord] (Action: approve/reject/transfer 三元控制,IsValid 标记历史失效链) ─────┘

  [SysBanner] (工作台轮播图,管理员维护)

注意

  • 所有主键均为 BIGINT IDENTITY(1,1)
  • 所有外键列均为 BIGINT,对应引用表的 Id 列。
  • ApprovalRecord.BizIdAnnouncementTarget.TargetId 为多态外键(BIGINT),根据相应的类型字段(BizType / TargetType)路由到不同业务表。
  • SQL Server 不支持多态外键约束,由应用层校验引用完整性。

4. 详细表结构定义 (Table Definitions)

4.1 权限与组织架构

4.1.1 SysUser(用户表)

业务含义:系统用户核心主表,存储所有员工账号、登录凭据、部门归属及默认收款银行信息。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 用户唯一标识
UserName VARCHAR(50) UNIQUE 登录账号 / 标准工号(全局唯一,不可重用。软删除用户的工号不释放,新员工不可占用)
RealName NVARCHAR(50) 员工真实姓名
PasswordHash VARCHAR(128) 密码加盐哈希值
DeptId BIGINT 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 GETDATE() 创建时间
UpdateTime DATETIME 最后修改时间
IsDeleted BIT DEFAULT 0 软删除标记

索引

索引 类型 说明
IX_User_Search NONCLUSTERED (UserName, RealName) INCLUDE (DeptId, IsActive, IsDeleted) 用户登录/搜索加速

4.1.2 SysRole(系统角色表)

业务含义:标准 RBAC 角色字典,存储系统内所有可用角色编码及中文名称。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 角色唯一标识
RoleCode VARCHAR(30) UNIQUE 角色编码
RoleName NVARCHAR(50) 角色中文名称
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

预置角色取值

RoleCode RoleName 说明
employee 员工 基础用户,可发起各类申请
approver 经理 部门审批人,可在待办列表审批/拒绝/转交单据
finance 财务 财务核销专员,执行三字合规查验与付款标记
admin 系统管理员 最高权限,管理用户、角色、组织架构、审批链、基础数据

4.1.3 SysUserRole(用户角色多对多映射表)

业务含义:实现一个用户拥有多个角色的复合授权能力,支持管理员通过复选框矩阵(TDCheckboxGroup)为员工自由分配角色。

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

索引

索引 类型 说明
UX_UserRole_Active UNIQUE NONCLUSTERED (UserId, RoleId) WHERE IsDeleted = 0 防止同一角色重复授权,同时允许删除后重新授权
IX_UserRole_Role_Reverse NONCLUSTERED (RoleId, IsDeleted) INCLUDE (UserId) 按角色反查用户——权限管理页 + 审批链动态寻人

4.1.4 SysRoleChangeLog(角色变更审计日志表)

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

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

注意:本表无 UpdateTimeIsDeleted 字段——审计日志不可修改、不可删除。

索引

索引 类型 说明
IX_RoleChangeLog_TargetUser NONCLUSTERED (TargetUserId, CreateTime DESC) INCLUDE (OperatorId, ChangeType, BeforeSnapshot, AfterSnapshot) 按目标用户查询角色变更审计历史

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

业务含义:企业部门树形组织架构,支持自引用父子关系(ParentId),用于生成无限层级部门树。

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

索引

索引 类型 说明
IX_Department_Tree NONCLUSTERED (ParentId, SortOrder) INCLUDE (DeptName, ManagerId, IsActive) WHERE IsDeleted = 0 按上级部门加载子节点,加速部门树渲染

4.1.6 SysBanner(工作台轮播图配置表)

业务含义:支持工作台顶部 TDRotation 轮播图组件的动态配置,管理员可维护图片、标题、跳转链接和排序。

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

索引

索引 类型 说明
IX_Banner_Active NONCLUSTERED (IsActive, SortOrder) INCLUDE (ImageUrl, Title, LinkUrl) WHERE IsDeleted = 0 按排序加载启用轮播项

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

4.2.1 SysProject(企业项目表)

业务含义:项目主数据。支持"项目 + 预算科目"级联选择器实时加载当前可用预算余额,超支时触发高管特批流。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 项目唯一标识
ProjectName NVARCHAR(200) 项目完整名称
ProjectCode VARCHAR(50) UNIQUE 项目标准控制编码(如:PROJ-2026-001)
DeptId BIGINT 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 GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

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

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


4.2.2 SysBudgetSubject(预算科目字典表)

业务含义:财务核算科目树形字典(如差旅费、业务招待费、日常采购)。支持自引用二级分类,叶子节点才可绑定预算金额。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 科目唯一标识
SubjectName NVARCHAR(100) 科目中文名称
SubjectCode VARCHAR(50) UNIQUE 科目代码(如:EXP-5001)
ParentId BIGINT FK → SysBudgetSubject.Id(自引用) 上级科目 ID,叶子节点才可绑定金额
SortOrder INT DEFAULT 0 字典排序权重
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

4.2.3 SysCostCategory(费用类别字典表)

业务含义:为事前申请预估明细和费用报销明细的"费用类别"下拉选择器提供数据源(交通费、住宿费、餐饮费、办公用品等),支持二级分类,叶子节点才可用于明细录入。通过 BizScopeExpenseType 实现事前申请端与报销端的子类差异化过滤。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 类别唯一标识
CategoryName NVARCHAR(50) 类别名称
CategoryCode VARCHAR(30) UNIQUE 类别编码
ParentId BIGINT FK → SysCostCategory.Id(自引用) 上级类别 ID,叶子节点才可用于明细录入
BizScope VARCHAR(20) DEFAULT 'both' 适用业务范围
ExpenseType VARCHAR(20) 绑定费用大类(仅 BizScope 含 expense_apply 时生效,NULL 表示通用)
SortOrder INT DEFAULT 0 排序权重
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

索引

索引 类型 说明
IX_CostCategory_Parent NONCLUSTERED (ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 级联下拉子类别加载
IX_CostCategory_Scope_Type NONCLUSTERED (BizScope, ExpenseType, ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 AND IsDeleted = 0 按业务范围 + 费用大类过滤——事前申请/报销端按类型联动子类下拉

4.2.4 SysProjectBudget(项目-科目联合预算动态控制表)

业务含义:高并发预算强管控的核心热点表。前端在录入明细金额时,后台通过此表的数据进行实时差额逻辑校验。支持预算冻结/占用/释放的完整生命周期。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 联合预算唯一标识
ProjectId BIGINT FK → SysProject.Id 关联项目
SubjectId BIGINT FK → SysBudgetSubject.Id 关联预算科目
ExpenseType VARCHAR(20) 费用类型维度(NULL 表示适用于所有费用类型)
AllocatedAmount DECIMAL(18,2) 初始分配总额度
AvailableAmount DECIMAL(18,2) 实时动态可用余额(已扣减审批通过的金额)
FrozenAmount DECIMAL(18,2) DEFAULT 0 冻结金额(已提交但未审批通过的申请金额)
Version ROWVERSION 乐观锁并发控制:每次扣减余额时 WHERE Version = @oldVersion,防止超扣
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 变更校准时间

实际可用余额公式AvailableAmount - FrozenAmount = 员工发起新申请时可见的余额。前端展示时使用此公式计算。

注意:本表无 IsDeleted 字段,预算记录由 Version 乐观锁控制。

索引

索引 类型 说明
UX_ProjectBudget_Concurrency_Control UNIQUE NONCLUSTERED (ProjectId, SubjectId, ExpenseType) INCLUDE (AvailableAmount, FrozenAmount, AllocatedAmount) 高并发预算余额校验唯一索引
UX_ProjectBudget_NULL_Type UNIQUE NONCLUSTERED (ProjectId, SubjectId) WHERE ExpenseType IS NULL 防止同一项目+科目下多条通用预算记录

4.2.5 SysCostCenter(企业成本中心表)

业务含义:成本中心主数据,报销单关联成本中心用于财务核算归集。

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

4.2.6 SysBank(银行字典表)

业务含义:为费用报销表单"开户行全称"下拉联想输入提供数据源,管理员可维护银行列表。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 银行唯一标识
BankName NVARCHAR(100) 银行全称
BankCode VARCHAR(20) UNIQUE 银行联行号
SortOrder INT DEFAULT 0 排序权重
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记

索引

索引 类型 说明
IX_Bank_List NONCLUSTERED (SortOrder, IsActive) INCLUDE (BankName, BankCode) 下拉列表按排序加载

4.3 事前申请与费用报销模块

4.3.1 ExpenseApplication(事前申请单主表)

业务含义:费用发生前的事前申请,支持草稿暂存、提交审批、关联项目与预算科目。审批通过后可被多张报销单分批引用,追踪剩余可报金额。按 ExpenseType 动态加载专用字段(差旅行程、招待合规、会议信息),数据库层全部允许 NULL,约束在应用层按类型校验。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 事前单据唯一标识
ApplicationNo VARCHAR(30) UNIQUE 单号序列(格式:BXSQ-YYYYMMDD-XXX)
ApplicantId BIGINT FK → SysUser.Id 发起员工
DeptId BIGINT FK → SysDepartment.Id 提单人当时所属部门
ApplicationDate DATE 申请日期(草稿时可空或默认当天,提交时写入提交当日)。与 CreateTime 分离——CreateTime 为服务器物理写入时间,ApplicationDate 为业务申请日期
ProjectId BIGINT FK → SysProject.Id 关联控制项目(草稿时可空,提交时应用层校验非空)
BudgetSubjectId BIGINT FK → SysBudgetSubject.Id 关联主控科目(草稿时可空,提交时应用层校验非空)
EstimatedAmount DECIMAL(18,2) DEFAULT 0 预估申请总金额(明细行 Quantity × UnitPrice 汇总)
Urgency VARCHAR(10) DEFAULT 'normal' 紧急程度
ExpenseType VARCHAR(20) 费用类型(7 种枚举值,决定专用字段校验规则和子类下拉选项)
Purpose NVARCHAR(500) 费用事由详细文本(限制 200 字)
Status VARCHAR(20) DEFAULT 'draft' 审批状态机
CurrentApproverId BIGINT FK → SysUser.Id 当前节点审批人。提交时写入首级审批人,每次审批动作完成后更新为下一级(或 NULL 表示流程结束)
UsageStatus VARCHAR(20) DEFAULT 'unused' 使用状态(unused / partially_used / fully_used)。由报销单引用/撤回/删除时自动重算更新
IsTaxIncluded BIT DEFAULT 0 预估金额是否含税(0=不含税,1=含税)。影响后续报销核销时的金额对比
ValidUntil DATE 申请有效期截止日。超过此日期未使用的申请预算不再可用,由定时 Job 自动释放冻结额度
ReferenceNo VARCHAR(50) 关联合同号/询价单号/采购单号(采购类申请使用)
EstimatedStartDate DATE 预计开始日期(travel/meeting 提交必填)
EstimatedEndDate DATE 预计结束日期(travel/meeting 提交必填)
IsOvernight BIT 是否过夜(travel 提交必填,1=需住宿,0=当天来回)
TransportType VARCHAR(20) 交通工具(travel 提交必填)
EntertainmentTarget NVARCHAR(200) 招待对象单位名称(entertainment 提交必填,税务合规底线)
EntertainmentLevel VARCHAR(20) 招待对象层级(entertainment 提交必填)
GuestCount INT 外部招待人数(entertainment 提交必填)
CompanionCount INT 内部陪同人数(entertainment 提交必填,应用层校验 ≤ GuestCount)
Venue NVARCHAR(200) 招待/会议地点(entertainment/meeting 提交必填)
CreateTime DATETIME DEFAULT GETDATE() 发起/存草稿时间
UpdateTime DATETIME 状态变动时间
IsDeleted BIT DEFAULT 0 逻辑删除标记

字段分层说明

  • 通用字段(前 17 个,Id ~ ReferenceNo):所有类型生效,草稿可空、提交按应用层规则校验
  • 差旅专用字段EstimatedStartDate / EstimatedEndDate / IsOvernight / TransportType):仅 ExpenseType='travel' 时前端展示。住宿天数由前端根据 IsOvernight=1EstimatedEndDate - EstimatedStartDate 自动计算
  • 招待专用字段EntertainmentTarget / EntertainmentLevel / GuestCount / CompanionCount / Venue):仅 ExpenseType='entertainment' 时前端展示。人均金额由前端实时计算 EstimatedAmount ÷ (GuestCount + CompanionCount)
  • 会议字段:起止日期和地点复用 EstimatedStartDate / EstimatedEndDate / Venue,会议名称含在 Purpose 中。人数在明细行中按 Quantity 体现(如"餐饮 × 30 人 × ¥80")
  • 以上专用字段数据库层全部 NULL 允许,约束完全在应用层按 ExpenseType 动态校验

索引

索引 类型 说明
IX_ExpenseApp_List_Path NONCLUSTERED (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, ApplicationDate, EstimatedAmount, Purpose, ExpenseType, UsageStatus, ValidUntil) 五状态 Chip 切换 + 时间倒序
IX_ExpenseApp_Import_Drawer NONCLUSTERED (ApplicantId, Status, UsageStatus) WHERE Status = 'approved' AND UsageStatus IN ('unused', 'partially_used') 一键导入时快速过滤已通过且尚有剩余额度的单据

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

业务含义:事前申请的费用预估明细行,支持多行录入,级联于主表。Quantity × UnitPrice 汇总至主表 EstimatedAmount

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 明细项唯一标识
ApplicationId BIGINT FK → ExpenseApplication.Id(级联删除) 关联主表
ExpenseCategory VARCHAR(20) 费用细分类别(值来源于 SysCostCategory.CategoryCode,前端按 ExpenseType 过滤)
Quantity INT DEFAULT 1 数量
UnitPrice DECIMAL(18,2) 单价
Unit VARCHAR(10) 单位(张/间/人/天/套/个)
EstimatedAmount DECIMAL(18,2) 单项预估金额(前端自动计算 = Quantity × UnitPrice,只读展示)
Remark NVARCHAR(200) 单项细目描述说明
SortOrder INT DEFAULT 1 移动端渲染序列权重
CreateTime DATETIME DEFAULT GETDATE() 明细添加时间
UpdateTime DATETIME 明细修改时间

索引

索引 类型 说明
IX_ExpenseAppDetail_AppId NONCLUSTERED (ApplicationId, SortOrder) INCLUDE (ExpenseCategory, EstimatedAmount, Remark) 子表 FK 查询

4.3.3 Expense(费用报销单主表)

业务含义:费用报销核心主表,支持从事前申请一键导入、手工录入明细、发票合规三字自查、银行收款信息录入及财务付款归档。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 报销主表唯一标识
ReportNo VARCHAR(30) UNIQUE 报销单号(格式:BX-YYYYMMDD-XXX)
SourceApplicationId BIGINT FK → ExpenseApplication.Id 关联导入的事前申请 ID(一键导入复制数据回填)
ApplicantId BIGINT FK → SysUser.Id 报销发起人
DeptId BIGINT FK → SysDepartment.Id 报销部门
ApplicationDate DATE 报销申请日期(草稿时可空或默认当天,提交时写入提交当日)。与 CreateTime 分离——CreateTime 为服务器物理写入时间,ApplicationDate 为业务日期
CostCenterId BIGINT FK → SysCostCenter.Id 成本中心
ProjectId BIGINT FK → SysProject.Id 关联项目(草稿时可空,提交时应用层校验非空)
BudgetSubjectId BIGINT FK → SysBudgetSubject.Id 关联预算科目(草稿时可空,提交时应用层校验非空)
TotalAmount DECIMAL(18,2) DEFAULT 0 实际报销总金额(系统自动计算累加明细)
Purpose NVARCHAR(500) 报销事由
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' 付款状态机
Status VARCHAR(20) DEFAULT 'draft' 审批状态机
CurrentApproverId BIGINT FK → SysUser.Id 当前节点审批人
CreateTime DATETIME DEFAULT GETDATE() 报销单创建时间
UpdateTime DATETIME 财务核销归档付款时间
IsDeleted BIT DEFAULT 0 逻辑删除标记

索引

索引 类型 说明
IX_Expense_List_Path NONCLUSTERED (ApplicantId, Status, CreateTime DESC) INCLUDE (TotalAmount, ReportNo, ApplicationDate, PaymentStatus) 五状态 Chip 切换 + 时间倒序
IX_Expense_SourceApp NONCLUSTERED (SourceApplicationId) WHERE SourceApplicationId IS NOT NULL 一键导入时排除已被引用的申请

4.3.4 ExpenseDetail(费用报销明细子表)

业务含义:费用报销的逐行明细,每行对应一张发票或一笔无发票费用。支持发票号码、代码、类型、税率录入。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 明细唯一标识
ExpenseId BIGINT FK → Expense.Id(级联删除) 关联主表
ExpenseDate DATE 费用发生具体日期
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) 发票类型
TaxRate DECIMAL(5,4) 税率(如:0.0600, 0.0900, 0.1300)
SortOrder INT DEFAULT 1 明细行排序号
CreateTime DATETIME DEFAULT GETDATE() 明细录入时间
UpdateTime DATETIME 明细修改时间

索引

索引 类型 说明
IX_ExpenseDetail_ExpenseId NONCLUSTERED (ExpenseId, SortOrder) INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType) 子表 FK 查询

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

业务含义:报销单关联的发票影像及附件文件,支持绑定到具体明细行或整个报销单。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 附件唯一标识
ExpenseId BIGINT FK → Expense.Id 关联报销主表
DetailId BIGINT FK → ExpenseDetail.Id 可空。绑定具体明细行的发票凭证
FileName NVARCHAR(200) 原始文件名称
FileUrl VARCHAR(500) 云端对象存储绝对 URL
FileSize BIGINT 文件体积(字节)
FileType VARCHAR(20) 格式划分
CreateTime DATETIME DEFAULT GETDATE() 上传时间
UpdateTime DATETIME 附件更换/删除时间

索引

索引 类型 说明
IX_ExpenseAttachment_ExpenseId NONCLUSTERED (ExpenseId) INCLUDE (FileName, FileUrl, FileType, FileSize) 子表 FK 查询

4.3.6 ExpenseApplicationAttachment(事前申请支撑材料附件表)

业务含义:事前申请的业务合理性支撑材料(报价单、合同、出差审批截图等),与 ExpenseAttachment 职责分离。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 附件唯一标识
ApplicationId BIGINT FK → ExpenseApplication.Id(级联删除) 关联事前申请
FileName NVARCHAR(200) 原始文件名称
FileUrl VARCHAR(500) 云端对象存储绝对 URL
FileType VARCHAR(20) 格式划分
FileSize BIGINT 文件体积(字节)
CreateTime DATETIME DEFAULT GETDATE() 上传时间
UpdateTime DATETIME 替换/删除时间

索引

索引 类型 说明
IX_ExpenseAppAttachment_AppId NONCLUSTERED (ApplicationId) INCLUDE (FileName, FileUrl, FileType, FileSize) 子表 FK 查询

4.4 勤务考勤、公车调度与外勤日志模块

4.4.1 Overtime(加班申请单业务表)

业务含义:员工加班申请,支持工作日/休息日/节假日三种加班类型,以及加班费结算/调休/混合三种补偿形式。后端自动扣除午休等盲区时段计算净工时。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 加班唯一标识
ApplicationNo VARCHAR(30) UNIQUE 单号(格式:JB-YYYYMMDD-XXX)
ApplicantId BIGINT FK → SysUser.Id 加班员工
DeptId BIGINT FK → SysDepartment.Id 关联部门
OtType VARCHAR(10) 加班类型
CompensationType VARCHAR(20) 补偿形式
CompLeaveRatio DECIMAL(3,2) 混合模式下调休比例(0.00~1.00,仅 CompensationType='mixed' 时生效)
StartTime DATETIME 加班启动时间(草稿时可空,提交时应用层校验非空)
EndTime DATETIME 加班截止时间(草稿时可空,提交时应用层校验非空)
NetOtHours DECIMAL(4,1) DEFAULT 0 扣除盲区后的净工时(草稿未计算时为 0)
Reason NVARCHAR(500) 加班主因详细说明
Status VARCHAR(20) DEFAULT 'draft' 审批状态机
CurrentApproverId BIGINT FK → SysUser.Id 当前节点审批人
CreateTime DATETIME DEFAULT GETDATE() 提单创建时间
UpdateTime DATETIME 状态/审批变更时间
IsDeleted BIT DEFAULT 0 逻辑删除标记

约束

  • CHECK (StartTime IS NULL OR EndTime IS NULL OR StartTime < EndTime):开始时间必须早于结束时间
  • CHECK (CompLeaveRatio IS NULL OR (CompLeaveRatio > 0 AND CompLeaveRatio < 1)):调休比例在 0~1 之间(不含边界)

索引

索引 类型 说明
IX_Overtime_List_Path NONCLUSTERED (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType) 五状态 Chip 切换 + 时间倒序

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

业务含义:覆盖从公车申请、审批、出车到还车登记核销的完整生命周期。支持排期冲突检测、里程表记录和实际费用报账。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 用车单据唯一标识
ApplicationNo VARCHAR(30) UNIQUE 单号(格式:YC-YYYYMMDD-XXX)
ApplicantId BIGINT FK → SysUser.Id 用车申请人
DeptId BIGINT FK → SysDepartment.Id 关联部门
VehicleId BIGINT FK → SysVehicle.Id 申请调配的公车(草稿时可空,提交时校验非空)
Purpose VARCHAR(20) 用车目的类别
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 DEFAULT 1 同行总人数
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' 复合状态机(含 returned 已还车归档)
CurrentApproverId BIGINT FK → SysUser.Id 当前节点审批人
CreateTime DATETIME DEFAULT GETDATE() 提单时间
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):还车时里程表必须递增

索引

索引 类型 说明
IX_Vehicle_List_Path NONCLUSTERED (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, VehicleId, Purpose, Origin, Destination, StartTime) 状态 Chip 切换 + 时间倒序
IX_Vehicle_Schedule_Collision_Defense NONCLUSTERED (VehicleId, StartTime, EndTime) WHERE Status IN ('pending', 'approved') 防止同一辆车时段重叠的排期冲突检测

4.4.3 VehiclePassenger(随行同行人员映射表)

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

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 乘客关联标识
ApplicationId BIGINT FK → Vehicle.Id 关联用车单主表
UserId BIGINT FK → SysUser.Id 可空。内部同事绑定 SysUser.Id
PassengerName NVARCHAR(50) 同行人员姓名文本
CreateTime DATETIME DEFAULT GETDATE() 添加时间
UpdateTime DATETIME 变更时间
IsDeleted BIT DEFAULT 0 软删除标记(移除同行人时标记)

索引

索引 类型 说明
IX_VehiclePassenger_AppId NONCLUSTERED (ApplicationId, IsDeleted) INCLUDE (UserId, PassengerName) 子表 FK 查询

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

业务含义:业务员外勤拜访日志。核心专为防止作弊设计——经纬度与逆地理编码地址在前端设为只读,强制匹配现场防伪相机水印。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 日志唯一标识
VisitNo VARCHAR(30) UNIQUE 单号(格式:VST-YYYYMMDD-XXX)
SalespersonId BIGINT FK → SysUser.Id 业务员
DeptId BIGINT FK → SysDepartment.Id 关联部门
CustomerId BIGINT FK → SysCustomer.Id 联动企业核心客户池(草稿时可空,提交时校验非空)。输入不存在的客户名称时,提交时后端在同一事务内 INSERT 新客户后回填此字段
CustomerName NVARCHAR(200) 客户公司标准全称冗余字段
ContactId BIGINT FK → SysCustomerContact.Id 关联客户联系人
CheckInLongitude DECIMAL(10,6) GPS 硬件定位经度(草稿时可空,提交时校验非空)
CheckInLatitude DECIMAL(10,6) GPS 硬件定位纬度(草稿时可空,提交时校验非空)
CheckInAddress NVARCHAR(500) 逆地理编码出的具体街道地址(禁止篡改,草稿时可空,提交时校验非空)
VisitSummary NVARCHAR(2000) 外勤核心工作总结正文
NextPlan NVARCHAR(500) 后续推进计划
Status VARCHAR(20) DEFAULT 'draft' 状态(draft 暂存草稿 / completed 已提交)
CreateTime DATETIME DEFAULT GETDATE() 服务器防伪授时创建时间
UpdateTime DATETIME 日志修改/点评更新时间
LastViewedTime DATETIME 员工最后一次查看详情页的时间(用于"新点评"红点判断)
IsDeleted BIT DEFAULT 0 逻辑删除标记

索引

索引 类型 说明
IX_OutingLog_Sales_Timeline NONCLUSTERED (SalespersonId, CreateTime DESC) INCLUDE (CustomerId, CustomerName, CheckInAddress) 按业务员时间倒序滚动
IX_OutingLog_Dept_Timeline NONCLUSTERED (DeptId, CreateTime DESC) INCLUDE (SalespersonId, CustomerId, CustomerName, CheckInAddress, Status) 经理端按部门查看下属外勤日志

4.4.5 OutingLogComment(主管考评打分与互动多轮点评历史表)

业务含义:支持经理在详情页底部进行星级评分(TDRate),并以多气泡流水样式进行指导性文字互动。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 点评项唯一标识
LogId BIGINT FK → OutingLog.Id(级联删除) 关联外勤拜访日志
CommenterId BIGINT FK → SysUser.Id 发表点评的经理或协同人
RatingStars INT 主管点选的考评星级(1-5星,仅限主管首条考评记录生效)
CommentText NVARCHAR(1000) 批示及指导意见具体文字内容
CreateTime DATETIME DEFAULT GETDATE() 回复点评发布时间
UpdateTime DATETIME 点评修改时间
IsDeleted BIT DEFAULT 0 软删除标记(经理撤回点评时标记)

约束CHECK (RatingStars IS NULL OR (RatingStars >= 1 AND RatingStars <= 5))

索引

索引 类型 说明
IX_OutingLogComment_LogId NONCLUSTERED (LogId, IsDeleted) INCLUDE (CommenterId, RatingStars, CommentText, CreateTime) 子表 FK 查询——外勤日志点评列表

4.4.6 OutingLogAttachment(外勤日志防伪照片及附件表)

业务含义:现场强制拍照上传及照片墙展示。照片由原生相机拍摄后本地渲染防伪水印(服务器授时 + GPS 经纬度),上传至云端存储。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 附件唯一标识
LogId BIGINT FK → OutingLog.Id(级联删除) 关联外勤日志
FileName NVARCHAR(200) 原始文件名/水印照片描述
FileUrl VARCHAR(500) 云端对象存储绝对 URL
FileType VARCHAR(20) 用途分类
FileSize BIGINT 文件体积(字节)
SortOrder INT DEFAULT 0 照片墙排序
CreateTime DATETIME DEFAULT GETDATE() 上传时间
UpdateTime DATETIME 替换/删除时间

索引

索引 类型 说明
IX_OutingLogAttachment_LogId NONCLUSTERED (LogId, SortOrder) INCLUDE (FileName, FileUrl, FileType) 子表 FK 查询——外勤日志照片墙

4.4.7 SysVehicle(企业车辆资产表)

业务含义:公车资产台账,用于公车排期冲突检测及车辆状态管理。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 车辆唯一标识
LicensePlate NVARCHAR(20) UNIQUE 车牌号(含中文字符,如:粤B12345)
VehicleType VARCHAR(20) 车辆类型
Brand NVARCHAR(50) 品牌型号
Seats INT 核定座位数
DriverName NVARCHAR(50) 默认驾驶员姓名
Status VARCHAR(20) DEFAULT 'idle' 车辆状态
IsActive BIT DEFAULT 1 启用/停用
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记(车辆报废/出售时标记)

4.4.8 SysCustomer(客户基础资料表)

业务含义:企业客户池主数据,为外勤日志模块提供客户名称联想匹配。

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

索引

索引 类型 说明
IX_Customer_Name_Search NONCLUSTERED (IsActive, CustomerName) INCLUDE (ShortName, SalespersonId) 客户名称模糊搜索——外勤日志选客户时的即时联想
IX_Customer_Salesperson NONCLUSTERED (SalespersonId, IsActive) INCLUDE (CustomerName, ShortName) 按业务员筛选客户池

4.4.9 SysCustomerContact(客户联系人表)

业务含义:客户下的联系人明细,支持一个客户多个联系人。

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

4.5 行政通告、已读审计与 DING 催办模块

4.5.1 Announcement(行政通告公告主表)

业务含义:行政公告/红头文件的发布、置顶、定时下架管理。支持按全员/部门/指定用户三种可见范围发布。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 公告唯一标识
Title NVARCHAR(200) 行政公告标题
Content NVARCHAR(MAX) 公告正文(支持 HTML / Markdown)
Type VARCHAR(20) 分类
Status VARCHAR(20) DEFAULT 'draft' 发布状态(draft 仅创建者和管理员可见 / published 已发布)
PublisherId BIGINT FK → SysUser.Id 发布管理员
CreateTime DATETIME DEFAULT GETDATE() 首次创建时间(含草稿)
PublishTime DATETIME 设定发布生效时间(草稿时为 NULL)
IsTop BIT DEFAULT 0 是否全局置顶(1=置顶,0=普通)
PrivateLevel INT DEFAULT 0 可见域范围级别(0=全员,1=按部门,2=按指定用户)
ExpiryDate DATETIME 自动下架失效截止时间(不填则永不过期)
UpdateTime DATETIME 最后修改/下架时间
IsDeleted BIT DEFAULT 0 逻辑删除标记

索引

索引 类型 说明
IX_Announcement_List NONCLUSTERED (Type, IsTop DESC, PublishTime DESC) INCLUDE (Title, PublisherId, Status, ExpiryDate) WHERE IsDeleted = 0 AND Status = 'published' 公告列表——分类筛选 + 置顶优先 + 时间倒序

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

业务含义:当公告按部门或按指定用户发布时,记录目标范围。多态外键设计——SQL Server 不支持多态 FK 约束,由应用层校验引用完整性。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 范围标识
AnnouncementId BIGINT FK → Announcement.Id 关联公告主表
TargetType VARCHAR(10) 范围实体类别划分
TargetId BIGINT 多态外键:根据 TargetType 指向 SysDepartment.Id 或 SysUser.Id
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间

4.5.3 AnnouncementReadLog(全员行政触达率审计与一键 DING 记录表)

业务含义:记录每个员工对每条公告的已读/未读状态,以及管理员"一键 DING 强力催办"的操作历史。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 审计行唯一标识
AnnouncementId BIGINT FK → Announcement.Id 关联公告
UserId BIGINT FK → SysUser.Id 被下发触达的员工
IsRead BIT DEFAULT 0 已读标记(停留 2 秒后异步置换为 1)
ReadTime DATETIME 员工实际查阅阅读时间
IsUrged BIT DEFAULT 0 是否被管理员一键 DING 催办过
LastUrgeTime DATETIME 最后一次触发高优先级 Push 或短信的时间
CreateTime DATETIME DEFAULT GETDATE() 触达记录创建时间
UpdateTime DATETIME 已读/催办更新时间

约束UNIQUE (AnnouncementId, UserId),每个员工对每条公告仅有一条触达记录。

索引

索引 类型 说明
IX_Announcement_Audit_Ding NONCLUSTERED (AnnouncementId, IsRead, IsUrged) INCLUDE (UserId, ReadTime) 管理员一键 DING 时瞬间抓取未读员工列表

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

业务含义:公告关联的附件文件(红头文件 PDF、图片等)。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 附件标识
AnnouncementId BIGINT FK → Announcement.Id 关联公告
FileName NVARCHAR(200) 附件名称
FileUrl VARCHAR(500) 绝对存储 URL 下载地址
FileType VARCHAR(20) 格式划分
FileSize BIGINT 文件体积(字节)
CreateTime DATETIME DEFAULT GETDATE() 上传时间
UpdateTime DATETIME 替换/删除时间

索引

索引 类型 说明
IX_AnnouncementAttachment_AnnId NONCLUSTERED (AnnouncementId) INCLUDE (FileName, FileUrl, FileType, FileSize) 子表 FK 查询

4.5.5 Message(消息通知流水与动态路由寻址表)

业务含义:框架外壳左侧首 Tab 的消息通知中心。支持待办提醒、审批结果通知、公告推送三种消息类型,配合前端 GoRouter 实现点击穿透跳转。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 消息行唯一标识
UserId BIGINT FK → SysUser.Id 接收消息通知的员工
SenderId BIGINT FK → SysUser.Id 消息发送人(消息创建时固化,不随源数据变更)
Title NVARCHAR(200) 消息通知卡片大标题
Content NVARCHAR(500) 卡片摘要文本
MsgType VARCHAR(20) 消息分类
BizType VARCHAR(30) 精准寻址路由标记(配合前端 GoRouter)
BizId BIGINT 关联单据的具体主键 ID(点击穿透跳转详情页)
IsRead BIT DEFAULT 0 未读已读状态(决定 Tab 上 TDBadge 红点)
ReadTime DATETIME 点击标记阅读时间
CreateTime DATETIME DEFAULT GETDATE() 消息推送产生时间
UpdateTime DATETIME 已读/删除时间
IsDeleted BIT DEFAULT 0 软删除标记(左滑删除时标记)

多态外键说明BizIdBizType 组成多态关联,根据 BizType 的值指向不同的业务主表。约束策略与 ApprovalRecord 一致——业务表一律软删除,查询时通过 BizType 路由 + IsDeleted 过滤。

索引

索引 类型 说明
IX_Message_List_Active NONCLUSTERED (UserId, IsDeleted, IsRead, CreateTime DESC) INCLUDE (Title, MsgType, BizType, BizId, SenderId) 消息列表查询——排除已软删除的消息

4.6 工作流引擎与审批记录

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

业务含义:多级审批链的配置中心。支持按部门/全局配置、固定审批人/动态角色审批、以及基于条件(超预算、金额门槛)的动态审批层级插入。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 配置项唯一标识
BizType VARCHAR(30) 业务单据类型标识
DeptId BIGINT FK → SysDepartment.Id 可空。绑定特定部门,NULL 代表全局通用流
LevelNo INT 审批层级序号(从 1 开始)
ApproverId BIGINT FK → SysUser.Id 固定指定特定审批人
ApproverRole VARCHAR(20) 按岗位角色动态寻人
ConditionType VARCHAR(20) 条件触发类型(budget_exceed 超预算 / amount_threshold 金额门槛 / NULL 无条件)
ConditionThreshold DECIMAL(18,2) 条件阈值
ConditionConfig NVARCHAR(500) 条件扩展配置 JSON
IsActive BIT DEFAULT 1 流程是否生效
CreateTime DATETIME DEFAULT GETDATE() 创建时间
UpdateTime DATETIME 修改时间
IsDeleted BIT DEFAULT 0 软删除标记(审批链废弃时标记)

约束

  • CHECK (ApproverId IS NOT NULL OR ApproverRole IS NOT NULL)ApproverIdApproverRole 至少填写一项
  • UNIQUE (BizType, DeptId, LevelNo) 结合 IsDeleted = 0 过滤

索引

索引 类型 说明
UX_ApprovalChain_Global UNIQUE NONCLUSTERED (BizType, LevelNo) WHERE DeptId IS NULL AND IsDeleted = 0 全局配置唯一约束
UX_ApprovalChain_Dept UNIQUE NONCLUSTERED (BizType, DeptId, LevelNo) WHERE DeptId IS NOT NULL AND IsDeleted = 0 部门级配置唯一约束

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

业务含义:每条审批动作(同意/拒绝/转交)的完整历史记录。支持审批链失效与重建(驳回后重新编辑再次发起)。

字段名 类型 必填 约束 说明
Id BIGINT PK, IDENTITY(1,1) 审批记录节点唯一标识
BizId BIGINT 关联的具体业务单据主键 ID(多态外键,与 BizType 联合路由)
BizType VARCHAR(30) 业务单据类型映射标志
ApproverId BIGINT FK → SysUser.Id 执行审批动作的领导/财务人员
ApprovalLevel INT 当前所处审批绝对层级(1, 2, 3...)
Action VARCHAR(20) 三元决策动作代码
TransferToUserId BIGINT FK → SysUser.Id 转交接管的新审批人(仅 Action='transfer' 时有值)
Opinion NVARCHAR(500) 审批意见备注(Action='reject' 时强制 ≥5 个汉字)
IsValid BIT DEFAULT 1 有效标记(1=有效,0=已失效)。重新发起时旧记录置为 0
CreateTime DATETIME DEFAULT GETDATE() 单据到达该层级节点的时间
ApprovalTime DATETIME 领导实际点击提交的执行时间
UpdateTime DATETIME 审批记录变更时间

多态外键说明BizIdBizType 组成多态关联——根据 BizType 的值指向不同的业务主表(expense_applyExpenseApplication.IdexpenseExpense.IdovertimeOvertime.IdvehicleVehicle.Id)。SQL Server 不支持多态外键约束,无法在数据库层保证引用完整性。

补偿措施

  1. 业务表删除单据时执行软删除IsDeleted=1),不物理删除行,确保审批记录的 BizId 始终有效
  2. 后端查询时始终通过 BizType 路由到正确的表,JOIN 时追加 AND target.IsDeleted = 0
  3. 定时 Job 扫描 ApprovalRecordBizId 在对应业务表中不存在的记录,标记为异常并告警

索引

索引 类型 说明
IX_ApprovalRecord_Approver_Todo NONCLUSTERED (ApproverId, Action, BizType, IsValid) INCLUDE (BizId, ApprovalLevel, CreateTime) WHERE IsValid = 1 经理端待办列表"待我审批的单据"
IX_ApprovalRecord_Biz_Valid NONCLUSTERED (BizId, BizType, IsValid, ApprovalLevel) INCLUDE (ApproverId, Action, Opinion, ApprovalTime) 前端时间线组件——按 BizId+BizType 过滤有效审批链

5. 表关系与业务逻辑与约束 (Business Logic & Constraints)

5.1 级联删除策略

主表 子表 删除策略 说明
ExpenseApplication ExpenseAppDetail 级联删除 删除事前申请时同时删除其明细行
ExpenseApplication ExpenseApplicationAttachment 级联删除 删除事前申请时同时删除其附件
Expense ExpenseDetail 级联删除 删除报销单时同时删除其明细行
OutingLog OutingLogComment 级联删除 删除外勤日志时同时删除其全部点评
OutingLog OutingLogAttachment 级联删除 删除外勤日志时同时删除其附件

重要:所有业务主表统一使用软删除IsDeleted = 1)。以上"级联删除"指在应用层执行主表软删除时,同步对子表执行软删除。绝不物理删除业务数据行,以确保审批记录(ApprovalRecord)中的多态外键始终有效。

5.2 软删除统一规范

  • 适用表:除 SysRoleChangeLog(审计日志不可删除)、SysProjectBudget(预算记录不可删除)、ApprovalRecord(通过 IsValid 控制有效性)外的所有业务表与基础数据表
  • AI 强制规则:编写所有 SELECT / JOIN 查询语句时,务必在 WHERE 子句中添加 table.IsDeleted = 0(如果是软删除适用表)
  • 删除操作实现:永远使用 UPDATE table SET IsDeleted = 1, UpdateTime = GETDATE() WHERE Id = @id,严禁使用 DELETE FROM

5.3 事务边界

5.3.1 事前申请提交事务

提交申请必须在同一数据库事务内原子完成以下操作:

  1. 更新业务主表 Status = 'pending'CurrentApproverIdApplicationDate = GETDATE()(若提交时为空则写入当日日期)
  2. 冻结预算:SysProjectBudget.FrozenAmount += 申请金额(携带 Version 乐观锁)
  3. 写入首级 ApprovalRecordAction = 'pending'
  4. 发送 Message 通知至首级审批人(MsgType = 'approval_notice'

注意:以上适用于 ExpenseApplicationExpense 两张表——两表均有 ApplicationDate 字段,提交逻辑一致。

5.3.2 审批动作事务(同意/拒绝/转交)

每次审批动作必须在同一数据库事务内原子完成以下操作:

  1. 更新业务主表 StatusCurrentApproverId
  2. 写入/更新 ApprovalRecordAction + Opinion + ApprovalTime
  3. 若审批通过:SysProjectBudget.FrozenAmount -= 申请金额 AvailableAmount -= 申请金额 + 累加 SysProject.SpentBudget(携带 Version 乐观锁)
  4. 若审批拒绝:SysProjectBudget.FrozenAmount -= 申请金额(释放冻结,不扣减 AvailableAmount
  5. 发送 Message 通知至申请人(MsgType = 'approval_result'
  6. 若当前层级为最后一级:更新 SysVehicle.Status(仅用车单据)

5.3.3 撤回事务

申请人撤回必须在同一数据库事务内原子完成:

  1. 更新业务主表 Status = 'withdrawn'CurrentApproverId = NULL
  2. 释放预算冻结:SysProjectBudget.FrozenAmount -= 申请金额
  3. 失效审批记录:ApprovalRecord.IsValid = 0
  4. 发送 Message 通知至当前审批人(告知申请已撤回)

任一步骤失败则整体回滚,确保不会出现"状态已改但预算未动"或"预算已改但状态未变"的不一致。

5.4 预算控制策略(冻结 → 扣减 → 释放)

适用表SysProjectBudget

核心公式:实际可用余额 = AvailableAmount - FrozenAmount

状态流转

环节 FrozenAmount AvailableAmount 说明
事前申请存草稿 不变 不变 草稿不参与预算控制
事前申请提交 +申请金额 不变 冻结预算,该额度不可被其他申请使用
审批通过 -申请金额 -申请金额 冻结转正式扣减
审批拒绝 -申请金额 不变 释放冻结,额度恢复可用
申请人撤回 -申请金额 不变 释放冻结
申请过期(ValidUntil 到期) -申请金额 不变 定时 Job 扫描释放

并发控制

  • 所有 UPDATE AvailableAmountFrozenAmount 的语句必须携带 WHERE Version = @oldVersion 做乐观锁校验
  • @@ROWCOUNT = 0(版本冲突),应用层重试最多 3 次
  • 禁止在事务外进行"先读后写"操作

约束

  • CHECK (FrozenAmount >= 0)
  • CHECK (AvailableAmount >= FrozenAmount):扣减后的可用余额不允许为负,但允许冻结金额等于可用余额(余额用完)
  • 前端校验时使用 AvailableAmount - FrozenAmount 作为员工可见余额
  • 若申请金额 > 可见余额,前端红色警告并触发超预算特批流程(ApprovalChain.ConditionType = 'budget_exceed'

5.5 事前申请-报销剩余额度追踪

一张已审批通过的事前申请可被多张报销单分批引用。UsageStatus 由后端在报销单状态变更时自动重算:

触发事件 计算逻辑
报销单提交(关联事前申请) 已报总额 = SUM(Expense.TotalAmount WHERE SourceApplicationId = @appId AND IsDeleted = 0)
已报总额 = 0UsageStatus = 'unused'
0 < 已报总额 < EstimatedAmountUsageStatus = 'partially_used'
已报总额 >= EstimatedAmountUsageStatus = 'fully_used'
报销单撤回/删除(关联事前申请) 重新计算,fully_used 可降级回 partially_usedunused
事前申请被拒/撤回 UsageStatus 不适用(申请未通过,不存在报销引用)

注意UsageStatus 联动更新不在审批事务边界内——报销单提交的审批事务只负责报销单自身的状态变更 + 预算扣减,UsageStatus 的更新在事务提交后异步触发,允许短暂不一致。

5.6 事前申请类型专用字段校验规则

ExpenseType 决定提交时哪些字段为必填。以下规则在应用层执行,数据库层不设 NOT NULL 约束:

ExpenseType 必填字段 校验关系
travel EstimatedStartDate, EstimatedEndDate, IsOvernight, TransportType EstimatedEndDate >= EstimatedStartDateIsOvernight=1 时前端自动计算住宿天数
entertainment EntertainmentTarget, EntertainmentLevel, GuestCount, CompanionCount, Venue, EstimatedStartDate CompanionCount <= GuestCount(陪同 ≤ 外部人数,八项规定红线)
meeting EstimatedStartDate, EstimatedEndDate, Venue EstimatedEndDate >= EstimatedStartDate;外部/内部参会人数在明细行中按 Quantity 体现
procurement —(ReferenceNo 建议填写但非必填) 附件上传报价单/合同(ExpenseApplicationAttachment
office / activity / training 通用字段已覆盖,无额外专用字段

5.7 SysProjectBudget.ExpenseType 匹配规则

同一 (ProjectId, SubjectId) 下:

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

5.8 还车登记与车辆状态同步

  • 还车登记在 Status = 'approved' 状态下均可触发(不限制当前时间必须超过 EndTime),提前还车时前端标注提示文案
  • SysVehicle.Status 由后端在审批动作时自动维护:
    • 用车申请审批通过 → SysVehicle.Status 置为 in_use
    • 用车单据变为 rejected / withdrawn / returned → 若无其他重叠时段的活跃申请,SysVehicle.Status 恢复为 idle
    • 车辆维修登记 → 人工置为 maintenance,维修期间所有申请校验拒绝

5.9 双状态机转换规则(报销单)

Status(审批状态)与 PaymentStatus(付款状态)相互独立但存在依赖:

Status:   draft → pending → approved → (财务打款后) → 归档
                       ↘ rejected
                       ↘ withdrawn

PaymentStatus: unpaid → paying → paid
               (仅 Status='approved' 时允许流转)
  • 审批通过(approved)后 PaymentStatus 才解锁,允许财务操作 unpaid → paying → paid
  • 审批被拒绝或撤回时,若 PaymentStatus 已进入 paying / paid,系统禁止撤回并告警

5.10 审批链失效与重建规则

当员工对 rejected 单据执行"重新编辑并发起"时:

  1. BizId 下所有 ApprovalRecordIsValid 置为 0(标记旧审批链失效)
  2. 新的审批流程从 ApprovalLevel = 1 开始,重新匹配 ApprovalChain 配置写入新记录
  3. 前端时间线组件渲染时增加 WHERE IsValid = 1 过滤条件

5.11 公告触达初始化策略

当管理员将公告 Statusdraft 变为 published(发布动作)时,后台通过异步 Job 根据 PrivateLevel 写入触达记录:

  • PrivateLevel = 0(全员):为所有在职员工(SysUser.IsActive = 1 AND IsDeleted = 0)批量插入 IsRead = 0 记录
  • PrivateLevel = 1(按部门)/ PrivateLevel = 2(按用户):依据 AnnouncementTarget 表的覆盖范围写入

草稿状态(Status = 'draft')不触发任何触达记录写入。新员工入职时不回溯历史公告。管理员 DING 催办仅作用于已存在记录的未读人员(IsRead = 0)。

5.12 发票查验与 InvoiceNo 为空的处理规则

当明细行 InvoiceType = 'none'(无发票场景,如小额零星采购、交通卡充值等),InvoiceNo / InvoiceCode 允许为 NULL。财务核销时三项合规自查(IsInvoiceVerified / IsTaxIdMatched / IsCategoryCompliant)的判定逻辑为:

  • 若该报销单全部明细行均为 InvoiceType = 'none',三项自查标记自动置 1,跳过发票查验流程,财务仅审核费用合理性
  • 若存在非 none 明细行但对应的 InvoiceNo 为空,前端发票查验区展示警告"明细行 X 缺少发票号码,请补充后重新查验",财务核销按钮保持锁定

5.13 收款银行默认值回写

当报销单首次提交审批通过(Status 变为 approved)时,后端自动将 Expense.BankNameExpense.AccountNameExpense.BankAccount 回写到 SysUser 表的 DefaultBankNameDefaultAccountNameDefaultBankAccount 字段,供下次发起报销时自动填充。

5.14 外勤日志新客户动态创建

当业务员输入不存在的客户名称时,提交时后端在同一事务内:

  1. INSERT INTO SysCustomer (CustomerName, ...) VALUES (@customerName, ...) 获取新 Id
  2. UPDATE OutingLog SET CustomerId = @newId WHERE Id = @logId

5.15 外勤日志未读点评红点逻辑

列表页"新点评"红点显示条件:OutingLogComment.CreateTime > COALESCE(LastViewedTime, '1900-01-01') 且评论者 CommenterId != 当前员工 Id


6. 枚举字段的完整取值说明

AI 注意:以下所有枚举取值均为数据库存储值(英文标识字符串)。编写业务代码时不得臆造新的取值,必须严格使用下表中定义的值。

6.1 通用状态机

Status(审批状态)— 多表共用

含义 适用表
draft 草稿,仅创建者可见 ExpenseApplication, Expense, Overtime, Vehicle, Announcement
pending 待审批,已提交进入审批链 ExpenseApplication, Expense, Overtime, Vehicle
approved 审批通过 ExpenseApplication, Expense, Overtime, Vehicle
rejected 审批驳回,可重新编辑发起 ExpenseApplication, Expense, Overtime, Vehicle
withdrawn 申请人主动撤回 ExpenseApplication, Expense, Overtime, Vehicle
returned 已还车归档 Vehicle(仅此表特有)
completed 已提交(外勤日志不走审批链) OutingLog
published 已发布 Announcement

PaymentStatus(付款状态)— 仅 Expense

含义
unpaid 未付款
paying 付款中(财务已发起银行转账)
paid 已付款到账

6.2 单据类型

BizType(业务单据类型)— ApprovalRecord, ApprovalChain, Message

含义 对应主表
expense_apply 事前申请 ExpenseApplication
expense 费用报销 Expense
overtime 加班申请 Overtime
vehicle 用车申请 Vehicle
announcement 行政公告 Announcement(仅 Message.BizType)
outing_log 外勤日志 OutingLog(仅 Message.BizType)

6.3 消息类型

MsgType(消息分类)— Message

含义
approval_notice 待办提醒(有单据待审批)
approval_result 审批结果通知(通过/拒绝/转交)
announcement 公告推送通知

6.4 角色与权限

RoleCode(角色编码)— SysRole

含义
employee 员工(基础权限)
approver 经理(审批权限)
finance 财务(核销权限)
admin 系统管理员(全权限)

ChangeType(角色变更类型)— SysRoleChangeLog

含义
assign_role 角色变更(增/删角色)
toggle_active 启用/禁用账号
batch 批量操作

6.5 审批相关

Action(审批动作)— ApprovalRecord

含义 约束
pending 处理中(单据刚到达审批节点)
approve 同意
reject 拒绝 强制填写 ≥5 个汉字的 Opinion
transfer 转交 必须填写 TransferToUserId

ApproverRole(审批人角色)— ApprovalChain

含义
dept_manager 部门主管(动态取 SysDepartment.ManagerId)
finance 财务专岗(动态取角色为 finance 的用户)

ConditionType(审批条件触发类型)— ApprovalChain

含义
budget_exceed 超预算高管特批(申请金额 > 可用余额时触发)
amount_threshold 金额门槛(申请金额 ≥ ConditionThreshold 时触发)
NULL 无条件(始终进入此审批层级)

6.6 费用相关

ExpenseType(费用类型)— ExpenseApplication, ExpenseDetail, SysProjectBudget

含义 适用业务
travel 差旅费 出差行程,含交通/住宿/补助
entertainment 业务招待费 宴请客户、赠送礼品、外部人员接待
procurement 日常采购 含报价单/合同的大额采购
activity 活动经费 团建、庆典等内部活动
office 办公费 文具、耗材、快递、饮用水等零散开销
meeting 会议费 内/外部会议,含场地/设备/餐饮/物料
training 培训费 员工外部培训、认证考试、教材

注意ExpenseType 为系统编码层面的固定枚举,不开放管理员自行新增。新增类型需发版支持,因为每种类型对应不同的表单 UI、审批链和费用标准校验逻辑。

InvoiceType(发票类型)— ExpenseDetail

含义 InvoiceNo/InvoiceCode
special 增值税专用发票 必填
general 增值税普通发票 必填
none 无发票 允许 NULL

Urgency(紧急程度)— ExpenseApplication

含义
normal 普通
urgent 紧急
critical 特急

TransportType(交通工具)— ExpenseApplication

含义
plane 飞机
high_speed_rail 高铁/动车
train 火车(普速)
self_drive 自驾

EntertainmentLevel(招待对象层级)— ExpenseApplication

含义 说明
normal 普通人员 一般业务对接人,人均标准最低
important 重要人员 客户中层/项目负责人
vip VIP 客户高管/政府领导,人均标准最高

UsageStatus(申请使用状态)— ExpenseApplication

含义 说明
unused 未被报销引用 申请审批通过后初始状态
partially_used 部分已报销 被一张或多张报销单分批引用,尚有剩余额度
fully_used 已全部报销 报销总金额已达申请预估金额

BizScope(字典适用范围)— SysCostCategory

含义
expense_apply 仅事前申请端使用
expense 仅费用报销端使用
both 两端通用

6.7 考勤与用车

OtType(加班类型)— Overtime

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

CompensationType(补偿形式)— Overtime

含义
overtime_pay 全部结算加班费
comp_leave 全部转为调休时数
mixed 混合模式(按 CompLeaveRatio 比例分配)

Purpose(用车目的类别)— Vehicle

含义
reception 客户接待
business 商务出行
official 公务

VehicleType(车辆类型)— SysVehicle

含义
sedan 轿车
suv SUV
mpv 商务车
van 面包车

车辆状态(SysVehicle.Status)— SysVehicle

含义
idle 空闲可用
in_use 使用中(有活跃用车申请)
maintenance 维修中(不可申请)

6.8 公告与附件

Announcement.Type(公告分类)

含义
notice 通知公告
policy 人事与制度
activity 放假与活动安排

PrivateLevel(公告可见范围级别)— Announcement

含义
0 全员发布
1 按特定部门树发布
2 按指定特定用户发布

TargetType(公告范围实体类别)— AnnouncementTarget

含义 TargetId 指向
dept 按部门 SysDepartment.Id
user 按指定个人 SysUser.Id

6.9 附件文件类型

FileType(文件格式)— 多表共用

含义
image 图片(JPG/PNG/WebP 等)
pdf PDF 文档
doc Word 文档
xls Excel 表格

OutingLogAttachment.FileType(外勤照片用途分类)— 附加取值

含义
sign_in_photo 签到照(水印含服务器授时 + GPS)
visit_photo 拜访现场照
other 其他附件

6.10 BOOLEAN 语义字段

字段 0 的含义 1 的含义
IsDeleted 正常记录(未删除) 已软删除
IsActive 停用/禁用 启用
IsRead 未读 已读
IsValid 已失效 有效
IsTop 普通 置顶
IsUrged 未催办 已催办
IsInvoiceVerified 未查验 已查验
IsTaxIdMatched 未匹配 税号已匹配
IsCategoryCompliant 不合规 合规

7. 生产级高性能企业级索引布局规范

以下索引需在 SQL Server 部署阶段直接建立,为 Flutter 前端应用提供极致流畅的下拉刷新、防抖模糊联想以及大数据穿透下钻多维分析能力。

-- ==========================================
-- 1. 费用报销列表:五状态 Chip 切换 + 时间倒序
-- ==========================================
CREATE NONCLUSTERED INDEX IX_Expense_List_Path 
ON Expense (ApplicantId, Status, CreateTime DESC) 
INCLUDE (TotalAmount, ReportNo, ApplicationDate, PaymentStatus);

-- ==========================================
-- 2. 事前申请一键导入:过滤已通过且尚有剩余额度的单据
-- ==========================================
CREATE NONCLUSTERED INDEX IX_ExpenseApp_Import_Drawer 
ON ExpenseApplication (ApplicantId, Status, UsageStatus) 
WHERE Status = 'approved' AND UsageStatus IN ('unused', 'partially_used');

-- ==========================================
-- 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, FrozenAmount, 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 催办:瞬间抓取未读员工列表
-- ==========================================
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. 审批记录有效链查询:前端时间线组件
-- ==========================================
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, ApplicationDate, EstimatedAmount, Purpose, ExpenseType, UsageStatus, ValidUntil);

-- ==========================================
-- 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. 用户角色映射:防止同一角色重复授权
-- ==========================================
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. 外勤日志点评列表
-- ==========================================
CREATE NONCLUSTERED INDEX IX_OutingLogComment_LogId 
ON OutingLogComment (LogId, IsDeleted) 
INCLUDE (CommenterId, RatingStars, CommentText, CreateTime);

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

-- ==========================================
-- 30. 事前申请明细行
-- ==========================================
CREATE NONCLUSTERED INDEX IX_ExpenseAppDetail_AppId 
ON ExpenseAppDetail (ApplicationId, SortOrder) 
INCLUDE (ExpenseCategory, EstimatedAmount, Remark);

-- ==========================================
-- 31. 报销发票附件
-- ==========================================
CREATE NONCLUSTERED INDEX IX_ExpenseAttachment_ExpenseId 
ON ExpenseAttachment (ExpenseId) 
INCLUDE (FileName, FileUrl, FileType, FileSize);

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

-- ==========================================
-- 33. 公告附件
-- ==========================================
CREATE NONCLUSTERED INDEX IX_AnnouncementAttachment_AnnId 
ON AnnouncementAttachment (AnnouncementId) 
INCLUDE (FileName, FileUrl, FileType, FileSize);

-- ==========================================
-- 34. 外勤日志照片墙
-- ==========================================
CREATE NONCLUSTERED INDEX IX_OutingLogAttachment_LogId 
ON OutingLogAttachment (LogId, SortOrder) 
INCLUDE (FileName, FileUrl, FileType);

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

-- ==========================================
-- 36. 费用类别字典:按业务范围 + 费用大类过滤
-- ==========================================
CREATE NONCLUSTERED INDEX IX_CostCategory_Scope_Type
ON SysCostCategory (BizScope, ExpenseType, ParentId, SortOrder)
INCLUDE (CategoryName, CategoryCode)
WHERE IsActive = 1 AND IsDeleted = 0;

-- ==========================================
-- 37. 预算冻结查询:按项目+科目查冻结额度
-- ==========================================
CREATE NONCLUSTERED INDEX IX_ProjectBudget_Frozen 
ON SysProjectBudget (ProjectId, SubjectId, ExpenseType)
INCLUDE (AvailableAmount, FrozenAmount, AllocatedAmount);

8. 常用查询/变更示例 (Code Snippets / Migration Examples)

以下示例基于 Dapper + ADO.NET 风格编写,展示本文档约定的正确用法。AI 在生成新业务代码时应模仿这些示例的命名、事务边界、乐观锁和软删除过滤模式。

8.1 报销单详情多表联查

一次性加载报销单的全部信息(主表 + 明细行 + 附件列表 + 申请人信息 + 审批历史),用于详情页渲染。

// 报销单详情多表联查
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,
    -- 申请人
    u.RealName AS ApplicantName,
    u.DeptId,
    -- 部门
    d.DeptName,
    -- 关联事前申请
    ea.ApplicationNo AS SourceApplicationNo,
    -- 项目
    p.ProjectName
FROM Expense e
INNER JOIN SysUser u ON u.Id = e.ApplicantId AND u.IsDeleted = 0
INNER JOIN SysDepartment d ON d.Id = e.DeptId AND d.IsDeleted = 0
LEFT JOIN ExpenseApplication ea ON ea.Id = e.SourceApplicationId AND ea.IsDeleted = 0
LEFT JOIN SysProject p ON p.Id = e.ProjectId AND p.IsDeleted = 0
WHERE e.Id = @expenseId AND e.IsDeleted = 0;

-- 明细行(含发票信息)
SELECT 
    ed.Id, ed.ExpenseDate, ed.ExpenseType, ed.ExpenseDesc,
    ed.Amount, ed.TaxAmount, ed.TotalAmount,
    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
FROM ExpenseAttachment
WHERE ExpenseId = @expenseId
ORDER BY CreateTime;

-- 审批历史
SELECT 
    ar.ApprovalLevel, ar.Action, ar.Opinion, ar.ApprovalTime,
    ar.IsValid,
    u.RealName AS ApproverName,
    tu.RealName AS TransferToUserName
FROM ApprovalRecord ar
INNER JOIN SysUser u ON u.Id = ar.ApproverId
LEFT JOIN SysUser tu ON tu.Id = ar.TransferToUserId
WHERE ar.BizId = @expenseId 
  AND ar.BizType = 'expense'
  AND ar.IsValid = 1
ORDER BY ar.ApprovalLevel;
";

8.2 预算余额查询与可用余额校验

查询某项目+科目的当前可用余额(含冻结额度),供前端展示和提交校验时使用。

public async Task<BudgetBalance> GetAvailableBalanceAsync(
    IDbConnection conn,
    long projectId, long subjectId, string expenseType)
{
    const string sql = @"
SELECT 
    AllocatedAmount,
    AvailableAmount,
    FrozenAmount,
    (AvailableAmount - FrozenAmount) AS ActualAvailable
FROM SysProjectBudget
WHERE ProjectId = @projectId 
  AND SubjectId = @subjectId 
  AND (ExpenseType = @expenseType OR ExpenseType IS NULL)
ORDER BY CASE WHEN ExpenseType = @expenseType THEN 0 ELSE 1 END;";

    var budget = await conn.QueryFirstOrDefaultAsync<BudgetBalance>(sql, new
    {
        projectId, subjectId, expenseType
    });

    return budget ?? throw new BusinessException("该项目+科目未配置预算");
}

// BudgetBalance 返回结构:
// AllocatedAmount  — 初始分配总额
// AvailableAmount  — 已扣减审批通过的额度后余额
// FrozenAmount     — 当前冻结中的额度(已提交但未审批通过)
// ActualAvailable  — 前端展示的实际可用余额 = AvailableAmount - FrozenAmount

8.3 事前申请提交 — 预算冻结(乐观锁 + 事务)

事前申请提交时冻结预算额度,携带乐观锁版本号防止并发超冻。

public async Task<bool> FreezeBudgetOnSubmitAsync(
    IDbConnection conn, IDbTransaction tx,
    long projectId, long subjectId, string expenseType, decimal amount)
{
    // 1. 查询预算记录、可用余额及当前版本号
    const string querySql = @"
SELECT Id, AvailableAmount, FrozenAmount, AllocatedAmount, Version
FROM SysProjectBudget
WHERE ProjectId = @projectId 
  AND SubjectId = @subjectId 
  AND (ExpenseType = @expenseType OR ExpenseType IS NULL)
ORDER BY CASE WHEN ExpenseType = @expenseType THEN 0 ELSE 1 END;";

    var budgets = (await conn.QueryAsync<ProjectBudget>(querySql, new
    {
        projectId, subjectId, expenseType
    }, tx)).ToList();

    var budget = budgets.FirstOrDefault();
    if (budget == null)
        throw new BusinessException("该项目+科目未配置预算");

    // 可用余额 = AvailableAmount - FrozenAmount
    var availableBalance = budget.AvailableAmount - budget.FrozenAmount;
    if (availableBalance < amount)
        throw new BusinessException($"预算余额不足,可用 {availableBalance:F2},申请 {amount:F2}");

    // 2. 乐观锁冻结(WHERE Version = @oldVersion)
    const string freezeSql = @"
UPDATE SysProjectBudget
SET FrozenAmount = FrozenAmount + @amount,
    UpdateTime = GETDATE()
WHERE Id = @id AND Version = @oldVersion;";

    var affected = await conn.ExecuteAsync(freezeSql, new
    {
        id = budget.Id,
        amount,
        oldVersion = budget.Version
    }, tx);

    if (affected == 0)
        throw new ConcurrencyException("预算版本冲突,冻结失败,请重试"); // 应用层重试最多 3 次

    return true;
}

8.4 审批通过 — 冻结转扣减 + 释放剩余冻结

审批通过时:释放冻结额度,同时扣减可用余额。两者在同一事务内执行。

public async Task ConvertFreezeToDeductionAsync(
    IDbConnection conn, IDbTransaction tx,
    long projectId, long subjectId, string expenseType, decimal amount)
{
    const string sql = @"
UPDATE SysProjectBudget
SET FrozenAmount = FrozenAmount - @amount,
    AvailableAmount = AvailableAmount - @amount,
    UpdateTime = GETDATE()
WHERE ProjectId = @projectId 
  AND SubjectId = @subjectId 
  AND (ExpenseType = @expenseType OR ExpenseType IS NULL)
  AND Version = @oldVersion;";

    // ... 乐观锁校验逻辑同 8.3
}

8.5 拒绝/撤回 — 释放冻结

审批拒绝或申请人撤回时:仅释放冻结额度,可用余额不扣减。

public async Task ReleaseFreezeAsync(
    IDbConnection conn, IDbTransaction tx,
    long projectId, long subjectId, string expenseType, decimal amount)
{
    const string sql = @"
UPDATE SysProjectBudget
SET FrozenAmount = FrozenAmount - @amount,
    UpdateTime = GETDATE()
WHERE ProjectId = @projectId 
  AND SubjectId = @subjectId 
  AND (ExpenseType = @expenseType OR ExpenseType IS NULL)
  AND Version = @oldVersion;";

    // ... 乐观锁校验逻辑同 8.3
}

8.6 事前申请 UsageStatus 联动更新

报销单提交/撤回/删除后,触发事前申请的 UsageStatus 重算。

public async Task UpdateUsageStatusAsync(
    IDbConnection conn, IDbTransaction tx, long applicationId)
{
    const string sql = @"
UPDATE ea
SET ea.UsageStatus = 
    CASE 
        WHEN ISNULL(SUM(e.TotalAmount), 0) = 0 THEN 'unused'
        WHEN ISNULL(SUM(e.TotalAmount), 0) >= ea.EstimatedAmount THEN 'fully_used'
        ELSE 'partially_used'
    END,
    ea.UpdateTime = GETDATE()
FROM ExpenseApplication ea
LEFT JOIN Expense e 
    ON e.SourceApplicationId = ea.Id 
    AND e.IsDeleted = 0
WHERE ea.Id = @applicationId
GROUP BY ea.Id, ea.EstimatedAmount;";

    await conn.ExecuteAsync(sql, new { applicationId }, tx);
}

8.7 待审批列表查询(多态外键 JOIN)

经理端"待我审批"列表需跨四种业务类型查询,每种类型 JOIN 对应的业务表和申请人信息。

// 方案:按 BizType 分别查询后 UNION ALL,或应用层分四次查询合并
const string sql = @"
-- 待审批的报销单
SELECT 
    'expense' AS BizType,
    e.Id AS BizId,
    e.ReportNo AS BizNo,
    e.TotalAmount AS Amount,
    e.Purpose AS Summary,
    e.CreateTime AS SubmitTime,
    u.RealName AS ApplicantName,
    d.DeptName,
    ar.Id AS RecordId,
    ar.ApprovalLevel
FROM ApprovalRecord ar
INNER JOIN Expense e ON e.Id = ar.BizId AND e.IsDeleted = 0
INNER JOIN SysUser u ON u.Id = e.ApplicantId
INNER JOIN SysDepartment d ON d.Id = e.DeptId
WHERE ar.ApproverId = @approverId
  AND ar.Action = 'pending'
  AND ar.IsValid = 1

UNION ALL

-- 待审批的加班申请
SELECT 
    'overtime' AS BizType,
    o.Id AS BizId,
    o.ApplicationNo AS BizNo,
    o.NetOtHours AS Amount,
    o.Reason AS Summary,
    o.CreateTime AS SubmitTime,
    u.RealName AS ApplicantName,
    d.DeptName,
    ar.Id AS RecordId,
    ar.ApprovalLevel
FROM ApprovalRecord ar
INNER JOIN Overtime o ON o.Id = ar.BizId AND o.IsDeleted = 0
INNER JOIN SysUser u ON u.Id = o.ApplicantId
INNER JOIN SysDepartment d ON d.Id = o.DeptId
WHERE ar.ApproverId = @approverId
  AND ar.Action = 'pending'
  AND ar.IsValid = 1

UNION ALL

-- 待审批的用车申请
SELECT 
    'vehicle' AS BizType,
    v.Id AS BizId,
    v.ApplicationNo AS BizNo,
    0 AS Amount,  -- 用车无金额字段
    v.Reason AS Summary,
    v.CreateTime AS SubmitTime,
    u.RealName AS ApplicantName,
    d.DeptName,
    ar.Id AS RecordId,
    ar.ApprovalLevel
FROM ApprovalRecord ar
INNER JOIN Vehicle v ON v.Id = ar.BizId AND v.IsDeleted = 0
INNER JOIN SysUser u ON u.Id = v.ApplicantId
INNER JOIN SysDepartment d ON d.Id = v.DeptId
WHERE ar.ApproverId = @approverId
  AND ar.Action = 'pending'
  AND ar.IsValid = 1

UNION ALL

-- 待审批的事前申请
SELECT 
    'expense_apply' AS BizType,
    ea.Id AS BizId,
    ea.ApplicationNo AS BizNo,
    ea.EstimatedAmount AS Amount,
    ea.Purpose AS Summary,
    ea.CreateTime AS SubmitTime,
    u.RealName AS ApplicantName,
    d.DeptName,
    ar.Id AS RecordId,
    ar.ApprovalLevel
FROM ApprovalRecord ar
INNER JOIN ExpenseApplication ea ON ea.Id = ar.BizId AND ea.IsDeleted = 0
INNER JOIN SysUser u ON u.Id = ea.ApplicantId
INNER JOIN SysDepartment d ON d.Id = ea.DeptId
WHERE ar.ApproverId = @approverId
  AND ar.Action = 'pending'
  AND ar.IsValid = 1

ORDER BY SubmitTime DESC;
";

8.4 单据编号原子生成(sp_getapplock)

高并发下安全生成按天重置的单据编号。

public async Task<string> GenerateApplicationNoAsync(
    IDbConnection conn, IDbTransaction tx, string prefix, 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);

    // 查询当日最大序号
    const string sql = @"
SELECT MAX(ApplicationNo) 
FROM ExpenseApplication WITH (UPDLOCK, ROWLOCK)
WHERE ApplicationNo LIKE @pattern";

    var maxNo = await conn.QuerySingleOrDefaultAsync<string>(sql, new { pattern }, tx);

    int seq = 1;
    if (maxNo != null)
    {
        // 提取序号部分(后 3 位)
        var seqStr = maxNo.Substring(maxNo.Length - 3);
        seq = int.Parse(seqStr) + 1;
    }

    return $"{prefix}-{dateStr}-{seq:D3}";
}

注意:以上编号生成逻辑适用于所有单据类型,仅需替换表名和前缀参数。


文档版本:v2.0(主键策略变更:VARCHAR(36) GUID → BIGINT IDENTITY(1,1);文档结构重组为 8 节标准格式)