tboss-oa-database.md 52 KB

TBOSS OA 模块 — 数据库表结构设计

版本:v1.0 | 日期:2026-06-04 | 基于 PRD v1.0


1. 设计约定

1.1 技术栈

项目 规格
数据库 Microsoft SQL Server 2019+
后端 .NET Framework 4.8 + Dapper / ADO.NET
移动端 Flutter 3.38.10(通过 HTTP API 交互,不直连数据库)
字符集 Chinese_PRC_CI_AS

1.2 命名规范

范畴 规范 示例
表名 PascalCase,单数 Expense, OaPermission
列名 PascalCase Id, ApprovalInstanceId
主键 BIGINT IDENTITY(1,1) Id BIGINT IDENTITY(1,1) PRIMARY KEY
ERP 引用列 存储 ERP ID,非 FK 约束 ApplicantId BIGINT(注释标注来源 ERP)
约束名 PK_表名 / UX_表名_列名 PK_Expense, UX_Expense_ReportNo
索引名 IX_表名_用途 IX_Expense_List

1.3 全局字段

每张 OA 自管表必须包含:

字段 类型 约束 说明
CreateTime DATETIME NOT NULL DEFAULT GETDATE() 服务器授时,行创建时间,不可修改
UpdateTime DATETIME NULL 最后修改时间,应用层 UPDATE 时显式赋 GETDATE()
IsDeleted BIT NOT NULL DEFAULT 0 软删除标记,0=正常,1=已删除。例外:OaPermissionChangeLog 审计日志不可删除

1.4 精度规范

类型 精度 示例
金额 DECIMAL(18,2) TotalAmount
经纬度 DECIMAL(10,6) CheckInLongitude
税率 DECIMAL(5,4) TaxRate
工时 DECIMAL(4,1) NetOtHours

1.5 状态枚举

所有状态/类型字段使用 VARCHAR(20) 存储英文标识,前端国际化映射。完整取值见第 6 节。

1.6 单据编号

格式 {前缀}-{YYYYMMDD}-{序号},序号按天重置高位补零至 3 位。.NET 服务端使用 sp_getapplock 原子生成。

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

2. 数据库 ERD

                        ┌─────────────────────────────┐
                        │     OA 权限体系(独立)        │
                        │  OaPermission                │
                        │  OaUserPermission            │
                        │  OaPermissionChangeLog       │
                        └─────────────────────────────┘

  [ERP 用户] ◄──── 所有业务表的 ApplicantId / SalespersonId
  [ERP 部门] ◄──── 所有业务表的 DeptId
  [ERP 客户] ◄──── OutingLog.CustomerId
  [ERP 审批] ◄──── ApprovalInstanceId(审批类主表)
  [ERP 预算] ◄──── BudgetService 适配器(余额/冻结/释放)
  [ERP 项目] ◄──── ProjectService 适配器(列表/级联)
  [ERP 科目] ◄──── SubjectService 适配器(预算科目)
  [ERP 成本] ◄──── CostCenterService 适配器(成本中心)

  ┌──────────────────────────────────────────────────────┐
  │                    OA 自管业务表                        │
  │                                                       │
  │  ExpenseApplication ──(1:N)── ExpenseAppDetail        │
  │       │                         (明细行)              │
  │                                                       │
  │  ExpenseApplicationMapping ── 申请↔报销多对多          │
  │       ├──(N:1)── Expense                              │
  │       │            (费用明细, 含币种)                   │
  │       │            └──(1:N)── ExpenseDetail            │
  │       └──(N:1)── ExpenseApplication                   │
  │                                                       │
  │  Overtime                                             │
  │                                                       │
  │  Vehicle ──(1:N)── VehiclePassenger                   │
  │       │                                               │
  │       └──(N:1)── SysVehicle (OA 自管车辆池)            │
  │                                                       │
  │  OutingLog ──(1:N)── OutingLogComment                 │
  │                                                       │
  │  Announcement ──(1:N)── AnnouncementTarget            │
  │       └──(1:N)── AnnouncementReadLog                  │
  │                                                       │
  │  Attachment ── 统一附件表                              │
  │       BizType: expense_apply/expense/outing_log/announcement │
  └──────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────┐
  │                    OA 自管基础表                        │
  │  SysVehicle         — 车辆池                           │
  │  SysBanner          — 工作台轮播图                      │
  │  SysCostCategory    — 费用类别字典                      │
  └──────────────────────────────────────────────────────┘

关键变更

  • 审批/预算/项目/科目/成本中心 → 全部通过 .NET 适配器对接 ERP,OA 本地不存储
  • Expense.SourceApplicationIdExpenseApplicationMapping 多对多关联
  • 4 张附件表合并为 1 张 Attachment 聚合表

3. 表结构定义

3.1 OA 权限体系

3.1.1 OaPermission — 权限点字典

OA 独立权限系统的权限点定义表,存储所有可用权限编码及中文名称。权限点按模块分组,管理员通过快捷套餐或手动勾选为用户赋权。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
PermissionCode VARCHAR(80) 权限点唯一编码,如 oa.expense.apply。UNIQUE 约束
PermissionName NVARCHAR(50) 权限点中文名称,如"发起报销",用于管理界面展示
Module VARCHAR(30) 所属功能模块,用于分组展示:expense / expense_apply / overtime / vehicle / outing_log / announcement / report / admin
SortOrder INT 管理界面排序权重,值越小越靠前,默认 0

本表为字典表,不包含 CreateTime/UpdateTime/IsDeleted。权限点预置后一般不增删。

3.1.2 OaUserPermission — 用户-权限关联(ACL)

