# TBOSS OA 模块 — API 接口文档

> 版本：v1.0 | 日期：2026-06-04 | Base URL：`https://{host}/api`

---

## 1. 通用规范

### 1.1 请求头

```
Content-Type: application/json
Authorization: Bearer {token}    ← 由宿主 App 注入
X-User-Id: {erpUserId}           ← 当前登录用户 ERP ID
```

### 1.2 统一响应格式

```json
{
  "code": 0,
  "message": "success",
  "data": { ... },
  "timestamp": 1717401600
}
```

### 1.3 分页响应

```json
{
  "code": 0,
  "data": {
    "items": [ ... ],
    "total": 128,
    "page": 1,
    "pageSize": 20
  }
}
```

### 1.4 错误码

| code | 含义 | 处理方式 |
|------|------|---------|
| 0 | 成功 | — |
| 400 | 参数校验失败 | Toast 提示具体字段错误 |
| 401 | 未授权 / Token 过期 | 触发宿主重新登录 |
| 403 | 无 OA 操作权限 | Toast "无此操作权限" |
| 404 | 资源不存在 | TDEmpty 或 Toast |
| 409 | 并发冲突 (CONCURRENCY_CONFLICT / APPROVAL_CONFLICT) | TDDialog 提示刷新 |
| 422 | 业务校验失败 | 展示具体错误信息 |
| 500 | 服务器内部错误 | Toast "服务器繁忙，请稍后重试" |
| 504 | ERP 审批接口超时 | Toast "审批服务暂不可用，请稍后重试" |

### 1.5 枚举值

所有状态/类型字段使用英文标识字符串，前端负责国际化映射。完整枚举取值见 `tboss-oa-database.md` 第 6 节。

---

## 2. OA 权限 API

### 2.1 获取权限点列表

```
GET /api/oa/permissions
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "permissions": [
      {
        "id": 1,
        "code": "oa.expense.apply",
        "name": "发起报销",
        "module": "expense",
        "sortOrder": 1
      },
      {
        "id": 2,
        "code": "oa.expense.view_own",
        "name": "查看自己的报销单",
        "module": "expense",
        "sortOrder": 2
      }
    ]
  }
}
```

### 2.2 获取用户权限

```
GET /api/oa/user-permissions?userId={erpUserId}
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "userId": 1001,
    "permissions": ["oa.expense.apply", "oa.expense.view_own", "oa.announcement.view"],
    "presetRoles": ["employee"]
  }
}
```

### 2.3 保存用户权限

```
PUT /api/oa/user-permissions
```

**请求**：
```json
{
  "userId": 1001,
  "permissionCodes": [
    "oa.expense.apply",
    "oa.expense.view_dept",
    "oa.expense.approve",
    "oa.outing_log.comment"
  ]
}
```

**响应**：
```json
{
  "code": 0,
  "message": "权限已更新"
}
```

**错误**：
| code | 场景 |
|------|------|
| 403 | 无 `oa.admin.permissions` 权限 |
| 422 | 试图移除自己的管理员权限 / 移除最后一名管理员 |

### 2.4 权限变更日志

```
GET /api/oa/permission-changelog?userId={erpUserId}&page=1&pageSize=20
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "id": 1,
        "targetUserId": 1001,
        "operatorId": 2001,
        "operatorName": "管理员王五",
        "changeType": "assign",
        "beforePermissions": ["oa.expense.apply", "oa.expense.view_own"],
        "afterPermissions": ["oa.expense.apply", "oa.expense.view_own", "oa.expense.approve"],
        "createTime": "2026-06-03T10:30:00"
      }
    ],
    "total": 5,
    "page": 1
  }
}
```

---

## 3. 业务单据 API

### 3.1 通用说明

`{bizType}` 取值：`expense-apply` | `expense` | `overtime` | `vehicle`

### 3.2 提交单据（新建 + 发起审批）

```
POST /api/oa/{bizType}/submit
```

#### 费用申请

**请求**：
```json
{
  "id": null,
  "urgency": "normal",
  "expenseTypes": "travel,office",
  "purpose": "北京客户拜访差旅费预估",
  "projectId": 100,
  "budgetSubjectId": 5,
  "isTaxIncluded": false,
  "validUntil": "2026-07-03",
  "referenceNo": "HT-2026-0089",
  "applicationDate": "2026-06-03",
  "estimatedStartDate": "2026-06-10",
  "estimatedEndDate": "2026-06-12",
  "isOvernight": true,
  "transportType": "high_speed_rail",
  "entertainmentTarget": null,
  "entertainmentLevel": null,
  "guestCount": null,
  "companionCount": null,
  "venue": null,
  "details": [
    {
      "expenseCategory": "transport",
      "quantity": 2,
      "unitPrice": 550.00,
      "unit": "张",
      "remark": "往返高铁票",
      "sortOrder": 1
    },
    {
      "expenseCategory": "hotel",
      "quantity": 2,
      "unitPrice": 400.00,
      "unit": "晚",
      "remark": "住宿",
      "sortOrder": 2
    }
  ],
  "attachments": [
    { "fileName": "出差审批截图.png", "fileUrl": "https://oss/...", "fileType": "image", "fileSize": 204800 }
  ]
}
```

