# TBOSS OA 模块 — 数据库表结构设计 > 版本:v2.0 | 日期:2026-06-03 | 基于 `tboss-oa-product-strategy.md` > > **v2.0 核心变更**:审批引擎复用 ERP、用户/组织/客户数据归 ERP 管理、新增 OA 独立权限模型。 --- ## 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` | 软删除(审计日志除外) | ### 1.4 精度规范 | 类型 | 精度 | 示例 | |------|------|------| | 金额 | `DECIMAL(18,2)` | `TotalAmount` | | 经纬度 | `DECIMAL(10,6)` | `CheckInLongitude` | | 税率 | `DECIMAL(5,4)` | `TaxRate` | | 工时 | `DECIMAL(4,1)` | `NetOtHours` | ### 1.5 状态枚举 所有状态字段使用 `VARCHAR(20)` 存储英文标识,前端国际化映射。 ### 1.6 单据编号 格式 `{前缀}-{YYYYMMDD}-{序号}`,序号按天重置高位补零至 3 位。 | 业务 | 前缀 | 示例 | |------|------|------| | 事前申请 | 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 适配器(成本中心) [ERP 标准] ◄──── StandardService 适配器(费用标准) ┌──────────────────────────────────────────────────────┐ │ OA 自管业务表 │ │ │ │ ExpenseApplication ──(1:N)── ExpenseAppDetail │ │ │ (明细行) │ │ └──(1:N)── ExpenseApplicationChange (变更记录) │ │ │ │ Expense ──(1:N)── ExpenseDetail │ │ │ (费用明细, 含币种+分摊) │ │ │ │ │ ExpenseApplicationMapping ── 申请↔报销多对多 │ │ ├──(N:1)── Expense │ │ └──(N:1)── ExpenseApplication │ │ │ │ Loan ──(1:N)── LoanRepayment (借款↔还款) │ │ │ │ │ Overtime │ │ │ │ Vehicle ──(1:N)── VehiclePassenger │ │ │ │ │ └──(N:1)── SysVehicle (OA 自管车辆池) │ │ │ │ OutingLog ──(1:N)── OutingLogComment │ │ │ │ Announcement ──(1:N)── AnnouncementTarget │ │ └──(1:N)── AnnouncementReadLog │ │ │ │ Attachment ── 统一附件表 │ │ BizType: expense_apply/expense/outing_log/announcement │ └──────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────┐ │ OA 自管基础表 │ │ SysVehicle — 车辆池 │ │ SysBanner — 工作台轮播图 │ │ SysCostCategory — 费用类别字典 │ └──────────────────────────────────────────────────────┘ ``` > **关键变更**: > - 审批/预算/项目/科目/成本中心/费用标准 → 全部通过 .NET 适配器对接 ERP,OA 本地不存储 > - `Expense.SourceApplicationId` → `ExpenseApplicationMapping` 多对多关联 > - 新增 Loan/LoanRepayment(借款备用金)、ExpenseApplicationChange(申请变更追加) > - 4 张附件表合并为 1 张 Attachment 聚合表 --- ## 3. 表结构定义 ### 3.1 OA 权限体系(新增) #### 3.1.1 OaPermission — 权限点字典 | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------| | **Id** | BIGINT | ✅ | PK, IDENTITY(1,1) | | | **PermissionCode** | VARCHAR(80) | ✅ | UNIQUE | 如 `oa.expense.apply` | | **PermissionName** | NVARCHAR(50) | ✅ | | 如"发起报销" | | **Module** | VARCHAR(30) | ✅ | | 所属模块:expense / overtime / vehicle / outing_log / announcement / report / admin | | **SortOrder** | INT | ✅ | DEFAULT 0 | 管理界面排序 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | | **UpdateTime** | DATETIME | | | | #### 3.1.2 OaUserPermission — 用户-权限关联(ACL) | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------| | **Id** | BIGINT | ✅ | PK, IDENTITY(1,1) | | | **UserId** | BIGINT | ✅ | | ERP 用户 ID(非 FK) | | **PermissionId** | BIGINT | ✅ | FK → OaPermission.Id | | | **GrantedBy** | BIGINT | ✅ | | 授权人 ERP 用户 ID | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | | **UpdateTime** | DATETIME | | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | 移除权限时软删除 | **索引**: - `UX_UserPermission` UNIQUE (UserId, PermissionId) WHERE IsDeleted = 0 - `IX_UserPermission_UserId` (UserId, IsDeleted) INCLUDE (PermissionId) #### 3.1.3 OaPermissionChangeLog — 权限变更审计 | 字段 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------| | **Id** | BIGINT | ✅ | PK, IDENTITY(1,1) | | | **TargetUserId** | BIGINT | ✅ | | 被操作人 ERP 用户 ID | | **OperatorId** | BIGINT | ✅ | | 操作人 ERP 用户 ID | | **ChangeType** | VARCHAR(20) | ✅ | | `assign` / `revoke` / `toggle_active` | | **BeforeSnapshot** | NVARCHAR(MAX) | ✅ | | 变更前权限 JSON | | **AfterSnapshot** | NVARCHAR(MAX) | ✅ | | 变更后权限 JSON | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | > 本表无 `UpdateTime` 和 `IsDeleted` — 审计日志不可修改不可删除。 **索引**:`IX_PermChangeLog_Target` (TargetUserId, CreateTime DESC) INCLUDE (OperatorId, ChangeType) --- ### 3.2 事前申请与费用报销 #### 3.2.1 ExpenseApplication — 事前申请主表 **变更说明**:`ApplicantId`、`DeptId`、`ProjectId`、`BudgetSubjectId` 不再 FK 到本地表,改为存储 ERP 对应实体 ID。`ExpenseType` → `ExpenseTypes`(逗号分隔,支持一次申请多费用类型)。新增审批字段、关联 ERP 集成字段。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK, IDENTITY(1,1) | | **ApplicationNo** | VARCHAR(30) | ✅ | UNIQUE,单号 BXSQ-YYYYMMDD-XXX | | **ApplicantId** | BIGINT | ✅ | ERP 用户 ID | | **DeptId** | BIGINT | ✅ | ERP 部门 ID | | **ApplicationDate** | DATE | | 业务申请日期(提交时写入) | | **ProjectId** | BIGINT | | ERP 项目 ID | | **BudgetSubjectId** | BIGINT | | ERP 预算科目 ID | | **EstimatedAmount** | DECIMAL(18,2) | ✅ | DEFAULT 0,明细行汇总 | | **Urgency** | VARCHAR(10) | ✅ | DEFAULT 'normal' | | **ExpenseTypes** | VARCHAR(200) | ✅ | 费用类型,逗号分隔支持多种。如 "travel,office" | | **Purpose** | NVARCHAR(500) | ✅ | 费用事由(≤200字) | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | | **UsageStatus** | VARCHAR(20) | ✅ | DEFAULT 'unused' | | **IsTaxIncluded** | BIT | ✅ | DEFAULT 0 | | **ValidUntil** | DATE | | 申请有效期 | | **ReferenceNo** | VARCHAR(50) | | 关联合同号/询价单号 | | **EstimatedStartDate** | DATE | | 差旅/会议专用 | | **EstimatedEndDate** | DATE | | 差旅/会议专用 | | **IsOvernight** | BIT | | 差旅专用 | | **TransportType** | VARCHAR(20) | | 差旅专用 | | **EntertainmentTarget** | NVARCHAR(200) | | 招待专用 | | **EntertainmentLevel** | VARCHAR(20) | | 招待专用 | | **GuestCount** | INT | | 招待专用 | | **CompanionCount** | INT | | 招待专用 | | **Venue** | NVARCHAR(200) | | 招待/会议地点 | | **ApprovalInstanceId** | VARCHAR(50) | | **新增**:ERP 审批实例 ID | | **ApprovalStatus** | VARCHAR(20) | | **新增**:OA 本地缓存状态 | | **PreviousInstanceIds** | VARCHAR(MAX) | | **新增**:历史实例 ID JSON 数组 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**: - `IX_ExpenseApp_List` (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseType, UsageStatus) - `IX_ExpenseApp_Import` (ApplicantId, Status, UsageStatus) WHERE Status = 'approved' AND UsageStatus IN ('unused', 'partially_used') #### 3.2.2 ExpenseApplicationChange — 申请变更/追加记录 已通过的事前申请可发起变更(追加金额、增删明细、延期),变更提交后触发补充审批。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **ApplicationId** | BIGINT | ✅ | FK → ExpenseApplication.Id | | **ChangeType** | VARCHAR(20) | ✅ | amount_increase / amount_decrease / detail_add / detail_remove / date_extend | | **BeforeSnapshot** | NVARCHAR(MAX) | ✅ | 变更前 JSON 快照(含明细行) | | **AfterSnapshot** | NVARCHAR(MAX) | ✅ | 变更后 JSON 快照 | | **ChangeReason** | NVARCHAR(500) | ✅ | 变更事由 | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'pending'(pending / approved / rejected) | | **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | **索引**:`IX_AppChange_AppId` (ApplicationId, CreateTime DESC) **审批通过后**:.NET 服务端将 `AfterSnapshot` 的明细数据写回 `ExpenseApplication` 和 `ExpenseAppDetail`,同时更新 `EstimatedAmount`。 --- #### 3.2.3 ExpenseAppDetail — 事前申请明细 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK, IDENTITY(1,1) | | **ApplicationId** | BIGINT | ✅ | FK → ExpenseApplication.Id(级联软删除) | | **ExpenseCategory** | VARCHAR(20) | ✅ | 费用细分类别(SysCostCategory.CategoryCode) | | **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 | ✅ | | | **UpdateTime** | DATETIME | | | **索引**:`IX_ExpenseAppDetail_AppId` (ApplicationId, SortOrder) INCLUDE (ExpenseCategory, EstimatedAmount, Remark) #### 3.2.4 Attachment — 统一附件表 4 种业务附件合并为一张聚合表,通过 `BizType` 区分业务类型。`DetailId` 仅报销场景使用(可选绑定到具体明细行),`SortOrder` 用于外勤照片墙排序。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK, IDENTITY(1,1) | | **BizType** | VARCHAR(30) | ✅ | expense_apply / expense / outing_log / announcement | | **BizId** | BIGINT | ✅ | 父表 ID(多态,非 FK,与 BizType 联合路由) | | **DetailId** | BIGINT | | 仅 BizType='expense' 使用,绑定 ExpenseDetail.Id | | **FileName** | NVARCHAR(200) | ✅ | 原始文件名 | | **FileUrl** | VARCHAR(500) | ✅ | 云端存储 URL | | **FileType** | VARCHAR(20) | ✅ | image / pdf / doc / xls / sign_in_photo / visit_photo / other | | **FileSize** | BIGINT | | 字节数 | | **SortOrder** | INT | | DEFAULT 0,外勤照片墙排序 | | **CreateTime** | DATETIME | ✅ | DEFAULT GETDATE() | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**: - `IX_Attachment_Biz` (BizType, BizId, IsDeleted) INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder) **各业务约束**(应用层校验): | BizType | 数量限制 | FileType 取值 | 额外约束 | |---------|---------|-------------|---------| | expense_apply | ≤6 | image/pdf/doc/xls | 单图≤10MB, PDF≤20MB | | expense | ≤9 | image/pdf/doc/xls | DetailId 可选绑定明细行;单图≤10MB | | outing_log | 1~4 | sign_in_photo/visit_photo/other | ≤10MB;提交时≥1 张 | | announcement | ≤5 | image/pdf/doc/xls | ≤20MB | **合并理由**:4 张表结构 90% 相同,唯一差异(DetailId/SortOrder)均为 NULL 允许字段。合并后 CRUD 代码从 4 套降为 1 套,前端一个 `AttachmentPicker` 组件覆盖全部场景。多态 BizType+BizId 模式与审批/公告范围等模块的架构一致。 #### 3.2.5 ExpenseApplicationMapping — 申请↔报销多对多关联 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **ExpenseId** | BIGINT | ✅ | FK → Expense.Id | | **ApplicationId** | BIGINT | ✅ | FK → ExpenseApplication.Id | | **ImportedAmount** | DECIMAL(18,2) | ✅ | 本次报销从该申请导入的金额 | | **CreateTime** | DATETIME | ✅ | | **约束**:UNIQUE (ExpenseId, ApplicationId) **索引**:`IX_AppMapping_AppId` (ApplicationId) INCLUDE (ExpenseId, ImportedAmount) **UsageStatus 新算法**: ``` 已报总额 = SUM(ExpenseApplicationMapping.ImportedAmount) JOIN Expense ON Expense.Id = ExpenseApplicationMapping.ExpenseId WHERE Expense.IsDeleted = 0 AND Expense.Status != 'draft' AND ExpenseApplicationMapping.ApplicationId = @appId ``` 根据已报总额与 `EstimatedAmount` 比较得出 unused / partially_used / fully_used。 --- #### 3.2.6 Expense — 费用报销主表 **变更说明**:删除 `SourceApplicationId`(改为 `ExpenseApplicationMapping` 多对多关联)。新增 `PaymentType` 区分对公/对私。ERP 引用字段改为存储 ID。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **ReportNo** | VARCHAR(30) | ✅ | UNIQUE,单号 BX-YYYYMMDD-XXX | | **ApplicantId** | BIGINT | ✅ | ERP 用户 ID | | **DeptId** | BIGINT | ✅ | ERP 部门 ID | | **ApplicationDate** | DATE | | 业务日期 | | **CostCenterId** | BIGINT | | ERP 成本中心 ID | | **ProjectId** | BIGINT | | ERP 项目 ID | | **BudgetSubjectId** | BIGINT | | ERP 预算科目 ID | | **TotalAmount** | DECIMAL(18,2) | ✅ | DEFAULT 0 | | **PaymentType** | VARCHAR(20) | ✅ | DEFAULT 'personal'(personal=对私报销 / corporate=对公付款) | | **SupplierName** | NVARCHAR(200) | | 对公付款时填供应商名称 | | **Purpose** | NVARCHAR(500) | ✅ | | | **BankName** | NVARCHAR(100) | ✅ | | | **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' | | **ApprovalInstanceId** | VARCHAR(50) | | | | **ApprovalStatus** | VARCHAR(20) | | | | **PreviousInstanceIds** | VARCHAR(MAX) | | | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_Expense_List` (ApplicantId, Status, CreateTime DESC) INCLUDE (TotalAmount, ReportNo, PaymentStatus) #### 3.2.7 ExpenseDetail — 费用报销明细 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **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,进项税额 | | **TotalAmount** | DECIMAL(18,2) | ✅ | 价税合计(多币种时为原币价税合计) | | **CurrencyCode** | VARCHAR(10) | | DEFAULT 'CNY',原币币种(USD/EUR/JPY/CNY...) | | **ExchangeRate** | DECIMAL(10,4) | | 汇率(CNY 时为 1.0000) | | **BaseAmount** | DECIMAL(18,2) | | 折算本币金额(= TotalAmount × ExchangeRate) | | **InvoiceNo** | VARCHAR(50) | | 发票号码 | | **InvoiceCode** | VARCHAR(50) | | 发票代码 | | **InvoiceType** | VARCHAR(20) | ✅ | special / general / none | | **TaxRate** | DECIMAL(5,4) | | 税率 | | **AllocationPercent** | DECIMAL(5,2) | | DEFAULT NULL(NULL=100%不分摊,如 60.00=60%) | | **AllocationDeptId** | BIGINT | | 分摊目标部门 ERP ID | | **AllocationProjectId** | BIGINT | | 分摊目标项目 ERP ID | | **SortOrder** | INT | ✅ | DEFAULT 1 | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | **索引**:`IX_ExpenseDetail_ExpenseId` (ExpenseId, SortOrder) INCLUDE (ExpenseDate, ExpenseType, ExpenseDesc, Amount, TotalAmount, InvoiceType) **多币种说明**:OA 表单选择币种后从 ERP 汇率表自动填 `ExchangeRate`,`BaseAmount` 由 .NET 服务端计算。报表和预算校验均以 `BaseAmount`(本币)为准。 **分摊说明**:`AllocationPercent = NULL` 时不参与分摊。若一笔费用拆到多个部门/项目,前端生成多行(如一行 60%→DeptA,一行 40%→DeptB),`AllocationDeptId` + `AllocationProjectId` 指定分摊目标。 --- ### 3.3 加班申请 #### 3.3.1 Overtime **变更说明**:`ApplicantId`、`DeptId` 改为 ERP ID,新增审批字段。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **ApplicationNo** | VARCHAR(30) | ✅ | UNIQUE,单号 JB-YYYYMMDD-XXX | | **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.00~1.00) | | **StartTime** | DATETIME | | | | **EndTime** | DATETIME | | | | **NetOtHours** | DECIMAL(4,1) | ✅ | DEFAULT 0 | | **Reason** | NVARCHAR(500) | ✅ | | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | | **ApprovalInstanceId** | VARCHAR(50) | | **新增** | | **ApprovalStatus** | VARCHAR(20) | | **新增** | | **PreviousInstanceIds** | VARCHAR(MAX) | | **新增** | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_Overtime_List` (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType) --- ### 3.4 用车申请 #### 3.4.1 Vehicle **变更说明**:`ApplicantId`、`DeptId` 改为 ERP ID。`VehicleId` 保持 FK 到 OA 自管的 `SysVehicle`。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **ApplicationNo** | VARCHAR(30) | ✅ | UNIQUE,单号 YC-YYYYMMDD-XXX | | **ApplicantId** | BIGINT | ✅ | ERP 用户 ID | | **DeptId** | BIGINT | ✅ | ERP 部门 ID | | **VehicleId** | BIGINT | | 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 | | 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`) | | **ApprovalInstanceId** | VARCHAR(50) | | **新增** | | **ApprovalStatus** | VARCHAR(20) | | **新增** | | **PreviousInstanceIds** | VARCHAR(MAX) | | **新增** | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**: - `IX_Vehicle_List` (ApplicantId, Status, 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 | ✅ | PK | | **ApplicationId** | BIGINT | ✅ | FK → Vehicle.Id | | **UserId** | BIGINT | | ERP 用户 ID(内部同事,可空) | | **PassengerName** | NVARCHAR(50) | ✅ | 同行人姓名 | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_VehiclePassenger_AppId` (ApplicationId, IsDeleted) INCLUDE (UserId, PassengerName) #### 3.4.3 SysVehicle — 车辆池(OA 自管,不变) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **LicensePlate** | NVARCHAR(20) | ✅ | UNIQUE | | **VehicleType** | VARCHAR(20) | ✅ | sedan / suv / mpv / van | | **Brand** | NVARCHAR(50) | | | | **Seats** | INT | | | | **DriverName** | NVARCHAR(50) | | | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'idle' | | **IsActive** | BIT | ✅ | DEFAULT 1 | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | --- ### 3.5 外勤日志 #### 3.5.1 OutingLog **变更说明**:`SalespersonId`、`DeptId` 改为 ERP ID。`CustomerId` 改为 ERP 客户 ID(非 FK)。外勤日志不走审批流,不需要审批字段。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **VisitNo** | VARCHAR(30) | ✅ | UNIQUE,单号 VST-YYYYMMDD-XXX | | **SalespersonId** | BIGINT | ✅ | ERP 用户 ID | | **DeptId** | BIGINT | ✅ | ERP 部门 ID | | **CustomerId** | BIGINT | | **变更**:ERP 客户 ID(非 FK) | | **CustomerName** | NVARCHAR(200) | ✅ | 客户名冗余字段 | | **ContactId** | BIGINT | | ERP 客户联系人 ID | | **CheckInLongitude** | DECIMAL(10,6) | | | | **CheckInLatitude** | DECIMAL(10,6) | | | | **CheckInAddress** | NVARCHAR(500) | | 逆地理编码地址(只读) | | **VisitSummary** | NVARCHAR(2000) | ✅ | | | **NextPlan** | NVARCHAR(500) | | | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft'(draft / completed) | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **LastViewedTime** | 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 | ✅ | PK | | **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 | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_OutingLogComment_LogId` (LogId, IsDeleted) INCLUDE (CommenterId, RatingStars, CommentText, CreateTime) --- ### 3.6 公告管理 公告模块完全 OA 自闭环,无需审批。 #### 3.6.1 Announcement | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **Title** | NVARCHAR(200) | ✅ | | | **Content** | NVARCHAR(MAX) | ✅ | HTML/Markdown | | **Type** | VARCHAR(20) | ✅ | notice / policy / activity | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft'(draft / published) | | **PublisherId** | BIGINT | ✅ | ERP 用户 ID | | **PublishTime** | DATETIME | | 发布时间 | | **IsTop** | BIT | ✅ | DEFAULT 0 | | **PrivateLevel** | INT | ✅ | DEFAULT 0(0=全员,1=按部门,2=按用户) | | **ExpiryDate** | DATETIME | | 过期时间(NULL=永不过期) | | **CreateTime** | DATETIME | ✅ | | | **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 — 发布范围 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **AnnouncementId** | BIGINT | ✅ | FK → Announcement.Id | | **TargetType** | VARCHAR(10) | ✅ | dept / user | | **TargetId** | BIGINT | ✅ | ERP 部门 ID 或 ERP 用户 ID(多态,非 FK) | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | #### 3.6.3 AnnouncementReadLog — 触达审计 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **AnnouncementId** | BIGINT | ✅ | FK → Announcement.Id | | **UserId** | BIGINT | ✅ | ERP 用户 ID | | **IsRead** | BIT | ✅ | DEFAULT 0 | | **ReadTime** | DATETIME | | | | **IsUrged** | BIT | ✅ | DEFAULT 0 | | **LastUrgeTime** | DATETIME | | | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | **约束**:UNIQUE (AnnouncementId, UserId) **索引**:`IX_Announcement_Ding` (AnnouncementId, IsRead, IsUrged) INCLUDE (UserId, ReadTime) --- ### 3.7 借款/备用金 #### 3.7.1 Loan — 借款/备用金主表 借款需走审批流。报销时可冲销未还借款(多退少补)。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **LoanNo** | VARCHAR(30) | ✅ | UNIQUE,单号 JK-YYYYMMDD-XXX | | **ApplicantId** | BIGINT | ✅ | ERP 用户 ID | | **DeptId** | BIGINT | ✅ | ERP 部门 ID | | **LoanType** | VARCHAR(20) | ✅ | imprest(备用金)/ travel_advance(差旅借款) | | **Amount** | DECIMAL(18,2) | ✅ | 借款金额 | | **Purpose** | NVARCHAR(500) | ✅ | 借款事由 | | **RepaymentStatus** | VARCHAR(20) | ✅ | DEFAULT 'outstanding' | | **Status** | VARCHAR(20) | ✅ | DEFAULT 'draft' | | **ApprovalInstanceId** | VARCHAR(50) | | ERP 审批实例 ID | | **ApprovalStatus** | VARCHAR(20) | | | | **PreviousInstanceIds** | VARCHAR(MAX) | | | | **CreateTime** | DATETIME | ✅ | | | **UpdateTime** | DATETIME | | | | **IsDeleted** | BIT | ✅ | DEFAULT 0 | **索引**:`IX_Loan_List` (ApplicantId, Status, CreateTime DESC) INCLUDE (LoanNo, Amount, LoanType, RepaymentStatus) #### 3.7.2 LoanRepayment — 还款/冲销记录 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **LoanId** | BIGINT | ✅ | FK → Loan.Id | | **ExpenseId** | BIGINT | | 关联报销单(报销冲销时填) | | **RepaymentType** | VARCHAR(20) | ✅ | expense_offset(报销冲销)/ cash_return(现金还款)/ bank_transfer(银行转账) | | **Amount** | DECIMAL(18,2) | ✅ | 本次还款金额 | | **CreateTime** | DATETIME | ✅ | | **索引**:`IX_LoanRepayment_LoanId` (LoanId) INCLUDE (ExpenseId, RepaymentType, Amount) **还款逻辑**: - Sum(LoanRepayment.Amount) < Loan.Amount → outstanding - Sum >= Loan.Amount → fully_repaid - 报销提交时自动匹配未还借款 → 报销金额先冲销借款 → 余款退/补 - 报销金额 < 借款余额 → 差额部分员工需现金/转账补还 --- ### 3.8 基础字典 #### 3.7.1 SysCostCategory — 费用类别字典 OA 自管,为报销/申请表单的费用类别下拉提供数据源,支持二级分类。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **CategoryName** | NVARCHAR(50) | ✅ | | | **CategoryCode** | VARCHAR(30) | ✅ | UNIQUE | | **ParentId** | BIGINT | | FK → SysCostCategory.Id(自引用) | | **BizScope** | VARCHAR(20) | ✅ | DEFAULT 'both'(expense_apply / expense / both) | | **ExpenseType** | VARCHAR(20) | | 绑定费用大类 | | **SortOrder** | INT | ✅ | DEFAULT 0 | | **IsActive** | BIT | ✅ | DEFAULT 1 | | **CreateTime** | DATETIME | ✅ | | | **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 — 工作台轮播图 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | **Id** | BIGINT | ✅ | PK | | **ImageUrl** | VARCHAR(500) | ✅ | | | **Title** | NVARCHAR(100) | | | | **LinkUrl** | VARCHAR(500) | | 可空,为空则不可点击 | | **SortOrder** | INT | ✅ | DEFAULT 0 | | **IsActive** | BIT | ✅ | DEFAULT 1 | | **CreateTime** | DATETIME | ✅ | | | **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 本地存储。`ApprovalInstanceId` + `ApprovalStatus` + `PreviousInstanceIds` 三个字段的作用: | 字段 | 作用 | 更新时机 | |------|------|---------| | `ApprovalInstanceId` | 关联 ERP 审批实例 | 提交审批时写入;驳回重新提交时覆盖 | | `ApprovalStatus` | 列表页秒开缓存 | 提交/审批操作后写入;下拉刷新时从 ERP 校准 | | `PreviousInstanceIds` | 历史追溯 | 驳回/撤回重新提交时追加旧 instanceId | **状态同步流程**: ``` 列表页 → 读 ApprovalStatus(缓存,秒开) → 后台静默调 .NET → ERP 刷新 详情页 → 打开时实时调 .NET → ERP 校准 审批操作 → OA 调 .NET → ERP 执行 → 返回结果 → 写缓存 ``` ### 4.3 事前申请-报销联动 `UsageStatus` 由 .NET 服务端在报销单状态变更时自动重算(逻辑不变): | 已报总额 | UsageStatus | |----------|-------------| | = 0 | unused | | > 0 且 < EstimatedAmount | partially_used | | ≥ EstimatedAmount | fully_used | ### 4.4 单据编号生成 .NET 服务端使用 `sp_getapplock` + `SELECT MAX(ApplicationNo)` 原子生成,逻辑不变。 ### 4.5 级联删除(软删除) | 主表 | 子表 | 方式 | |------|------|------| | ExpenseApplication | ExpenseAppDetail, Attachment (BizType='expense_apply') | 主表软删除时同步子表软删除 | | Expense | ExpenseDetail, ExpenseApplicationMapping, Attachment (BizType='expense') | 同上 | | OutingLog | OutingLogComment, Attachment (BizType='outing_log') | 同上 | | Announcement | AnnouncementTarget, AnnouncementReadLog, Attachment (BizType='announcement') | 同上 | | Loan | LoanRepayment | 同上 | ### 4.6 收款银行默认回写 报销单首次提交审批通过后(`Status` 变为 `approved`),.NET 服务端将收款银行信息回写到 ERP 用户扩展属性。 ### 4.7 外勤日志新点评红点 `OutingLogComment.CreateTime > COALESCE(LastViewedTime, '1900-01-01')` 且 `CommenterId != SalespersonId` 时显示"新点评"标记。 ### 4.8 CHECK 约束 **Overtime**: - `CHECK (StartTime IS NULL OR EndTime IS NULL OR StartTime < EndTime)` - `CHECK (CompLeaveRatio IS NULL OR (CompLeaveRatio > 0 AND CompLeaveRatio < 1))` **Vehicle**: - `CHECK (StartTime IS NULL OR EndTime IS NULL OR StartTime < EndTime)` - `CHECK (EndOdometer IS NULL OR StartOdometer IS NULL OR EndOdometer > StartOdometer)` **OutingLogComment**: - `CHECK (RatingStars IS NULL OR (RatingStars >= 1 AND RatingStars <= 5))` ### 4.9 公告触达初始化 管理员发布公告(`Status` 从 `draft` → `published`)时,.NET 服务端根据 `PrivateLevel` 批量写入 `AnnouncementReadLog`(`IsRead=0`)。草稿状态不触发。新员工入职不回溯历史公告。 ### 4.10 外勤日志新客户动态创建 业务员输入不存在的客户名称时,提交时 .NET 服务端在同一事务内:1) 调 ERP 创建客户 → 获取新 ID;2) 回写 `OutingLog.CustomerId`。 ### 4.11 ERP 集成:预算/项目/科目/成本/标准 以下能力全部通过 .NET 服务端适配器层对接 ERP,OA 本地不存储: | 能力 | 适配器 | OA 调用的典型场景 | |------|--------|-----------------| | 预算余额查询 | BudgetService | 事前申请表单:选项目+科目后展示可用余额 | | 预算冻结/扣减/释放 | BudgetService | 提交审批/通过/拒绝/撤回时自动处理 | | 项目列表/级联 | ProjectService | 表单下拉:选项目→加载下级的预算科目 | | 预算科目列表 | SubjectService | 同上 | | 成本中心列表 | CostCenterService | 报销表单成本中心下拉 | | 费用标准查询 | StandardService | 提交校验:住宿是否超 500/晚、高铁是否二等座 | | 汇率表查询 | ExchangeRateService | 报销明细选外币→自动填汇率 | **ERP 无某能力时**:该 Adapter 返回空/不限,OA 前端自动隐藏对应区块。例如 ERP 无预算 → 不展示余额、不校验超支。 ### 4.12 费用标准管控 员工提交报销时,.NET 服务端按费用类型+城市级别+员工级别查询 ERP 费用标准,超标明细行标红 + 自动触发超标特批审批链。标准表在 ERP 维护,常见维度: ``` { expenseType, cityLevel, employeeLevel, maxAmount, maxQuantity } 例:{ hotel, 一线城市, 普通员工, 500, 1 } ``` ### 4.13 借款冲销 报销提交时 .NET 服务端自动查询该员工未还借款(`Loan WHERE ApplicantId=@uid AND RepaymentStatus IN ('outstanding','partially_repaid') AND Status='approved'`): - 报销金额 ≥ 借款余额 → 自动生成 LoanRepayment (expense_offset) 冲销全部借款,余额退还给员工 - 报销金额 < 借款余额 → 冲销部分借款,剩余借款仍需归还,`RepaymentStatus` 保持 `outstanding` --- ## 5. 索引汇总 ```sql -- 事前申请列表 CREATE NONCLUSTERED INDEX IX_ExpenseApp_List ON ExpenseApplication (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, EstimatedAmount, Purpose, ExpenseType, UsageStatus); -- 事前申请一键导入 CREATE NONCLUSTERED INDEX IX_ExpenseApp_Import ON ExpenseApplication (ApplicantId, Status, UsageStatus) WHERE Status = 'approved' AND UsageStatus IN ('unused', 'partially_used'); -- 费用报销列表 CREATE NONCLUSTERED INDEX IX_Expense_List ON Expense (ApplicantId, Status, CreateTime DESC) INCLUDE (TotalAmount, ReportNo, PaymentStatus); -- 报销关联申请 CREATE NONCLUSTERED INDEX IX_Expense_SourceApp ON Expense (SourceApplicationId) WHERE SourceApplicationId IS NOT NULL; -- 加班列表 CREATE NONCLUSTERED INDEX IX_Overtime_List ON Overtime (ApplicantId, Status, CreateTime DESC) INCLUDE (ApplicationNo, OtType, NetOtHours, CompensationType); -- 用车列表 CREATE NONCLUSTERED INDEX IX_Vehicle_List ON Vehicle (ApplicantId, Status, 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); -- OA 用户权限 CREATE UNIQUE NONCLUSTERED INDEX UX_UserPermission ON OaUserPermission (UserId, PermissionId) WHERE IsDeleted = 0; -- 权限变更审计 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_Banner_Active ON SysBanner (IsActive, SortOrder) INCLUDE (ImageUrl, Title, LinkUrl) WHERE IsDeleted = 0; -- 统一附件查询 CREATE NONCLUSTERED INDEX IX_Attachment_Biz ON Attachment (BizType, BizId, IsDeleted) INCLUDE (FileName, FileUrl, FileType, FileSize, DetailId, SortOrder); -- 申请↔报销多对多 CREATE NONCLUSTERED INDEX IX_AppMapping_AppId ON ExpenseApplicationMapping (ApplicationId) INCLUDE (ExpenseId, ImportedAmount); -- 借款列表 CREATE NONCLUSTERED INDEX IX_Loan_List ON Loan (ApplicantId, Status, CreateTime DESC) INCLUDE (LoanNo, Amount, LoanType, RepaymentStatus); -- 还款记录 CREATE NONCLUSTERED INDEX IX_LoanRepayment_LoanId ON LoanRepayment (LoanId) INCLUDE (ExpenseId, RepaymentType, Amount); -- 申请变更记录 CREATE NONCLUSTERED INDEX IX_AppChange_AppId ON ExpenseApplicationChange (ApplicationId, CreateTime DESC); -- 外勤点评 CREATE NONCLUSTERED INDEX IX_OutingLogComment_LogId ON OutingLogComment (LogId, IsDeleted) INCLUDE (CommenterId, RatingStars, CommentText, CreateTime); ``` --- ## 6. 枚举取值 ### 6.1 审批状态(OA 缓存 — ApprovalStatus) | 值 | 含义 | 适用表 | |----|------|--------| | `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 | ### 6.2 付款状态(PaymentStatus) | 值 | 含义 | |----|------| | `unpaid` | 未付款 | | `paying` | 付款中 | | `paid` | 已付款 | ### 6.3 费用类型(ExpenseTypes — 逗号分隔,支持多选) | 值 | 含义 | |----|------| | `travel` | 差旅费 | | `entertainment` | 业务招待费 | | `procurement` | 日常采购 | | `activity` | 活动经费 | | `office` | 办公费 | | `meeting` | 会议费 | | `training` | 培训费 | 例:`"travel,office"` 表示一次申请同时涵盖差旅费和办公费。 ### 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` | 无发票 | 允许 NULL | ### 6.9 加班类型(OtType) | 值 | 含义 | |----|------| | `workday` | 工作日加班 | | `weekend` | 休息日加班 | | `holiday` | 法定节假日加班 | ### 6.10 补偿方式(CompensationType) | 值 | 含义 | |----|------| | `overtime_pay` | 全部结算加班费 | | `comp_leave` | 全部转为调休 | | `mixed` | 混合模式 | ### 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 对公/对私(PaymentType) | 值 | 含义 | |----|------| | `personal` | 对私报销(还给员工) | | `corporate` | 对公付款(付给供应商) | ### 6.19 借款类型(LoanType) | 值 | 含义 | |----|------| | `imprest` | 备用金 | | `travel_advance` | 差旅借款 | ### 6.20 还款状态(RepaymentStatus) | 值 | 含义 | |----|------| | `outstanding` | 未还 | | `partially_repaid` | 部分已还 | | `fully_repaid` | 已还清 | | `written_off` | 已核销 | ### 6.21 还款方式(RepaymentType) | 值 | 含义 | |----|------| | `expense_offset` | 报销冲销 | | `cash_return` | 现金还款 | | `bank_transfer` | 银行转账 | ### 6.22 申请变更类型(ChangeType) | 值 | 含义 | |----|------| | `amount_increase` | 追加金额 | | `amount_decrease` | 减少金额 | | `detail_add` | 新增明细行 | | `detail_remove` | 删除明细行 | | `date_extend` | 延期 | ### 6.23 BOOLEAN 语义 | 字段 | 0 的含义 | 1 的含义 | |------|---------|---------| | `IsDeleted` | 正常 | 已软删除 | | `IsActive` | 停用/禁用 | 启用 | | `IsRead` | 未读 | 已读 | | `IsTop` | 普通 | 置顶 | | `IsUrged` | 未催办 | 已催办 | | `IsInvoiceVerified` | 未查验 | 已查验 | | `IsTaxIdMatched` | 未匹配 | 已匹配 | | `IsCategoryCompliant` | 不合规 | 合规 | ### 6.24 权限模块(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.25 权限变更类型(OaPermissionChangeLog.ChangeType) | 值 | 含义 | |----|------| | `assign` | 赋予权限 | | `revoke` | 移除权限 | | `toggle_active` | 启用/禁用 | --- ## 7. 常用查询 / 代码示例 > 以下示例基于 **Dapper + ADO.NET**(.NET Framework 4.8 服务端)。ERP 侧的数据(用户/部门/客户/审批)不在此处查询,均通过 API 获取。 ### 7.1 报销单详情查询(OA 本地) ```csharp const string sql = @" SELECT e.Id, e.ReportNo, e.TotalAmount, e.Purpose, e.BankName, e.AccountName, e.BankAccount, e.IsInvoiceVerified, e.IsTaxIdMatched, e.IsCategoryCompliant, e.BankTransferNo, e.VoucherNo, e.Status, e.PaymentStatus, e.ApplicationDate, e.CreateTime, e.ApprovalInstanceId, e.ApprovalStatus, e.PreviousInstanceIds, e.SourceApplicationId, ea.ApplicationNo AS SourceApplicationNo FROM Expense e LEFT JOIN ExpenseApplication ea ON ea.Id = e.SourceApplicationId AND ea.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.CurrencyCode, ed.ExchangeRate, ed.BaseAmount, ed.AllocationPercent, ed.AllocationDeptId, ed.AllocationProjectId, 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 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 Id, FileName, FileUrl, FileType, FileSize, DetailId, SortOrder FROM Attachment WHERE BizType = 'expense' AND BizId = @expenseId AND IsDeleted = 0 ORDER BY SortOrder, CreateTime; "; // 注意:申请人姓名、部门名称通过 .NET API GET /api/user/{ApplicantId} 获取 // 审批时间线通过 .NET API GET /api/oa/approval/timeline?bizType=expense&bizId={id} 获取 ``` ### 7.2 UsageStatus 联动更新 ```csharp public async Task UpdateUsageStatusAsync( IDbConnection conn, IDbTransaction tx, long applicationId) { const string sql = @" UPDATE ea SET ea.UsageStatus = CASE WHEN ISNULL(SUM(eam.ImportedAmount), 0) = 0 THEN 'unused' WHEN ISNULL(SUM(eam.ImportedAmount), 0) >= ea.EstimatedAmount THEN 'fully_used' ELSE 'partially_used' END, ea.UpdateTime = GETDATE() FROM ExpenseApplication ea LEFT JOIN ExpenseApplicationMapping eam ON eam.ApplicationId = ea.Id LEFT JOIN Expense e ON e.Id = eam.ExpenseId AND e.IsDeleted = 0 AND e.Status != 'draft' WHERE ea.Id = @applicationId GROUP BY ea.Id, ea.EstimatedAmount;"; await conn.ExecuteAsync(sql, new { applicationId }, tx); } ``` ### 7.3 单据编号原子生成 ```csharp public async Task 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(sql, new { pattern }, tx); int seq = 1; if (maxNo != null) { var seqStr = maxNo.Substring(maxNo.Length - 3); seq = int.Parse(seqStr) + 1; } return $"{prefix}-{dateStr}-{seq:D3}"; } // 注意:以上逻辑适用于所有单据类型,替换表名和前缀即可 ``` ### 7.4 待审批列表(调 .NET API,非本地 SQL 查询) 审批数据由 ERP 引擎管理,OA 本地不存储。经理端"待我审批"通过 .NET 服务端 API 获取: ``` GET /api/oa/approval/pending?userId={erpUserId}&bizType=&page=1 响应示例: { "items": [ { "bizType": "expense", "bizId": 12345, "bizNo": "BX-20260603-001", "amount": 1580.00, "summary": "差旅费报销", "submitTime": "2026-06-03T10:30:00", "applicantName": "张三", "deptName": "销售部" } ], "total": 8 } ``` ### 7.5 OA 权限校验示例 ```csharp // 判断用户是否有某个权限点 public async Task HasPermissionAsync( IDbConnection conn, long erpUserId, string permissionCode) { const string sql = @" SELECT COUNT(1) FROM OaUserPermission up INNER JOIN OaPermission p ON p.Id = up.PermissionId WHERE up.UserId = @erpUserId AND p.PermissionCode = @permissionCode AND up.IsDeleted = 0"; var count = await conn.ExecuteScalarAsync(sql, new { erpUserId, permissionCode }); return count > 0; } // 获取用户所有权限点 public async Task> GetUserPermissionsAsync( IDbConnection conn, long erpUserId) { const string sql = @" SELECT p.PermissionCode FROM OaUserPermission up INNER JOIN OaPermission p ON p.Id = up.PermissionId WHERE up.UserId = @erpUserId AND up.IsDeleted = 0"; var permissions = await conn.QueryAsync(sql, new { erpUserId }); return permissions.ToList(); } ``` --- > **文档版本**:v1.0 | 日期:2026-06-03