用户与权限点的多对多关联表。每个用户可拥有多个权限点,通过 IsDeleted 软删除实现权限的授予与回收。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
UserId BIGINT ERP 用户 ID(非 FK 约束,关联 ERP 用户主数据)
PermissionId BIGINT 权限点 ID,FK → OaPermission.Id
GrantedBy BIGINT 授权操作人 ERP 用户 ID,用于审计追溯
CreateTime DATETIME DEFAULT GETDATE(),授权时间
UpdateTime DATETIME 最后变更时间
IsDeleted BIT DEFAULT 0。移除权限时设为 1(软删除,保留历史记录),重新授权时置回 0

索引

  • UX_UserPermission UNIQUE (UserId, PermissionId) WHERE IsDeleted = 0 — 防止同一权限重复授权
  • IX_UserPermission_UserId (UserId, IsDeleted) INCLUDE (PermissionId) — 按用户查权限列表

3.1.3 OaPermissionChangeLog — 权限变更审计

记录管理员每次修改用户权限的完整快照。本表仅追加不修改(无 UpdateTime),不可软删除。用于变更追溯和合规审计。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
TargetUserId BIGINT 被操作的目标用户 ERP 用户 ID
OperatorId BIGINT 执行操作的管理员 ERP 用户 ID
ChangeType VARCHAR(20) 变更类型:assign(赋权) / revoke(收权) / toggle_active(启停)
BeforeSnapshot NVARCHAR(MAX) 变更前权限 JSON 快照(含权限列表 + IsActive 状态)
AfterSnapshot NVARCHAR(MAX) 变更后权限 JSON 快照
CreateTime DATETIME DEFAULT GETDATE(),审计时间戳

本表无 UpdateTime 和 IsDeleted —— 审计日志不可修改、不可删除。

索引IX_PermChangeLog_Target (TargetUserId, CreateTime DESC) INCLUDE (OperatorId, ChangeType)


3.2 事前申请与费用报销

3.2.1 ExpenseApplication — 事前申请主表

员工在费用发生前提交的预估申请。支持多费用类型、关联项目与预算科目、多行预估明细。审批通过后可被多张报销单分批引用。

变更说明ApplicantIdDeptIdProjectIdBudgetSubjectId 为 ERP 对应实体 ID(非 FK)。ExpenseTypeExpenseTypes(逗号分隔多选)。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ApplicationNo VARCHAR(30) 单据唯一编号,格式 BXSQ-YYYYMMDD-XXX,提交时由 .NET 服务端原子生成
ApplicantId BIGINT 申请人 ERP 用户 ID
DeptId BIGINT 申请人所属部门 ERP 部门 ID,提交时写入
ApplicationDate DATE 业务申请日期(提交时写入当日,与 CreateTime 服务器时间区分)
ProjectId BIGINT 关联项目,对应 ERP 项目主数据 ID。通过 .NET → ERP ProjectService 下拉选择,选定后联动查询该项目下的预算科目
BudgetSubjectId BIGINT 关联预算科目,对应 ERP 科目主数据 ID。通过 .NET → ERP SubjectService 加载。选定后联合 ProjectId 查询可用预算余额
EstimatedAmount DECIMAL(18,2) 预估申请总金额 = 所有明细行 Quantity × UnitPrice 汇总,只读不手动修改,默认 0
Urgency VARCHAR(10) 紧急程度:normal(普通)/urgent(紧急)/critical(特急),默认 normal
ExpenseTypes VARCHAR(200) 申请涵盖的费用大类,逗号分隔支持多选。如 "travel,office" 表示同时申请差旅费和办公费。选定的值会联动过滤明细行的 SysCostCategory 下拉选项(只展示绑定了该大类的费用小类)。值来自枚举 §6.3
Purpose NVARCHAR(500) 费用事由详细说明,限制 200 字
Status VARCHAR(20) 业务状态:draft(草稿)/pending(审批中)/approved(已通过)/rejected(已拒绝)/withdrawn(已撤回)。审批状态通过 .NET → ERP 实时查询
UsageStatus VARCHAR(20) 被报销引用的状态:unused(未引用)/partially_used(部分引用)/fully_used(已用完)。由报销单提交/删除时自动重算,默认 unused
IsTaxIncluded BIT 预估金额是否含税:0=不含税,1=含税。影响后续报销核销时的金额对比
ValidUntil DATE 申请有效期截止日。到期后定时 Job 自动释放冻结预算,UsageStatus 标记为 fully_used,不可再被报销引用
ReferenceNo VARCHAR(50) 关联的外部单号(合同号/询价单号),用于采购类申请追溯,非必填
ApprovalInstanceId VARCHAR(50) ERP 审批实例 ID,提交审批时由 .NET → ERP 创建后回写。撤回/驳回重新提交时覆盖为新值
PreviousInstanceIds VARCHAR(MAX) 历史审批实例 ID JSON 数组,撤回/驳回重新提交时追加旧 ApprovalInstanceId,用于追溯完整审批历史
以下为费用类型专用字段(数据库全部允许 NULL,应用层按 ExpenseTypes 校验)
EstimatedStartDate DATE 预计行程开始日期,仅 ExpenseTypes 含 travel 或 meeting 时前端展示并必填
EstimatedEndDate DATE 预计行程结束日期,仅 ExpenseTypes 含 travel 或 meeting 时必填。校验:必须 ≥ StartDate
IsOvernight BIT 是否过夜:1=需住宿,0=当天来回。仅 travel 时生效,决定住宿天数
TransportType VARCHAR(20) 交通工具:plane(飞机)/high_speed_rail(高铁)/train(普速火车)/self_drive(自驾)。仅 travel 时必填
EntertainmentTarget NVARCHAR(200) 招待对象单位名称,仅 ExpenseTypes 含 entertainment 时必填,用于税务合规
EntertainmentLevel VARCHAR(20) 招待对象层级:normal(普通)/important(重要)/vip(VIP)。影响人均消费标准
GuestCount INT 外部招待人数,仅 entertainment 时必填
CompanionCount INT 内部陪同人数,仅 entertainment 时必填。应用层校验:必须 ≤ GuestCount
Venue NVARCHAR(200) 招待或会议举办地点,entertainment 或 meeting 时必填
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引

  • IX_ExpenseApp_List (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseTypes, UsageStatus)
  • IX_ExpenseApp_Import (ApplicantId, UsageStatus) WHERE UsageStatus IN ('unused', 'partially_used') — 报销导入时快速过滤可用申请