> **费用类型联动字段**：仅当 `expenseTypes` 含对应类型时前端展示并必填。`travel` 时需 `estimatedStartDate/EndDate/isOvernight/transportType`；`entertainment` 时需 `entertainmentTarget/Level/guestCount/companionCount/venue`（校验 `companionCount ≤ guestCount`）；`meeting` 时需 `estimatedStartDate/EndDate/venue`。数据库全部 NULL 允许，应用层按 expenseTypes 动态校验。

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 12345,
    "applicationNo": "BXSQ-20260603-001",
    "approvalInstanceId": "inst_erp_a_009832",
    "previousInstanceIds": []
  }
}
```

#### 费用报销

**请求**：
```json
{
  "id": null,
  "applicationMappings": [
    { "applicationId": 12300, "importedAmount": 5500.00 }
  ],
  "purpose": "北京客户拜访差旅费报销",
  "projectId": 100,
  "budgetSubjectId": 5,
  "costCenterId": 10,
  "applicationDate": "2026-06-03",
  "bankName": "中国工商银行",
  "accountName": "张三",
  "bankAccount": "6222021234567890123",
  "details": [
    {
      "expenseDate": "2026-06-10",
      "expenseType": "transport",
      "expenseDesc": "G123 北京南-上海虹桥",
      "amount": 523.58,
      "taxAmount": 26.42,
      "currencyCode": "CNY",
      "exchangeRate": 1.0000,
      "invoiceNo": "12345678",
      "invoiceCode": "011001900111",
      "invoiceType": "special",
      "taxRate": 0.0900,
      "sortOrder": 1
    }
  ],
  "attachments": [
    { "fileName": "高铁票.jpg", "fileUrl": "https://oss/...", "fileType": "image", "fileSize": 307200, "detailIndex": 0 }
  ]
}
```

> **导入申请**：`applicationMappings` 非必填，传入时项目/科目/成本中心自动从申请带入。直接新建时不传，需手动填写 `projectId`/`budgetSubjectId`。`costCenterId` 可选（ERP 无数据时不传）。`currencyCode` 默认 CNY，选外币时 `exchangeRate` 由服务端从 ERP 查询后填入，前端只传 `currencyCode`。`totalAmount`（价税合计）= amount + taxAmount 由服务端计算。`baseAmount` = totalAmount × exchangeRate 由服务端计算。

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 12348,
    "reportNo": "BX-20260603-001",
    "approvalInstanceId": "inst_erp_a_009834",
    "previousInstanceIds": []
  }
}
```

#### 加班申请

**请求**：
```json
{
  "id": null,
  "otType": "workday",
  "compensationType": "overtime_pay",
  "compLeaveRatio": null,
  "startTime": "2026-06-03T18:00:00",
  "endTime": "2026-06-03T22:00:00",
  "reason": "紧急上线支持，需加班完成部署"
}
```

> **混合模式**：`compensationType=mixed` 时 `compLeaveRatio` 必填，范围 0.10~0.90（步长 0.10），如 0.30 表示 30% 调休 + 70% 加班费。`NetOtHours` 由服务端自动扣除午休(12:00-13:00)和晚餐(18:00-18:30)盲区后计算。`applicationDate` 由服务端填写提交当日。

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 12346,
    "applicationNo": "JB-20260603-001",
    "netOtHours": 3.5,
    "approvalInstanceId": "inst_erp_a_009833",
    "previousInstanceIds": []
  }
}
```

#### 用车申请

**请求**：
```json
{
  "id": null,
  "vehicleId": 3,
  "purpose": "business",
  "reason": "前往客户现场演示产品",
  "origin": "公司总部",
  "originLongitude": 121.473701,
  "originLatitude": 31.230416,
  "destination": "客户公司",
  "destLongitude": 121.510000,
  "destLatitude": 31.250000,
  "startTime": "2026-06-05T09:00:00",
  "endTime": "2026-06-05T17:00:00",
  "passengerCount": 3,
  "passengers": [
    { "userId": 1002, "passengerName": "李四" },
    { "userId": null, "passengerName": "客户代表" }
  ]
}
```

> **排期冲突检测**：提交前应调 `GET /api/oa/vehicle/check-collision` 异步校验。冲突时前端锁死提交按钮。`applicationDate` 由服务端填写提交当日。

**响应**：同其他单据。

### 3.3 存草稿

```
PUT /api/oa/{bizType}/draft
```

请求体与提交相同，`id` 为已有草稿 ID（编辑已有草稿）或 null（新建草稿）。不触发审批。

**响应**：
```json
{
  "code": 0,
  "data": { "id": 12347 }
}
```

### 3.4 列表查询

```
GET /api/oa/expense-apply/list?status=pending&page=1&pageSize=20
GET /api/oa/expense/list?status=all&page=1
GET /api/oa/overtime/list?status=approved&page=1
GET /api/oa/vehicle/list?status=all&page=1
```

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | string | 否 | 审批单据：all / draft / pending / approved / rejected / withdrawn。报销额外：unpaid / paid。用车额外：returned |
| page | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 20 |
| scope | string | 否 | own(默认) / dept / all — 根据权限自动限定 |

**响应**（以报销列表为例）：
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "id": 100,
        "reportNo": "BX-20260603-001",
        "totalAmount": 1580.00,
        "purpose": "差旅费报销",
        "applicationDate": "2026-06-03",
        "status": "approved",
        "paymentStatus": "unpaid",
        "applicantName": "张三",
        "deptName": "销售部",
        "createTime": "2026-06-03T10:00:00"
      }
    ],
    "total": 42,
    "page": 1,
    "pageSize": 20
  }
}
```

**费用申请列表响应**（增量字段）：
```json
{
  "id": 200,
  "applicationNo": "BXSQ-20260603-001",
  "estimatedAmount": 1900.00,
  "purpose": "差旅费预估",
  "expenseTypes": "travel,office",
  "applicationDate": "2026-06-03",
  "status": "pending",
  "usageStatus": "unused",
  "applicantName": "张三",
  "deptName": "销售部",
  "createTime": "2026-06-03T09:00:00"
}
```

**加班列表响应**（增量字段）：
```json
{
  "id": 300,
  "applicationNo": "JB-20260603-001",
  "otType": "workday",
  "netOtHours": 3.5,
  "compensationType": "overtime_pay",
  "compLeaveRatio": null,
  "reason": "紧急上线支持",
  "applicationDate": "2026-06-03",
  "status": "approved",
  "applicantName": "张三",
  "deptName": "销售部",
  "createTime": "2026-06-03T18:30:00"
}
```

**用车列表响应**（增量字段）：
```json
{
  "id": 400,
  "applicationNo": "YC-20260603-001",
  "vehicleId": 3,
  "licensePlate": "粤B12345",
  "purpose": "business",
  "origin": "公司总部",
  "destination": "客户公司",
  "startTime": "2026-06-05T09:00:00",
  "status": "approved",
  "applicantName": "张三",
  "deptName": "销售部",
  "createTime": "2026-06-03T11:00:00"
}
```

### 3.5 详情查询

```
GET /api/oa/expense-apply/detail/12345
GET /api/oa/expense/detail/100
GET /api/oa/overtime/detail/12346
GET /api/oa/vehicle/detail/200
```

**费用申请详情响应**：
```json
{
  "code": 0,
  "data": {
    "id": 12345,
    "applicationNo": "BXSQ-20260603-001",
    "applicantId": 1001,
    "applicantName": "张三",
    "deptId": 10,
    "deptName": "销售部",
    "applicationDate": "2026-06-03",
    "urgency": "normal",
    "expenseTypes": "travel,office",
    "purpose": "北京客户拜访差旅费预估",
    "projectId": 100,
    "projectName": "华东市场拓展",
    "budgetSubjectId": 5,
    "budgetSubjectName": "差旅费",
    "estimatedAmount": 1900.00,
    "isTaxIncluded": false,
    "validUntil": "2026-07-03",
    "referenceNo": "HT-2026-0089",
    "estimatedStartDate": "2026-06-10",
    "estimatedEndDate": "2026-06-12",
    "isOvernight": true,
    "transportType": "high_speed_rail",
    "entertainmentTarget": null,
    "entertainmentLevel": null,
    "guestCount": null,
    "companionCount": null,
    "venue": null,
    "status": "pending",
    "usageStatus": "unused",
    "approvalInstanceId": "inst_erp_a_009832",
    "previousInstanceIds": [],
    "details": [
      {
        "id": 600,
        "expenseCategory": "transport",
        "expenseCategoryName": "交通费",
        "quantity": 2,
        "unitPrice": 550.00,
        "unit": "张",
        "estimatedAmount": 1100.00,
        "remark": "往返高铁票",
        "sortOrder": 1
      },
      {
        "id": 601,
        "expenseCategory": "hotel",
        "expenseCategoryName": "住宿费",
        "quantity": 2,
        "unitPrice": 400.00,
        "unit": "晚",
        "estimatedAmount": 800.00,
        "remark": "住宿",
        "sortOrder": 2
      }
    ],
    "attachments": [
      { "id": 20, "fileName": "出差审批截图.png", "fileUrl": "https://oss/...", "fileType": "image", "fileSize": 204800 }
    ],
    "createTime": "2026-06-03T09:00:00",
    "updateTime": "2026-06-03T09:00:00"
  }
}
```

**费用报销详情响应**：
```json
{
  "code": 0,
  "data": {
    "id": 100,
    "reportNo": "BX-20260603-001",
    "applicantId": 1001,
    "applicantName": "张三",
    "deptId": 10,
    "deptName": "销售部",
    "applicationDate": "2026-06-03",
    "projectId": 100,
    "projectName": "华东市场拓展",
    "budgetSubjectId": 5,
    "budgetSubjectName": "差旅费",
    "costCenterId": 10,
    "costCenterName": "销售部-华东区",
    "totalAmount": 1580.00,
    "purpose": "差旅费报销",
    "bankName": "中国工商银行",
    "accountName": "张三",
    "bankAccount": "6222021234567890123",
    "isInvoiceVerified": false,
    "isTaxIdMatched": false,
    "isCategoryCompliant": false,
    "bankTransferNo": null,
    "voucherNo": null,
    "status": "approved",
    "paymentStatus": "unpaid",
    "approvalInstanceId": "inst_erp_a_009832",
    "previousInstanceIds": [],
    "applicationMappings": [
      {
        "applicationId": 12300,
        "applicationNo": "BXSQ-20260520-003",
        "importedAmount": 5500.00
      }
    ],
    "details": [
      {
        "id": 500,
        "expenseDate": "2026-06-10",
        "expenseType": "transport",
        "expenseTypeName": "交通费",
        "expenseDesc": "G123 高铁票",
        "amount": 523.58,
        "taxAmount": 26.42,
        "totalAmount": 550.00,
        "currencyCode": "CNY",
        "exchangeRate": 1.0000,
        "baseAmount": 550.00,
        "invoiceNo": "12345678",
        "invoiceCode": "011001900111",
        "invoiceType": "special",
        "taxRate": 0.0900,
        "sortOrder": 1
      }
    ],
    "attachments": [
      { "id": 10, "fileName": "高铁票.jpg", "fileUrl": "https://oss/...", "fileType": "image", "fileSize": 307200, "detailId": 500 }
    ],
    "createTime": "2026-06-03T10:00:00",
    "updateTime": "2026-06-03T15:30:00"
  }
}
```

**加班详情响应**：
```json
{
  "code": 0,
  "data": {
    "id": 12346,
    "applicationNo": "JB-20260603-001",
    "applicantId": 1001,
    "applicantName": "张三",
    "deptId": 10,
    "deptName": "销售部",
    "otType": "workday",
    "compensationType": "overtime_pay",
    "compLeaveRatio": null,
    "startTime": "2026-06-03T18:00:00",
    "endTime": "2026-06-03T22:00:00",
    "netOtHours": 3.5,
    "reason": "紧急上线支持，需加班完成部署",
    "status": "approved",
    "approvalInstanceId": "inst_erp_a_009833",
    "previousInstanceIds": [],
    "createTime": "2026-06-03T18:30:00",
    "updateTime": "2026-06-03T18:30:00"
  }
}
```

**用车详情响应**：
```json
{
  "code": 0,
  "data": {
    "id": 200,
    "applicationNo": "YC-20260603-001",
    "applicantId": 1001,
    "applicantName": "张三",
    "deptId": 10,
    "deptName": "销售部",
    "vehicleId": 3,
    "licensePlate": "粤B12345",
    "vehicleType": "sedan",
    "brand": "丰田凯美瑞",
    "purpose": "business",
    "reason": "前往客户现场演示产品",
    "origin": "公司总部",
    "originLongitude": 121.473701,
    "originLatitude": 31.230416,
    "destination": "客户公司",
    "destLongitude": 121.510000,
    "destLatitude": 31.250000,
    "passengerCount": 3,
    "passengers": [
      { "userId": 1002, "passengerName": "李四" },
      { "userId": null, "passengerName": "客户代表" }
    ],
    "startTime": "2026-06-05T09:00:00",
    "endTime": "2026-06-05T17:00:00",
    "actualReturnTime": null,
    "startOdometer": null,
    "endOdometer": null,
    "actualCost": null,
    "costRemark": null,
    "status": "approved",
    "approvalInstanceId": "inst_erp_a_009835",
    "previousInstanceIds": [],
    "createTime": "2026-06-03T11:00:00",
    "updateTime": "2026-06-03T11:00:00"
  }
}
```

### 3.6 删除单据

```
DELETE /api/oa/expense/100
DELETE /api/oa/{bizType}/{id}
```

**约束**：仅 `draft` 状态可删除。软删除（`IsDeleted=1`）。