3.2.2 ExpenseAppDetail — 事前申请预估明细

事前申请的逐行预估明细。每行包含费用类别、数量、单价,汇总金额写入主表 EstimatedAmount。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ApplicationId BIGINT 关联主表,FK → ExpenseApplication.Id(主表软删除时级联软删除)
ExpenseCategory VARCHAR(20) 费用细分类别编码,值来源于 SysCostCategory.CategoryCode
Quantity INT 数量,默认 1
UnitPrice DECIMAL(18,2) 单价
Unit VARCHAR(10) 计量单位(张/间/人/天/套/个),辅助展示
EstimatedAmount DECIMAL(18,2) 单项预估金额 = Quantity × UnitPrice,前端自动计算只读展示
Remark NVARCHAR(200) 明细项说明备注
SortOrder INT 排序号,默认 1,控制移动端渲染顺序
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_ExpenseAppDetail_AppId (ApplicationId, SortOrder) INCLUDE (ExpenseCategory, EstimatedAmount, Remark)

3.2.3 Attachment — 统一附件表

事前申请(支撑材料)、费用报销(发票影像)、外勤日志(现场照片)、公告(附件)四种业务附件合并为一张聚合表。通过 BizType 区分业务类型,DetailId 可绑定报销明细行(仅报销使用),SortOrder 用于外勤照片墙排序。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
BizType VARCHAR(30) 业务类型:expense_apply(事前申请)/expense(费用报销)/outing_log(外勤日志)/announcement(公告)
BizId BIGINT 父表 ID(多态,与 BizType 联合路由到具体业务表)
DetailId BIGINT 仅 BizType='expense' 使用,绑定 ExpenseDetail.Id,实现发票与明细行的关联
FileName NVARCHAR(200) 原始文件名
FileUrl VARCHAR(500) 云端对象存储(OSS)绝对 URL
FileType VARCHAR(20) 文件类型:image/pdf/doc/xls(通用)/ sign_in_photo(签到照)/ visit_photo(现场照)/ other(外勤专用)
FileSize BIGINT 文件字节数,用于上传大小校验
SortOrder INT 排序号,默认 0。外勤照片墙按此排序展示
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

应用层约束(前端 + .NET 校验):

  • expense_apply:≤9 张,图片 ≤10MB / PDF ≤20MB
  • expense:≤9 张,图片 ≤10MB
  • outing_log:1~9 张,仅支持 image 类型,≤10MB
  • announcement:≤5 个,PDF/图片/Word/Excel ≤20MB

索引IX_Attachment_Biz (BizType, BizId, IsDeleted) INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder)

3.2.4 ExpenseApplicationMapping — 申请↔报销多对多关联

一张报销单可导入多张事前申请,一张事前申请也可被多张报销单分批引用。每行记录一次导入关联,保存导入金额用于 UsageStatus 计算。应用层约束:单次导入的多张申请必须属于同一项目和同一预算科目(ProjectId + BudgetSubjectId 一致),以保证预算扣减的准确性。ImportedAmount 总和 ≤ 对应申请 EstimatedAmount - 已报总额

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ExpenseId BIGINT 报销单 ID,FK → Expense.Id
ApplicationId BIGINT 事前申请 ID,FK → ExpenseApplication.Id
ImportedAmount DECIMAL(18,2) 本次报销从该申请导入的金额。∑ImportedAmount 不能超过申请 EstimatedAmount
CreateTime DATETIME DEFAULT GETDATE()

约束:UNIQUE (ExpenseId, ApplicationId) — 同一张报销单不能重复导入同一张申请 索引IX_AppMapping_AppId (ApplicationId) INCLUDE (ExpenseId, ImportedAmount) — 查某申请的被引用情况

3.2.5 Expense — 费用报销主表

员工费用报销的核心主表。支持从事前申请拼单导入或直接新建,包含收款银行信息、财务核销标记。审批通过后由财务核销归档。

变更说明:删除 SourceApplicationId(改为 ExpenseApplicationMapping 多对多)。审批状态实时查询不缓存。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ReportNo VARCHAR(30) 单据唯一编号,格式 BX-YYYYMMDD-XXX,UNIQUE 约束
ApplicantId BIGINT 报销人 ERP 用户 ID
DeptId BIGINT 报销人所属部门 ERP 部门 ID
ApplicationDate DATE 报销申请日期(提交时写入)
CostCenterId BIGINT 成本中心 ERP ID,通过 .NET → ERP CostCenterService 下拉选择。可选,ERP 无数据时隐藏
ProjectId BIGINT 关联项目 ERP ID(直接新建时手动选,导入事前申请时自动带入)
BudgetSubjectId BIGINT 关联预算科目 ERP ID
TotalAmount DECIMAL(18,2) 报销总金额 = 所有明细行 TotalAmount 汇总,只读不手动修改,默认 0
Purpose NVARCHAR(500) 报销事由说明
BankName NVARCHAR(100) 收款银行全称,前端支持下拉联想(数据源 .NET 字典 API),也可自由输入
AccountName NVARCHAR(50) 收款开户户名,默认填入当前用户姓名,可修改
BankAccount VARCHAR(50) 收款银行账号,前端校验 16-19 位数字格式
IsInvoiceVerified BIT 财务核销标记一:发票已在全国增值税发票查验平台验真,默认 0
IsTaxIdMatched BIT 财务核销标记二:发票抬头与公司税号一致,默认 0
IsCategoryCompliant BIT 财务核销标记三:报销类目与发票项目匹配合规,默认 0
BankTransferNo VARCHAR(50) 财务核销时录入的银行电汇流水号
VoucherNo VARCHAR(50) 财务核销时录入的金蝶/财务系统记账凭证号
PaymentStatus VARCHAR(20) 付款状态:unpaid(待付款)/paid(已付款)。仅 Status=approved 时允许流转,默认 unpaid
Status VARCHAR(20) 业务状态:draft/pending/approved/rejected/withdrawn。审批状态通过 .NET → ERP 实时查询
ApprovalInstanceId VARCHAR(50) ERP 审批实例 ID,提交审批时由 .NET → ERP 创建后回写
PreviousInstanceIds VARCHAR(MAX) 历史审批实例 ID JSON 数组
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_Expense_List (ApplicantId, CreateTime DESC) INCLUDE (TotalAmount, ReportNo, PaymentStatus)