**响应**：
```json
{ "code": 0, "message": "已删除" }
```

**错误**：
| code | 场景 |
|------|------|
| 422 | 非 draft 状态不可删除 |

### 3.7 费用申请导入候选

```
GET /api/oa/expense-apply/import-candidates?applicantId=1001
```

**说明**：费用报销表单中"导入已通过的费用申请"的数据源。返回当前用户已通过且尚有可用额度（`UsageStatus IN ('unused', 'partially_used')`）的申请列表。

**响应**：
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "id": 12300,
        "applicationNo": "BXSQ-20260520-003",
        "estimatedAmount": 10000.00,
        "usedAmount": 4500.00,
        "availableAmount": 5500.00,
        "expenseTypes": "travel",
        "purpose": "北京出差预估",
        "projectId": 100,
        "projectName": "华东市场拓展",
        "budgetSubjectId": 5,
        "budgetSubjectName": "差旅费"
      }
    ]
  }
}
```

### 3.8 用车排期冲突检测

```
GET /api/oa/vehicle/check-collision?vehicleId=3&startTime=2026-06-05T09:00:00&endTime=2026-06-05T17:00:00&excludeId=
```

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| vehicleId | long | ✅ | 车牌 ID |
| startTime | datetime | ✅ | 出车时间 |
| endTime | datetime | ✅ | 还车时间 |
| excludeId | long | 否 | 编辑已有申请时排除自身 ID |

**响应（无冲突）**：
```json
{ "code": 0, "data": { "hasCollision": false } }
```

**响应（有冲突）**：
```json
{
  "code": 0,
  "data": {
    "hasCollision": true,
    "conflictApplicationNo": "YC-20260604-002",
    "conflictTimeRange": "2026-06-05 08:00 ~ 2026-06-05 12:00"
  }
}
```

### 3.9 还车登记

```
POST /api/oa/vehicle/200/return
```

**权限**：申请人本人。

**请求**：
```json
{
  "actualReturnTime": "2026-06-05T16:30:00",
  "startOdometer": 45678.50,
  "endOdometer": 45712.30,
  "actualCost": 85.00,
  "costRemark": "停车费30元，高速通行费55元"
}
```

**校验**：
- `endOdometer` ≥ `startOdometer`
- 仅 `status=approved` 可还车

**响应**：
```json
{
  "code": 0,
  "data": {
    "status": "returned",
    "mileage": 33.80,
    "actualCost": 85.00
  }
}
```

**后端操作**：更新 Vehicle 表还车字段 → `Status='returned'` → 车辆恢复 `SysVehicle.Status='idle'`。

### 3.10 财务核销

```
PUT /api/oa/expense/100/write-off
```

**权限**：需要 `oa.expense.mark_paid` 权限。

**请求**：
```json
{
  "isInvoiceVerified": true,
  "isTaxIdMatched": true,
  "isCategoryCompliant": true,
  "bankTransferNo": "BT20260603001",
  "voucherNo": "PZ-2026-0603-0015"
}
```

**校验**：
- 三项合规检查必须全部为 true
- `bankTransferNo` 和 `voucherNo` 必填
- 仅 `status=approved` 且 `paymentStatus=unpaid` 可核销

**响应**：
```json
{
  "code": 0,
  "data": {
    "paymentStatus": "paid",
    "message": "核销完成，已归档"
  }
}
```

**错误**：
| code | 场景 |
|------|------|
| 422 | 合规检查未全部勾选 / 流水号或凭证号为空 |
| 422 | 单据状态不满足核销条件 |

### 3.11 退回修改

```
POST /api/oa/expense/100/return-for-revision
```

**权限**：需要 `oa.expense.mark_paid` 权限。

**请求**：
```json
{
  "returnTo": "applicant",
  "reason": "发票验真未通过，请核实后重新提交"
}
```

`returnTo` 取值：`applicant`（退回员工重填）/ `approver`（退回经理重审）。

**响应**：
```json
{ "code": 0, "message": "已退回修改" }
```

---

## 4. 外勤日志 API

### 4.1 外勤日志列表

```
GET /api/oa/outing-log/list?status=all&page=1&pageSize=20
```

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | string | 否 | all / draft / completed |
| page | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 20 |
| scope | string | 否 | own(默认) / dept / all — 根据权限自动限定 |

**响应**：
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "id": 300,
        "visitNo": "VST-20260603-001",
        "salespersonId": 1001,
        "salespersonName": "张三",
        "deptName": "销售部",
        "customerId": 5001,
        "customerName": "北京科技有限公司",
        "checkInAddress": "北京市朝阳区建国路88号",
        "visitSummary": "与客户CTO进行了产品演示...",
        "status": "completed",
        "hasNewComment": true,
        "createTime": "2026-06-03T14:00:00"
      }
    ],
    "total": 15,
    "page": 1,
    "pageSize": 20
  }
}
```

> `hasNewComment`：`Comment.CreateTime > COALESCE(LastViewedTime, '1900-01-01')` 且评论者≠本人时为 true，前端展示橙色"新点评"标记。

### 4.2 外勤日志详情

```
GET /api/oa/outing-log/detail/300
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 300,
    "visitNo": "VST-20260603-001",
    "salespersonId": 1001,
    "salespersonName": "张三",
    "deptId": 10,
    "deptName": "销售部",
    "customerId": 5001,
    "customerName": "北京科技有限公司",
    "contactId": 6001,
    "contactName": "王总",
    "checkInLongitude": 116.407526,
    "checkInLatitude": 39.904030,
    "checkInAddress": "北京市朝阳区建国路88号",
    "visitSummary": "与客户CTO进行了产品演示，讨论了定制化需求...",
    "nextPlan": "下周三前送出详细方案和报价",
    "status": "completed",
    "attachments": [
      { "id": 30, "fileName": "现场_20260603_143021.jpg", "fileUrl": "https://oss/...", "fileType": "sign_in_photo", "fileSize": 2048000, "sortOrder": 1 }
    ],
    "comments": [
      {
        "id": 400,
        "commenterId": 2001,
        "commenterName": "王经理",
        "ratingStars": 4,
        "commentText": "拜访记录详实，客户需求梳理清晰。建议补充技术方案的可行性分析。",
        "createTime": "2026-06-03T16:00:00"
      }
    ],
    "createTime": "2026-06-03T14:00:00",
    "updateTime": "2026-06-03T14:00:00"
  }
}
```