3.2.6 ExpenseDetail — 费用报销明细

报销单的逐行费用明细。每行对应一张发票或一笔无发票费用。支持多币种(原币+汇率+本币),币种从 ERP 汇率表获取。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ExpenseId BIGINT 关联报销主表,FK → Expense.Id(主表软删除时级联软删除)
ExpenseDate DATE 费用实际发生日期
ExpenseType VARCHAR(20) 费用细分类别编码,值来源于 SysCostCategory.CategoryCode 叶子节点。前端下拉选项受主表 ExpenseTypes(大类)和 SysCostCategory.ExpenseType 绑定关系联动过滤
ExpenseDesc NVARCHAR(200) 费用摘要描述
Amount DECIMAL(18,2) 原币不含税金额。多币种时为原币金额
TaxAmount DECIMAL(18,2) 进项税额,无发票时为 0,默认 0
TotalAmount DECIMAL(18,2) 原币价税合计 = Amount + TaxAmount
CurrencyCode VARCHAR(10) 原币币种代码,默认 CNY。选外币(如 USD/EUR/JPY)时从 .NET → ERP ExchangeRateService 获取当日汇率自动填入 ExchangeRate
ExchangeRate DECIMAL(10,4) 原币→本币汇率,CNY 时为 1.0000。由 .NET 服务端从 ERP 汇率表查询后填入
BaseAmount DECIMAL(18,2) 折算后的本币金额 = TotalAmount × ExchangeRate。预算校验和报表统计均以本币为准
InvoiceNo VARCHAR(50) 发票号码,InvoiceType=none 时允许 NULL
InvoiceCode VARCHAR(50) 发票代码
InvoiceType VARCHAR(20) 发票类型:special(专票)/general(普票)/none(无发票)。无发票场景(单笔 ≤200 元小额零星)时选 none
TaxRate DECIMAL(5,4) 税率,如 0.0600 / 0.0900 / 0.1300
SortOrder INT 排序号,默认 1
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_ExpenseDetail_ExpenseId (ExpenseId, SortOrder) INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType)


3.3 加班申请

3.3.1 Overtime — 加班申请单

员工加班申请,支持三种加班类型和三种补偿方式。后端自动扣除午休/晚餐盲区计算净工时。审批通过后可用于调休或加班费结算。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ApplicationNo VARCHAR(30) 单据唯一编号,格式 JB-YYYYMMDD-XXX,UNIQUE 约束
ApplicantId BIGINT 申请人 ERP 用户 ID
DeptId BIGINT 所属部门 ERP 部门 ID
OtType VARCHAR(10) 加班类型:workday(工作日加班)/weekend(休息日加班)/holiday(法定节假日加班)
CompensationType VARCHAR(20) 补偿方式:overtime_pay(全部结算加班费)/comp_leave(全部转为调休)/mixed(部分调休+部分加班费)
CompLeaveRatio DECIMAL(3,2) 混合模式下转为调休的比例,范围 0.10~0.90,仅 CompensationType=mixed 时生效。如 0.30=30%调休+70%加班费
StartTime DATETIME 加班开始时间,提交时必填。校验:必须早于 EndTime
EndTime DATETIME 加班结束时间,提交时必填
NetOtHours DECIMAL(4,1) 实际净工时。由 .NET 服务端自动扣除午餐(12:00-13:00)和晚餐(18:00-18:30)盲区后计算,前端只读展示。≤0 时提交按钮置灰
Reason NVARCHAR(500) 加班原因说明
Status VARCHAR(20) 业务状态:draft/pending/approved/rejected/withdrawn。审批状态通过 .NET → ERP 实时查询
ApprovalInstanceId VARCHAR(50) ERP 审批实例 ID
PreviousInstanceIds VARCHAR(MAX) 历史审批实例 ID JSON 数组
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_Overtime_List (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType)


3.4 用车申请

3.4.1 Vehicle — 用车申请与还车核销

覆盖公车申请、审批、还车登记全生命周期。车辆池 OA 自管(SysVehicle),排期冲突检测、里程表记录和实际费用报账均在 OA 完成。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ApplicationNo VARCHAR(30) 单据唯一编号,格式 YC-YYYYMMDD-XXX,UNIQUE 约束
ApplicantId BIGINT 申请人 ERP 用户 ID
DeptId BIGINT 所属部门 ERP 部门 ID
VehicleId BIGINT 申请调配的公车 ID,FK → SysVehicle.Id(OA 自管车辆池)。草稿时可空,提交时必填
Purpose VARCHAR(20) 用车目的:reception(客户接待)/business(商务出行)/official(公务)
Reason NVARCHAR(500) 用车事由详细说明
Origin NVARCHAR(200) 行程预计出发地地址
OriginLongitude DECIMAL(10,6) 始发地经度(地图选点回填)
OriginLatitude DECIMAL(10,6) 始发地纬度
Destination NVARCHAR(200) 行程预计目的地地址
DestLongitude DECIMAL(10,6) 目的地经度
DestLatitude DECIMAL(10,6) 目的地纬度
PassengerCount INT 同行总人数,默认 1
StartTime DATETIME 预计出车时间,提交时必填
EndTime DATETIME 预计还车时间,提交时必填。校验:必须晚于 StartTime
以下为还车登记字段(审批通过后填写)
ActualReturnTime DATETIME 还车登记:实际归还时间
StartOdometer DECIMAL(10,2) 还车登记:出车前里程表读数
EndOdometer DECIMAL(10,2) 还车登记:还车后里程表读数。校验:必须 > StartOdometer
ActualCost DECIMAL(18,2) 还车登记:路桥费/停车费等实际费用总额
CostRemark NVARCHAR(500) 还车登记:费用明细备注
Status VARCHAR(20) 业务状态:draft/pending/approved/rejected/withdrawn/returned(已还车)。审批状态通过 .NET → ERP 实时查询
ApprovalInstanceId VARCHAR(50) ERP 审批实例 ID
PreviousInstanceIds VARCHAR(MAX) 历史审批实例 ID JSON 数组
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引

  • IX_Vehicle_List (ApplicantId, CreateTime DESC) INCLUDE (ApplicationNo, VehicleId, Purpose, Origin, Destination, StartTime)
  • IX_Vehicle_Collision (VehicleId, StartTime, EndTime) WHERE Status IN ('pending', 'approved') — 排期冲突检测

3.4.2 VehiclePassenger — 随行同行人员

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ApplicationId BIGINT 关联用车单主表,FK → Vehicle.Id
UserId BIGINT 内部同事 ERP 用户 ID(可空,添加外部人员时为空)
PassengerName NVARCHAR(50) 同行人姓名文本(ERP 用户取 RealName,外部人员自由输入)
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_VehiclePassenger_AppId (ApplicationId, IsDeleted) INCLUDE (UserId, PassengerName)

3.4.3 SysVehicle — 企业车辆资产池(OA 自管)

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
LicensePlate NVARCHAR(20) 车牌号(含中文字符如粤B12345),UNIQUE 约束
VehicleType VARCHAR(20) 车辆类型:sedan(轿车)/suv(SUV)/mpv(商务车)/van(面包车)
Brand NVARCHAR(50) 品牌型号
Seats INT 核定座位数
DriverName NVARCHAR(50) 默认驾驶员姓名
Status VARCHAR(20) 车辆状态:idle(空闲可用)/in_use(使用中)/maintenance(维修中)。审批通过自动变更为 in_use,还车后恢复 idle
IsActive BIT 启用/停用标记,默认 1。停用的车辆不可被申请
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

3.5 外勤日志

3.5.1 OutingLog — 外勤拜访日志

业务员外出拜访客户的工作记录。GPS 定位只读防篡改,照片强制拍摄并加水印。不走审批流,提交即完成。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
VisitNo VARCHAR(30) 单据唯一编号,格式 VST-YYYYMMDD-XXX,UNIQUE 约束
SalespersonId BIGINT 业务员 ERP 用户 ID
DeptId BIGINT 所属部门 ERP 部门 ID
CustomerId BIGINT 拜访客户 ERP 客户 ID(非 FK)。输入新客户名时,提交后由 .NET → ERP CustomerService 创建新客户并回填此字段
CustomerName NVARCHAR(200) 客户公司全称冗余字段,前端输入时调 ERP 联想匹配,也支持自由输入
ContactId BIGINT 客户联系人 ERP ID
CheckInLongitude DECIMAL(10,6) GPS 硬件定位经度,提交时必填。前端设为只读,不可手动修改
CheckInLatitude DECIMAL(10,6) GPS 硬件定位纬度
CheckInAddress NVARCHAR(500) 逆地理编码出的街道地址,前端设为只读,不可手动修改
VisitSummary NVARCHAR(2000) 今日工作核心总结
NextPlan NVARCHAR(500) 后续推进计划
Status VARCHAR(20) 状态:draft(草稿)/completed(已提交)。不走审批流,提交即完成
LastViewedTime DATETIME 员工最后一次查看详情页的时间,用于判断是否有新点评(红点逻辑)
CreateTime DATETIME DEFAULT GETDATE(),服务器授时(用于防伪水印)
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引

  • IX_OutingLog_Sales (SalespersonId, CreateTime DESC) INCLUDE (CustomerId, CustomerName, CheckInAddress)
  • IX_OutingLog_Dept (DeptId, CreateTime DESC) INCLUDE (SalespersonId, CustomerId, CustomerName, CheckInAddress, Status)

3.5.2 OutingLogComment — 主管考评点评

经理在外勤日志详情页的星级评分和文字点评,以气泡样式展示。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
LogId BIGINT 关联外勤日志,FK → OutingLog.Id(主表软删除时级联软删除)
CommenterId BIGINT 点评人(经理)ERP 用户 ID
RatingStars INT 星级评分 1-5,CHECK 约束 (RatingStars IS NULL OR RatingStars >= 1 AND RatingStars <= 5)
CommentText NVARCHAR(1000) 点评指导意见文字内容
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_OutingLogComment_LogId (LogId, IsDeleted) INCLUDE (CommenterId, RatingStars, CommentText, CreateTime)


3.6 公告管理

3.6.1 Announcement — 行政公告主表

管理员发布的公司公告。支持按全员/部门/指定用户三种范围发布,可置顶、设有效期。管理员可查看已读/未读名单并 DING 催办。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
Title NVARCHAR(200) 公告标题
Content NVARCHAR(MAX) 公告正文,支持 HTML/Markdown 格式
Type VARCHAR(20) 公告分类:notice(通知公告)/policy(人事与制度)/activity(放假与活动)
Status VARCHAR(20) 发布状态:draft(草稿,仅创建者和管理员可见)/published(已发布,全员可见)
PublisherId BIGINT 发布管理员 ERP 用户 ID
PublishTime DATETIME 实际发布时间,草稿时为 NULL,发布时写入 GETDATE()
IsTop BIT 是否全局置顶:1=置顶优先展示,0=正常排序,默认 0
PrivateLevel INT 可见范围级别:0=全员,1=按部门,2=按指定用户。默认 0
ExpiryDate DATETIME 自动下架失效时间,不填则永不过期。过期后列表置灰标注"已过期"
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_Announcement_List (Type, IsTop DESC, PublishTime DESC) INCLUDE (Title, PublisherId, Status, ExpiryDate) WHERE IsDeleted = 0 AND Status = 'published'