### 4.3 存草稿

```
PUT /api/oa/outing-log/draft
```

请求体与提交相同（见 §4.4），`id` 为已有草稿 ID 或 null。不校验 GPS 和照片。

**响应**：
```json
{ "code": 0, "data": { "id": 301 } }
```

### 4.4 提交外勤日志

```
PUT /api/oa/outing-log/submit
```

**请求**：
```json
{
  "id": null,
  "customerId": 5001,
  "customerName": "北京科技有限公司",
  "checkInLongitude": 116.407526,
  "checkInLatitude": 39.904030,
  "checkInAddress": "北京市朝阳区建国路88号",
  "visitSummary": "与客户CTO进行了产品演示，讨论了定制化需求...",
  "nextPlan": "下周三前送出详细方案和报价",
  "attachments": [
    { "fileName": "现场_20260603_143021.jpg", "fileUrl": "https://oss/...", "fileType": "sign_in_photo", "fileSize": 2048000 }
  ]
}
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 300,
    "visitNo": "VST-20260603-001",
    "status": "completed"
  }
}
```

**校验**：
- GPS 定位不能为空
- 至少 1 张现场照片
- 客户名为空时自动创建 ERP 客户

### 4.5 标记已查看

```
PUT /api/oa/outing-log/300/view
```

无请求体。更新 `LastViewedTime = NOW()`，使列表页"新点评"红点消失。

### 4.6 主管点评

```
POST /api/oa/outing-log/300/comment
```

**请求**：
```json
{
  "ratingStars": 4,
  "commentText": "拜访记录详实，客户需求梳理清晰。建议补充技术方案的可行性分析。"
}
```

**权限**：需要 `oa.outing_log.comment` 权限。

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 400,
    "commenterId": 2001,
    "commenterName": "王经理",
    "ratingStars": 4,
    "commentText": "拜访记录详实，客户需求梳理清晰。建议补充技术方案的可行性分析。",
    "createTime": "2026-06-03T16:00:00"
  }
}
```

### 4.7 删除外勤日志

```
DELETE /api/oa/outing-log/{id}
```

**约束**：仅 `draft` 状态可删除。软删除（`IsDeleted=1`）。级联软删除附件。

**响应**：
```json
{ "code": 0, "message": "已删除" }
```

---

## 5. 审批代理 API

### 5.1 待审批列表

```
GET /api/oa/approval/pending?userId=2001&bizType=&page=1&pageSize=20
```

**参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| userId | long | ✅ | 审批人 ERP 用户 ID |
| bizType | string | 否 | 为空时返回所有类型 |
| page | int | 否 | |
| pageSize | int | 否 | |

**响应**：
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "bizType": "expense",
        "bizId": 100,
        "bizNo": "BX-20260603-001",
        "amount": 1580.00,
        "summary": "差旅费报销",
        "submitTime": "2026-06-03T10:00:00",
        "applicantId": 1001,
        "applicantName": "张三",
        "deptName": "销售部",
        "approvalLevel": 1
      },
      {
        "bizType": "overtime",
        "bizId": 12346,
        "bizNo": "JB-20260603-001",
        "amount": 3.5,
        "summary": "紧急上线支持",
        "submitTime": "2026-06-03T18:30:00",
        "applicantId": 1002,
        "applicantName": "李四",
        "deptName": "技术部",
        "approvalLevel": 2
      }
    ],
    "total": 8,
    "page": 1
  }
}
```

### 5.2 待审批数量

```
GET /api/oa/approval/pending-count?userId=2001
```

**响应**：
```json
{ "code": 0, "data": { "count": 8 } }
```

### 5.3 审批时间线

```
GET /api/oa/approval/timeline?bizType=expense&bizId=100
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "bizType": "expense",
    "bizId": 100,
    "currentStatus": "pending",
    "currentApproverName": "王经理",
    "nodes": [
      {
        "level": 0,
        "action": "submit",
        "approverName": "张三",
        "opinion": null,
        "approvalTime": "2026-06-03T10:00:00",
        "isValid": true
      },
      {
        "level": 1,
        "action": "pending",
        "approverName": "王经理",
        "opinion": null,
        "approvalTime": null,
        "isValid": true
      }
    ]
  }
}
```

### 5.4 审批动作

```
POST /api/oa/approval/action
```

**请求（同意）**：
```json
{
  "instanceId": "inst_erp_a_009832",
  "action": "approve",
  "opinion": "同意报销"
}
```

**请求（拒绝）**：
```json
{
  "instanceId": "inst_erp_a_009832",
  "action": "reject",
  "opinion": "发票金额与申请金额不符，请核实后重新提交"
}
```

**响应**：
```json
{
  "code": 0,
  "message": "审批成功",
  "data": { "newStatus": "approved" }
}
```

**校验**：
- `action=reject` 时 `opinion` 必填且 ≥5 个汉字
- `action=transfer` 时 `transferToUserId` 必填

### 5.5 撤回申请

```
POST /api/oa/approval/withdraw
```

**请求**：
```json
{
  "bizType": "expense",
  "bizId": 100,
  "instanceId": "inst_erp_a_009832"
}
```

**响应**：
```json
{ "code": 0, "message": "已撤回" }
```

**后端操作**：.NET 服务端调 ERP 撤回 → OA 更新 `Status='withdrawn'`，旧 `ApprovalInstanceId` 追加到 `PreviousInstanceIds`。

### 5.6 下属单据列表（经理版）

```
GET /api/oa/approval/subordinates?approverId=2001&bizType=expense&status=pending&page=1
```

**响应**：结构与列表查询相同，额外返回申请人姓名/部门。

---

## 6. 公告 API

### 6.1 公告列表

```
GET /api/oa/announcement/list?type=all&page=1&pageSize=20
```

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| type | string | 否 | all / notice / policy / activity。管理员额外支持 `draft`（我的草稿） |
| page | int | 否 | 默认 1 |
| pageSize | int | 否 | 默认 20 |