3.6.2 AnnouncementTarget — 公告可见范围

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

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
AnnouncementId BIGINT 关联公告主表,FK → Announcement.Id
TargetType VARCHAR(10) 目标实体类型:dept(按部门)/user(按指定个人)
TargetId BIGINT 多态外键:TargetType=dept 时指向 ERP 部门 ID,TargetType=user 时指向 ERP 用户 ID
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME

3.6.3 AnnouncementReadLog — 触达率审计

记录每个员工对每条公告的已读/未读状态。发布时根据 PrivateLevel 批量初始化 IsRead=0 记录,停留 ≥2 秒标记已读。管理员通过此表查看触达率并 DING 催办。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
AnnouncementId BIGINT 关联公告,FK → Announcement.Id
UserId BIGINT 被触达员工 ERP 用户 ID
IsRead BIT 已读标记:0=未读,1=已读。停留 ≥2 秒自动标记,默认 0
ReadTime DATETIME 员工实际阅读时间
IsUrged BIT 是否已被管理员 DING 催办过,默认 0
LastUrgeTime DATETIME 最后一次 DING 催办时间
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME

约束:UNIQUE (AnnouncementId, UserId) — 每个员工对每条公告仅有一条触达记录 索引IX_Announcement_Ding (AnnouncementId, IsRead, IsUrged) INCLUDE (UserId, ReadTime)


3.7 基础字典

3.7.1 SysCostCategory — 费用类别字典(OA 自管)

费用报销和事前申请表单中"费用类别"下拉的数据源,支持二级分类。通过 BizScope 控制适用范围(事前/报销/通用),ExpenseType 可绑定费用大类以进一步过滤。与 ExpenseApplication.ExpenseTypes 的关系:后者是大类(如 travel),选定后联动过滤此表中 ExpenseType 匹配的叶子节点作为下拉选项。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
CategoryName NVARCHAR(50) 类别中文名称,如"交通费"
CategoryCode VARCHAR(30) 类别编码,UNIQUE 约束
ParentId BIGINT 上级类别 ID,FK → SysCostCategory.Id(自引用)。NULL 为一级分类,非 NULL 为叶子节点。仅叶子节点可用于明细录入
BizScope VARCHAR(20) 适用业务范围:expense_apply(仅事前申请)/expense(仅报销)/both(通用),默认 both
ExpenseType VARCHAR(20) 绑定费用大类(如 travel/office),进一步细化过滤。NULL 表示通用,不限定大类
SortOrder INT 排序权重,默认 0
IsActive BIT 启用/停用标记:1=启用,0=停用
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引

  • IX_CostCategory_Parent (ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1
  • IX_CostCategory_Scope (BizScope, ExpenseType, ParentId, SortOrder) INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 AND IsDeleted = 0

3.7.2 SysBanner — 工作台轮播图(OA 自管)

工作台顶部轮播图组件的数据源。管理员可维护图片、标题、跳转链接和排序。

字段 类型 必填 说明
Id BIGINT 主键,IDENTITY(1,1)
ImageUrl VARCHAR(500) 轮播图片云端存储绝对 URL
Title NVARCHAR(100) 图片标题/alt 文本
LinkUrl VARCHAR(500) 点击跳转链接。为空时不可点击,仅全屏预览图片
SortOrder INT 排序权重,值越小越靠前,默认 0
IsActive BIT 启用/停用标记:1=启用,0=停用
CreateTime DATETIME DEFAULT GETDATE()
UpdateTime DATETIME
IsDeleted BIT DEFAULT 0

索引IX_Banner_Active (IsActive, SortOrder) INCLUDE (ImageUrl, Title, LinkUrl) WHERE IsDeleted = 0


4. 业务逻辑与约束

4.1 软删除

所有 OA 自管表统一使用 IsDeleted 软删除。SELECT/JOIN 必须加 WHERE IsDeleted = 0。 例外:OaPermissionChangeLog(审计日志不可修改不可删除)。

4.2 审批状态管理

审批数据不在 OA 本地存储。审批状态、审批时间线、待办列表均通过 .NET → ERP 实时查询。业务表仅存两个字段用于关联和追溯:

字段 作用 更新时机
ApprovalInstanceId 关联 ERP 审批实例 提交审批时写入;撤回/驳回重新提交时覆盖
PreviousInstanceIds 追溯历史审批 撤回/驳回重新提交时追加旧 instanceId

状态同步流程

列表页 → 实时调 .NET → ERP 查询审批状态
详情页 → 实时调 .NET → ERP 查询审批状态和审批时间线
审批操作 → OA 调 .NET → ERP 执行审批动作
消息通知 → .NET 服务端消息模块在 ERP 审批事件后触发推送

4.3 事前申请-报销联动

UsageStatus 由 .NET 服务端在报销单状态变更时自动重算:

已报总额 UsageStatus
SUM(ExpenseApplicationMapping.ImportedAmount) = 0 unused
> 0 且 < EstimatedAmount partially_used
≥ EstimatedAmount fully_used

4.4 级联删除(软删除)

主表 子表
ExpenseApplication ExpenseAppDetail, Attachment (BizType='expense_apply')
Expense ExpenseDetail, ExpenseApplicationMapping, Attachment (BizType='expense')
OutingLog OutingLogComment, Attachment (BizType='outing_log')
Announcement AnnouncementTarget, AnnouncementReadLog, Attachment (BizType='announcement')

4.5 单据编号生成

.NET 服务端使用 sp_getapplock + SELECT MAX(ApplicationNo) 原子生成,保证高并发下按天不重复。


5. 索引汇总

-- 事前申请列表
CREATE NONCLUSTERED INDEX IX_ExpenseApp_List 
ON ExpenseApplication (ApplicantId, CreateTime DESC) 
INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseTypes, UsageStatus);

-- 事前申请一键导入(仅已通过且尚未用完的)
CREATE NONCLUSTERED INDEX IX_ExpenseApp_Import 
ON ExpenseApplication (ApplicantId, UsageStatus) 
WHERE UsageStatus IN ('unused', 'partially_used');

-- 费用报销列表
CREATE NONCLUSTERED INDEX IX_Expense_List 
ON Expense (ApplicantId, CreateTime DESC) 
INCLUDE (TotalAmount, ReportNo, PaymentStatus);

-- 申请↔报销多对多
CREATE NONCLUSTERED INDEX IX_AppMapping_AppId 
ON ExpenseApplicationMapping (ApplicationId) 
INCLUDE (ExpenseId, ImportedAmount);

-- 加班列表
CREATE NONCLUSTERED INDEX IX_Overtime_List 
ON Overtime (ApplicantId, CreateTime DESC) 
INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType);

-- 用车列表 + 排期冲突检测
CREATE NONCLUSTERED INDEX IX_Vehicle_List 
ON Vehicle (ApplicantId, CreateTime DESC) 
INCLUDE (ApplicationNo, VehicleId, Purpose, Origin, Destination, StartTime);
CREATE NONCLUSTERED INDEX IX_Vehicle_Collision 
ON Vehicle (VehicleId, StartTime, EndTime) 
WHERE Status IN ('pending', 'approved');

-- 外勤日志-业务员
CREATE NONCLUSTERED INDEX IX_OutingLog_Sales 
ON OutingLog (SalespersonId, CreateTime DESC) 
INCLUDE (CustomerId, CustomerName, CheckInAddress);
-- 外勤日志-部门
CREATE NONCLUSTERED INDEX IX_OutingLog_Dept 
ON OutingLog (DeptId, CreateTime DESC) 
INCLUDE (SalespersonId, CustomerId, CustomerName, CheckInAddress, Status);

-- 公告列表 + 触达审计
CREATE NONCLUSTERED INDEX IX_Announcement_List 
ON Announcement (Type, IsTop DESC, PublishTime DESC) 
INCLUDE (Title, PublisherId, Status, ExpiryDate)
WHERE IsDeleted = 0 AND Status = 'published';
CREATE NONCLUSTERED INDEX IX_Announcement_Ding 
ON AnnouncementReadLog (AnnouncementId, IsRead, IsUrged) 
INCLUDE (UserId, ReadTime);

-- 统一附件
CREATE NONCLUSTERED INDEX IX_Attachment_Biz 
ON Attachment (BizType, BizId, IsDeleted) 
INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder);

-- OA 权限
CREATE UNIQUE NONCLUSTERED INDEX UX_UserPermission 
ON OaUserPermission (UserId, PermissionId) WHERE IsDeleted = 0;
CREATE NONCLUSTERED INDEX IX_UserPermission_UserId 
ON OaUserPermission (UserId, IsDeleted) INCLUDE (PermissionId);
CREATE NONCLUSTERED INDEX IX_PermChangeLog_Target 
ON OaPermissionChangeLog (TargetUserId, CreateTime DESC) 
INCLUDE (OperatorId, ChangeType);

-- 基础字典
CREATE NONCLUSTERED INDEX IX_CostCategory_Parent 
ON SysCostCategory (ParentId, SortOrder) 
INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1;
CREATE NONCLUSTERED INDEX IX_CostCategory_Scope 
ON SysCostCategory (BizScope, ExpenseType, ParentId, SortOrder) 
INCLUDE (CategoryName, CategoryCode) WHERE IsActive = 1 AND IsDeleted = 0;
CREATE NONCLUSTERED INDEX IX_Banner_Active 
ON SysBanner (IsActive, SortOrder) 
INCLUDE (ImageUrl, Title, LinkUrl) WHERE IsDeleted = 0;

-- 外勤点评 + 用车同行人 + 报销明细 + 事前明细
CREATE NONCLUSTERED INDEX IX_OutingLogComment_LogId 
ON OutingLogComment (LogId, IsDeleted) 
INCLUDE (CommenterId, RatingStars, CommentText, CreateTime);
CREATE NONCLUSTERED INDEX IX_VehiclePassenger_AppId 
ON VehiclePassenger (ApplicationId, IsDeleted) 
INCLUDE (UserId, PassengerName);
CREATE NONCLUSTERED INDEX IX_ExpenseDetail_ExpenseId 
ON ExpenseDetail (ExpenseId, SortOrder) 
INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType);
CREATE NONCLUSTERED INDEX IX_ExpenseAppDetail_AppId 
ON ExpenseAppDetail (ApplicationId, SortOrder) 
INCLUDE (ExpenseCategory, EstimatedAmount, Remark);

6. 枚举取值

6.1 单据业务状态

审批状态(draft/pending/approved/rejected/withdrawn)仅 draft 由 OA 本地直接写入,其余状态由 ERP 审批引擎管理,OA 通过 .NET → ERP 实时查询。

含义 适用表 写入方
draft 草稿 全部业务表 OA 本地
pending 待审批 ExpenseApplication, Expense, Overtime, Vehicle OA 写入后 ERP 接管
approved 已通过 同上 ERP 管理
rejected 已拒绝 同上 ERP 管理
withdrawn 已撤回 同上 OA 本地
returned 已还车 Vehicle(独有) OA 本地
completed 已提交 OutingLog(不走审批流) OA 本地
published 已发布 Announcement(不走审批流) OA 本地

6.2 付款状态(PaymentStatus)

含义
unpaid 待付款
paid 已付款

6.3 费用类型(ExpenseTypes)