**响应**：
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "id": 50,
        "title": "关于2026年端午节放假安排的通知",
        "type": "activity",
        "status": "published",
        "publisherId": 2001,
        "publisherName": "管理员王五",
        "deptName": "行政部",
        "publishTime": "2026-06-03T14:00:00",
        "isTop": true,
        "isRead": false,
        "isExpired": false,
        "expiryDate": "2026-06-22T00:00:00"
      }
    ],
    "total": 12,
    "page": 1,
    "pageSize": 20
  }
}
```

> 排序：`isTop` 优先 → 未过期按 `publishTime` 倒序 → 已过期置灰垫底。`isRead` 来自 `AnnouncementReadLog.IsRead`，`isExpired` = `expiryDate < NOW()`。

### 6.2 公告详情

```
GET /api/oa/announcement/detail/50
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 50,
    "title": "关于2026年端午节放假安排的通知",
    "content": "<h2>放假时间</h2><p>6月19日（周五）至6月21日（周日）...</p>",
    "type": "activity",
    "status": "published",
    "publisherId": 2001,
    "publisherName": "管理员王五",
    "deptName": "行政部",
    "publishTime": "2026-06-03T14:00:00",
    "isTop": true,
    "privateLevel": 0,
    "expiryDate": "2026-06-22T00:00:00",
    "isExpired": false,
    "attachments": [
      { "id": 40, "fileName": "放假安排.pdf", "fileUrl": "https://oss/...", "fileType": "pdf", "fileSize": 512000 }
    ],
    "createTime": "2026-06-03T13:00:00",
    "updateTime": "2026-06-03T14:00:00"
  }
}
```

### 6.3 存草稿

```
PUT /api/oa/announcement/draft
```

请求体与发布相同（见 §6.4），不校验必填项。`Status='draft'`，仅创建者和管理员可见。

**响应**：
```json
{ "code": 0, "data": { "id": 51 } }
```

### 6.4 发布公告

```
POST /api/oa/announcement/publish
```

**请求（新建发布）**：
```json
{
  "id": null,
  "title": "关于2026年端午节放假安排的通知",
  "content": "<h2>放假时间</h2><p>6月19日（周五）至6月21日（周日）...</p>",
  "type": "activity",
  "isTop": false,
  "expiryDate": "2026-06-22T00:00:00",
  "privateLevel": 0,
  "targets": []
}
```

**请求（按部门发布）**：
```json
{
  "id": null,
  "title": "销售部Q2业绩考核通知",
  "content": "<p>...</p>",
  "type": "notice",
  "isTop": true,
  "privateLevel": 1,
  "targets": [
    { "targetType": "dept", "targetId": 10 }
  ]
}
```

**请求（按指定用户发布）**：
```json
{
  "id": null,
  "title": "销售部Q3考核安排",
  "content": "<p>...</p>",
  "type": "notice",
  "isTop": false,
  "privateLevel": 2,
  "targets": [
    { "targetType": "user", "targetId": 1001 },
    { "targetType": "user", "targetId": 1002 }
  ]
}
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "id": 50,
    "status": "published",
    "publishTime": "2026-06-03T14:00:00",
    "targetCount": 156
  }
}
```

**后端操作**：发布时异步初始化 `AnnouncementReadLog`（IsRead=0）。

### 6.5 标记已读

```
POST /api/oa/announcement/50/read
```

无请求体。停留 ≥2s 时调用。无权限要求。

### 6.6 DING 催办

```
POST /api/oa/announcement/50/ding
```

**权限**：需要 `oa.announcement.manage` 权限。

**响应**：
```json
{
  "code": 0,
  "data": { "urgedCount": 5 }
}
```

**后端操作**：查 `AnnouncementReadLog WHERE IsRead=0` → 更新 `IsUrged=1` → 调消息模块推送。

### 6.7 已读/未读统计

```
GET /api/oa/announcement/read-stats/50
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "readCount": 45,
    "unreadCount": 5,
    "readUsers": [
      { "userId": 1001, "userName": "张三", "deptName": "销售部", "readTime": "2026-06-03T14:05:00" }
    ],
    "unreadUsers": [
      { "userId": 1005, "userName": "赵六", "deptName": "技术部" }
    ]
  }
}
```

---

## 7. 报表 API

### 7.1 通用格式

```
GET /api/oa/report/{type}?range=month&startDate=2026-06-01&endDate=2026-06-30&userId={erpUserId}&deptId={deptId}
```

**通用参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| range | string | 否 | month / quarter / year。与 startDate+endDate 互斥，优先使用自定义日期 |
| startDate | string | 否 | 自定义开始日期 YYYY-MM-DD |
| endDate | string | 否 | 自定义结束日期 YYYY-MM-DD |
| userId | long | 否 | 个人报表时传入；部门/全公司报表时不传 |
| deptId | long | 否 | 部门报表时传入 |
| scope | string | 否 | own(默认) / dept / all — 根据角色权限自动限定 |

**type 取值**：`expense-apply` | `expense` | `overtime` | `vehicle` | `outing-log`

> **数据来源**：数值卡片和图表均来自 OA 本地业务表，.NET 按角色权限 + 时间范围聚合。审批状态统计通过 ERP 实时查询补充。预置时间段（month/quarter/year）缓存 5 分钟，自定义日期实时查询。

**通用响应结构**：
```json
{
  "code": 0,
  "data": {
    "cards": { ... },
    "chart": { ... },
    "memberBreakdown": [ ... ],
    "detailList": {
      "items": [ ... ],
      "total": 0,
      "page": 1
    }
  }
}
```

> `memberBreakdown` 仅经理角色返回（部门横向对比柱状图数据）。`detailList` 仅经理/财务/管理员返回（底部明细列表，分页）。

### 7.2 费用申请明细报表

```
GET /api/oa/report/expense-apply?range=year&userId=1001
```

**专属参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | string | 否 | all / approved / rejected / withdrawn |

**响应**：
```json
{
  "code": 0,
  "data": {
    "cards": {
      "yearTotalAmount": 28500.00,
      "monthCount": 3,
      "approvedCount": 2,
      "approvedAmount": 15000.00
    },
    "chart": {
      "labels": ["2025-07", "2025-08", "...", "2026-06"],
      "appliedAmounts": [1200, 3400, 0, 0, 5600, 2300, 4500, 1800, 2900, 0, 3800, 2000],
      "approvedAmounts": [1200, 3000, 0, 0, 0, 2300, 4000, 1800, 2900, 0, 3800, 2000]
    },
    "memberBreakdown": [
      { "userId": 1001, "userName": "张三", "appliedAmount": 15000.00, "approvedAmount": 12000.00 },
      { "userId": 1002, "userName": "李四", "appliedAmount": 8000.00, "approvedAmount": 8000.00 }
    ],
    "detailList": {
      "items": [
        {
          "id": 12345,
          "applicationNo": "BXSQ-20260603-001",
          "applicantName": "张三",
          "deptName": "销售部",
          "estimatedAmount": 1900.00,
          "expenseTypes": "travel,office",
          "status": "pending",
          "createTime": "2026-06-03T09:00:00"
        }
      ],
      "total": 42,
      "page": 1
    }
  }
}
```

### 7.3 费用报销明细报表

```
GET /api/oa/report/expense?range=year&userId=1001
```

**专属参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | string | 否 | all / approved / rejected |
| paymentStatus | string | 否 | all / unpaid / paid |

**响应**：
```json
{
  "code": 0,
  "data": {
    "cards": {
      "yearTotalPaid": 28500.00,
      "monthCount": 3,
      "pendingCount": 1,
      "approvedUnpaidCount": 2
    },
    "chart": {
      "labels": ["2025-07", "2025-08", "...", "2026-06"],
      "appliedAmounts": [1200, 3400, 0, 0, 5600, 2300, 4500, 1800, 2900, 0, 3800, 2000],
      "approvedAmounts": [1200, 3000, 0, 0, 5600, 2300, 4000, 1800, 2900, 0, 3800, 2000]
    },
    "memberBreakdown": [
      { "userId": 1001, "userName": "张三", "appliedAmount": 12000.00, "approvedAmount": 12000.00 }
    ],
    "detailList": {
      "items": [ { "id": 100, "reportNo": "BX-20260603-001", "...": "..." } ],
      "total": 42,
      "page": 1
    }
  }
}
```

### 7.4 加班明细报表

```
GET /api/oa/report/overtime?range=year&deptId=10
```

**专属参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| otType | string | 否 | all / workday / weekend / holiday |

**响应**：
```json
{
  "code": 0,
  "data": {
    "cards": {
      "monthTotalHours": 42.5,
      "monthCount": 8,
      "compLeaveHours": 24.0,
      "overtimePayCount": 3
    },
    "chart": {
      "labels": ["2025-07", "...", "2026-06"],
      "workdayHours": [10, 8, 12, 5, 0, 8, 15, 6, 0, 10, 4, 8],
      "weekendHours": [5, 0, 8, 0, 0, 0, 0, 4, 0, 0, 8, 0],
      "holidayHours": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    },
    "memberBreakdown": [
      { "userId": 1001, "userName": "张三", "totalHours": 18.5 },
      { "userId": 1002, "userName": "李四", "totalHours": 24.0 }
    ],
    "detailList": {
      "items": [ { "id": 12346, "applicationNo": "JB-20260603-001", "...": "..." } ],
      "total": 15,
      "page": 1
    }
  }
}
```

### 7.5 用车明细报表

```
GET /api/oa/report/vehicle?range=year&deptId=10
```

**专属参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| vehicleId | long | 否 | 按车牌筛选 |
| purpose | string | 否 | all / reception / business / official |

**响应**：
```json
{
  "code": 0,
  "data": {
    "cards": {
      "monthCount": 5,
      "totalMileage": 1250.80,
      "tollParkingCost": 385.00,
      "unreturnedCount": 1
    },
    "chart": {
      "labels": ["2025-07", "...", "2026-06"],
      "tripCounts": [3, 2, 5, 1, 4, 2, 6, 3, 0, 2, 5, 4],
      "costs": [120, 80, 250, 50, 180, 90, 300, 150, 0, 100, 220, 180]
    },
    "memberBreakdown": [
      { "userId": 1001, "userName": "张三", "tripCount": 8, "totalCost": 450.00 }
    ],
    "detailList": {
      "items": [ { "id": 200, "applicationNo": "YC-20260603-001", "...": "..." } ],
      "total": 25,
      "page": 1
    }
  }
}
```

### 7.6 外勤日志明细报表

```
GET /api/oa/report/outing-log?range=year&deptId=10
```

**专属参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customerName | string | 否 | 客户名模糊搜索 |

**响应**：
```json
{
  "code": 0,
  "data": {
    "cards": {
      "monthVisitCount": 12,
      "uniqueCustomerCount": 8,
      "avgRatingStars": 4.2,
      "uncommentedCount": 3
    },
    "chart": {
      "labels": ["2025-07", "...", "2026-06"],
      "visitCounts": [5, 8, 12, 6, 10, 7, 15, 9, 4, 11, 8, 12],
      "avgRatings": [4.0, 4.5, 3.8, 4.2, 4.0, 4.3, 3.9, 4.5, 4.0, 4.1, 4.2, 4.2]
    },
    "memberBreakdown": [
      { "userId": 1001, "userName": "张三", "visitCount": 25, "avgRating": 4.0 }
    ],
    "detailList": {
      "items": [ { "id": 300, "visitNo": "VST-20260603-001", "...": "..." } ],
      "total": 80,
      "page": 1
    }
  }
}
```

### 7.7 导出 Excel

```
POST /api/oa/report/{type}/export
```

**权限**：需要 `oa.report.export` 权限（财务/管理员角色）。

**请求**：
```json
{
  "range": "quarter",
  "startDate": "2026-04-01",
  "endDate": "2026-06-30",
  "deptId": 10,
  "status": "all",
  "paymentStatus": "all"
}
```

> 参数与对应报表查询接口一致。`range` 与 `startDate+endDate` 互斥。

**响应**：`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`，二进制文件流。Flutter 收到后通过 MethodChannel 唤起系统分享面板。

---

## 8. 工作台 API

### 8.1 工作台汇总数据

```
GET /api/oa/home/summary
```

**说明**：工作台首页的快捷看板数据。根据当前用户 OA 权限自动判定角色，返回对应数据范围（本人/本部门/全公司）。

**权限**：登录即可访问，数据范围按角色自动限定。

**响应（员工版）**：
```json
{
  "code": 0,
  "data": {
    "role": "employee",
    "banners": [
      { "id": 1, "imageUrl": "https://oss/...", "title": "Q2优秀员工表彰", "linkUrl": "tboss://oa/announcement/detail/50", "sortOrder": 1 }
    ],
    "cards": {
      "monthTotalPaid": 28500.00,
      "monthSubmitCount": 5
    }
  }
}
```

**响应（经理版）**：
```json
{
  "code": 0,
  "data": {
    "role": "manager",
    "banners": [ "..." ],
    "pendingApprovalCount": 8,
    "cards": {
      "monthTotalPaid": 128500.00,
      "monthSubmitCount": 42,
      "deptPendingCount": 8
    }
  }
}
```

**响应（财务/管理员版）**：
```json
{
  "code": 0,
  "data": {
    "role": "finance",
    "banners": [ "..." ],
    "cards": {
      "monthTotalPaid": 525000.00,
      "unpaidTotal": 185000.00,
      "weekRejectedCount": 3
    }
  }
}
```

> 数据缓存 5 分钟。管理员版额外在金刚区展示"发布公告"入口（由前端根据 role 或权限判断）。员工/经理/财务/管理员角色判定优先级见 PRD §3.2。

---

## 9. ERP 集成 API（.NET 服务端转发）

### 9.1 预算查询

```
GET /api/oa/budget/balance?projectId=100&subjectId=5
```
**响应**：
```json
{ "code": 0, "data": { "hasBudget": true, "availableAmount": 150000.00, "frozenAmount": 20000.00 } }
```
ERP 无预算时返回 `{ "hasBudget": false }`，OA 前端隐藏预算区块。

### 9.2 项目/科目/成本中心

```
GET /api/oa/projects              # 项目列表
GET /api/oa/subjects?projectId=   # 某项目下的预算科目
GET /api/oa/cost-centers          # 成本中心列表
```

### 9.3 汇率查询

```
GET /api/oa/exchange-rate?currencyCode=USD
```
**响应**：
```json
{ "code": 0, "data": { "currencyCode": "USD", "rate": 7.2456, "date": "2026-06-03" } }
```

---

## 10. 基础数据 API

### 10.1 费用类别字典

```
GET /api/oa/cost-categories?bizScope=both&expenseType=travel
```

**参数**：

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| bizScope | string | 否 | expense_apply / expense / both（默认 both） |
| expenseType | string | 否 | 按费用大类过滤（travel/entertainment/office 等），不传返回全部 |

**说明**：费用申请和费用报销表单中"费用类别"下拉的数据源。返回启用的叶子节点（`ParentId IS NOT NULL`），`ExpenseType` 用于联动过滤（如费用申请选了"差旅费"大类，则仅返回绑定了 travel 的费用小类）。

**响应**：
```json
{
  "code": 0,
  "data": {
    "categories": [
      {
        "id": 10,
        "categoryName": "交通费",
        "categoryCode": "transport",
        "parentId": 1,
        "expenseType": "travel",
        "sortOrder": 1
      },
      {
        "id": 11,
        "categoryName": "住宿费",
        "categoryCode": "hotel",
        "parentId": 1,
        "expenseType": "travel",
        "sortOrder": 2
      },
      {
        "id": 20,
        "categoryName": "办公用品",
        "categoryCode": "office_supplies",
        "parentId": 5,
        "expenseType": "office",
        "sortOrder": 1
      }
    ]
  }
}
```

### 10.2 车辆管理

```
GET    /api/oa/vehicles              # 列表
POST   /api/oa/vehicles              # 添加
PUT    /api/oa/vehicles/3            # 修改
```

**GET 响应**：
```json
{
  "code": 0,
  "data": {
    "vehicles": [
      {
        "id": 1,
        "licensePlate": "粤B12345",
        "vehicleType": "sedan",
        "brand": "丰田凯美瑞",
        "seats": 5,
        "driverName": "陈司机",
        "status": "idle",
        "isActive": true
      }
    ]
  }
}
```

### 10.3 轮播图

```
GET /api/oa/banners
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "banners": [
      {
        "id": 1,
        "imageUrl": "https://oss/...",
        "title": "2026年Q2优秀员工表彰",
        "linkUrl": "tboss://oa/announcement/detail/50",
        "sortOrder": 1
      }
    ]
  }
}
```

### 10.4 文件上传

```
POST /api/oa/upload
Content-Type: multipart/form-data
```

**说明**：统一文件上传端点。文件上传至云端对象存储（OSS），返回绝对 URL。前端拿到 URL 后在提交表单时写入 `attachments` 数组。

**参数**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | ✅ | 文件本体 |
| bizType | string | ✅ | expense_apply / expense / outing_log / announcement |

**校验**：

| bizType | 数量限制 | 类型限制 | 大小限制 |
|---------|:--:|------|:--:|
| expense_apply | ≤9 | 图片/PDF | 图片≤10MB, PDF≤20MB |
| expense | ≤9 | 图片 | ≤10MB |
| outing_log | 1~9 | 仅图片 | ≤10MB |
| announcement | ≤5 | PDF/图片/Word/Excel | ≤20MB |

> 数量限制由前端在上传入口处控制（满额隐藏上传按钮），后端做二次校验。

**响应**：
```json
{
  "code": 0,
  "data": {
    "fileUrl": "https://oss.example.com/oa/2026/06/abc123.jpg",
    "fileName": "现场_20260603_143021.jpg",
    "fileType": "image",
    "fileSize": 2048000
  }
}
```

**错误**：
| code | 场景 |
|------|------|
| 422 | 文件大小超限 / 类型不支持 / 数量超限 |

---

## 11. 复用已有 API（参考）

以下 API 为 .NET 服务端已有，OA 模块直接调用。此处仅列接口签名，详细文档见原系统。

| 方法 | URL | 说明 | 关键参数 |
|------|-----|------|---------|
| GET | `/api/user/{id}` | 用户信息 | id: ERP 用户 ID |
| GET | `/api/user/search?q=&page=` | 用户搜索 | q: 姓名/工号 |
| GET | `/api/dept/tree` | 部门树 | — |
| PUT | `/api/user/avatar` | 上传头像 | multipart/form-data |
| GET | `/api/customer/search?q=` | 客户联想 | q: 客户名称 |
| GET | `/api/messages?page=` | 消息列表 | page |
| GET | `/api/messages/unread-count` | 未读消息数 | — |
| PUT | `/api/messages/:id/read` | 标记已读 | id |
| POST | `/api/messages/read-all` | 全部已读 | — |
| DELETE | `/api/messages/:id` | 删除消息 | id |
| GET | `/api/dict/banks` | 银行列表 | — |
| GET | `/api/dict/cost-categories` | 费用类别 | — |

---

> **文档版本**：v1.0 | 日期：2026-06-04