含义
travel 差旅费(含交通/住宿/补助)
entertainment 业务招待费(宴请/礼品)
procurement 日常采购(含报价单/合同)
activity 活动经费(团建/庆典)
office 办公费(文具/耗材/快递)
meeting 会议费(内/外部会议)
training 培训费(外部培训/认证)

6.4 紧急程度(Urgency)

含义
normal 普通
urgent 紧急
critical 特急

6.5 交通工具(TransportType)

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

6.6 招待对象层级(EntertainmentLevel)

含义
normal 普通人员
important 重要人员
vip VIP

6.7 申请使用状态(UsageStatus)

含义
unused 未被报销引用
partially_used 部分金额已报销
fully_used 全部金额已报销,不可再被引用

6.8 发票类型(InvoiceType)

含义 InvoiceNo/InvoiceCode
special 增值税专用发票 必填
general 增值税普通发票 必填
none 无发票(小额零星 ≤200 元) 允许 NULL

6.9 加班类型(OtType)

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

6.10 补偿方式(CompensationType)

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

6.11 用车目的(Purpose — Vehicle)

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

6.12 车辆类型(VehicleType)

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

6.13 车辆状态(SysVehicle.Status)

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

6.14 公告分类(Announcement.Type)

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

6.15 公告范围(PrivateLevel / TargetType)

PrivateLevel 含义 TargetType 含义
0 全员
1 按部门 dept TargetId → ERP 部门 ID
2 按用户 user TargetId → ERP 用户 ID

6.16 费用类别适用范围(BizScope)

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

6.17 附件文件类型(Attachment.FileType)

含义 适用 BizType
image 图片(JPG/PNG/WebP) 通用
pdf PDF 文档 expense_apply / expense / announcement
doc Word 文档 expense_apply / expense / announcement
xls Excel 表格 expense_apply / expense / announcement
sign_in_photo 外勤签到照(水印含授时+GPS) outing_log
visit_photo 外勤拜访现场照 outing_log
other 其他附件 outing_log

6.18 BOOLEAN 语义

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

6.19 权限模块(OaPermission.Module)

对应权限点前缀
expense oa.expense.*
expense_apply oa.expense_apply.*
overtime oa.overtime.*
vehicle oa.vehicle.*
outing_log oa.outing_log.*
announcement oa.announcement.*
report oa.report.*
admin oa.admin.*

6.20 权限变更类型(OaPermissionChangeLog.ChangeType)

含义
assign 赋予权限
revoke 移除权限
toggle_active 启用/禁用

7. 常用查询 / 代码示例

以下示例基于 Dapper + ADO.NET(.NET Framework 4.8 服务端)。

7.1 报销单详情查询

// 主表 + 关联的申请
const string sql = @"
SELECT 
    e.Id, e.ReportNo, e.TotalAmount, e.Purpose,
    e.BankName, e.AccountName, e.BankAccount,
    e.IsInvoiceVerified, e.IsTaxIdMatched, e.IsCategoryCompliant,
    e.BankTransferNo, e.VoucherNo,
    e.Status, e.PaymentStatus, e.ApplicationDate, e.CreateTime,
    e.ApprovalInstanceId, e.PreviousInstanceIds
FROM Expense e
WHERE e.Id = @expenseId AND e.IsDeleted = 0;

-- 关联的申请
SELECT eam.ApplicationId, eam.ImportedAmount,
    ea.ApplicationNo, ea.EstimatedAmount, ea.ExpenseTypes
FROM ExpenseApplicationMapping eam
INNER JOIN ExpenseApplication ea ON ea.Id = eam.ApplicationId AND ea.IsDeleted = 0
WHERE eam.ExpenseId = @expenseId;

-- 明细行
SELECT ed.Id, ed.ExpenseDate, ed.ExpenseType, ed.ExpenseDesc,
    ed.Amount, ed.TaxAmount, ed.TotalAmount,
    ed.CurrencyCode, ed.ExchangeRate, ed.BaseAmount,
    ed.InvoiceNo, ed.InvoiceCode, ed.InvoiceType, ed.TaxRate, ed.SortOrder,
    cc.CategoryName AS ExpenseTypeName
FROM ExpenseDetail ed
LEFT JOIN SysCostCategory cc ON cc.CategoryCode = ed.ExpenseType AND cc.IsDeleted = 0
WHERE ed.ExpenseId = @expenseId
ORDER BY ed.SortOrder;

-- 附件
SELECT Id, FileName, FileUrl, FileType, FileSize, DetailId, SortOrder
FROM Attachment
WHERE BizType = 'expense' AND BizId = @expenseId AND IsDeleted = 0
ORDER BY SortOrder, CreateTime;
";

// 申请人姓名、部门通过 .NET API GET /api/user/{ApplicantId} 获取
// 审批时间线通过 .NET API GET /api/oa/approval/timeline?bizType=expense&bizId={id} 获取

7.2 UsageStatus 联动更新

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

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

7.3 单据编号原子生成

public async Task<string> GenerateNoAsync(
    IDbConnection conn, IDbTransaction tx, string prefix, string tableName, DateTime date)
{
    var dateStr = date.ToString("yyyyMMdd");
    var pattern = $"{prefix}-{dateStr}-%";
    var lockKey = $"AppNo_{prefix}_{dateStr}";

    await conn.ExecuteAsync("sp_getapplock @Resource, @LockMode, @LockOwner", new
    {
        Resource = lockKey, LockMode = "Exclusive", LockOwner = "Transaction"
    }, tx);

    var sql = $@"
SELECT MAX(ApplicationNo) 
FROM {tableName} WITH (UPDLOCK, ROWLOCK)
WHERE ApplicationNo LIKE @pattern";

    var maxNo = await conn.QuerySingleOrDefaultAsync<string>(sql, new { pattern }, tx);
    int seq = 1;
    if (maxNo != null)
        seq = int.Parse(maxNo.Substring(maxNo.Length - 3)) + 1;

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

文档版本:v1.0 | 日期:2026-06-04