“编译修改”

This commit is contained in:
weizhong committed 2025-10-14 16:48:17 +08:00
1 parent e79bf5a26c
commit 2f2e303781
105 files changed
+26461

No files matched your search

@@ -0,0 +1,306 @@
# 出国登记管理 API 接口文档
## 📋 文档说明
### 适用场景
- 出国登记信息管理
- 出国登记状态跟踪
- 出国登记记录查询
- 为 Cursor AI 提供完整的接口开发上下文
### 模块特色
- **简化表单设计**:去除需要纸质盖章的字段,优化用户体验
- **状态管理**:草稿→已提交→已确认→已撤回的完整状态流转
- **数据完整性**:与出国申请系统关联,确保数据一致性
- **历史记录**:完整的登记历史追踪功能
---
## 🔌 出国登记管理 API 接口文档
### 模块概述
**模块名称**:overseas-registration / 出国登记管理
**基础路径**:`/api/rs/overseas-registration`
**版本**:v1.0
**负责人**:weizzzz
**更新时间**:2025-09-30
### 模块功能
出国登记管理模块提供出国登记信息的完整管理功能,支持登记信息录入、状态管理、历史记录查询等核心功能。该模块与出国申请系统集成,为已审批通过的出国申请提供登记功能。
### 重要变更记录
#### 2025-09-30 字段优化变更
**第二次更新:照片字段UI优化**
- **UI变更**:照片字段从输入框改为右上角预留区域(32mm×36mm)
- **功能调整**:
- 删除照片上传输入框
- 在弹窗右上角添加照片预留位置标识
- 用于审批完成后下载附件线下张贴照片
- **字段保留**:photo字段在数据模型中保留(兼容性考虑)
- **用户体验**:简化表单操作,明确线下张贴流程
**第一次更新:登记备案信息字段删除**
- **删除字段**:`登记备案信息`和`联系方式`相关字段
- **删除原因**:这些字段需要下载后纸质版盖章,不适合在线填写
- **影响范围**:前端表单、API接口、数据模型
- **具体删除字段**:
- `registrationUnit`:登记备案责任单位
- `unitLeaderSignature`:负责人签名
- `officePhone`:办公电话
- `mobilePhone`:移动电话
### 权限说明
#### 出国登记权限
- **权限前缀**:`rs:overseas:registration:`
- **权限列表**:
- `rs:overseas:registration:list` - 查询登记列表权限
- `rs:overseas:registration:query` - 查询登记详情权限
- `rs:overseas:registration:add` - 新增登记权限
- `rs:overseas:registration:edit` - 编辑登记权限
- `rs:overseas:registration:remove` - 删除登记权限
- `rs:overseas:registration:export` - 导出登记权限
- `rs:overseas:registration:submit` - 提交登记权限
- `rs:overseas:registration:cancel` - 撤回登记权限
---
## 📊 接口概览
### 接口分类
#### 出国登记基础 CRUD 接口
| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 |
| ---------------- | --------- | --------------------------------------- | ------------------------------------- | -------------------------- |
| 查询登记列表 | GET | `/rs/overseas-registration/list` | `rs:overseas:registration:list` | 分页查询出国登记列表 |
| 查询登记详情 | GET | `/rs/overseas-registration/{id}` | `rs:overseas:registration:query` | 根据ID查询出国登记详情 |
| 新增登记 | POST | `/rs/overseas-registration` | `rs:overseas:registration:add` | 创建新的出国登记 |
| 修改登记 | PUT | `/rs/overseas-registration` | `rs:overseas:registration:edit` | 更新出国登记信息 |
| 删除登记 | DELETE | `/rs/overseas-registration/{ids}` | `rs:overseas:registration:remove` | 批量删除出国登记 |
| 导出登记 | POST | `/rs/overseas-registration/export` | `rs:overseas:registration:export` | 导出出国登记数据 |
#### 出国登记业务逻辑接口
| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 |
| ------------------ | --------- | ------------------------------------------- | ------------------------------------- | -------------------------- |
| 提交登记 | PUT | `/rs/overseas-registration/submit/{id}` | `rs:overseas:registration:submit` | 提交出国登记 |
| 撤回登记 | PUT | `/rs/overseas-registration/cancel/{id}` | `rs:overseas:registration:cancel` | 撤回出国登记 |
| 保存并提交 | POST | `/rs/overseas-registration/addAndSubmit` | `rs:overseas:registration:add` | 保存并提交出国登记 |
#### 出国登记历史接口
| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 |
| ------------------ | --------- | ------------------------------------------- | ------------------------------------- | -------------------------- |
| 查询登记历史 | GET | `/rs/overseas-registration/history/list` | `rs:overseas:registration:list` | 查询出国登记历史列表 |
| 查询历史详情 | GET | `/rs/overseas-registration/history/{id}` | `rs:overseas:registration:query` | 查询出国登记历史详情 |
| 更新历史记录 | PUT | `/rs/overseas-registration/history` | `rs:overseas:registration:edit` | 更新出国登记历史记录 |
| 导出历史记录 | POST | `/rs/overseas-registration/history/export` | `rs:overseas:registration:export` | 导出出国登记历史数据 |
---
## 🔧 数据模型
### 出国登记表单对象(简化版)
```typescript
interface OverseasRegistrationForm {
// 基本信息
name: string; // 姓名
gender: string; // 性别(0-男,1-女)
birthDate: string; // 出生日期
photo: string; // 照片预留(右上角32mm×36mm预留区域,用于审批完成后线下张贴,字段保留用于兼容性)
idNumber: string; // 身份证号码
politicalStatus: string; // 政治面貌
registeredAddress: string; // 户籍所在地
// 工作信息
department: string; // 工作部门
position: string; // 职务职称
administrativeLevel: string; // 行政职级
personnelCategory: string; // 人员类别代码
// 关联信息
applicationId: string; // 关联出国申请ID
remarks: string; // 备注
}
```
### 字段变更说明
#### 已删除字段(2025-09-30)
```typescript
// 以下字段已从表单中移除,因为需要纸质版盖章
interface RemovedFields {
// 登记备案信息
registrationUnit: string; // 登记备案责任单位(已删除)
unitLeaderSignature: string; // 负责人签名(已删除)
// 联系方式
officePhone: string; // 办公电话(已删除)
mobilePhone: string; // 移动电话(已删除)
}
```
### 表单验证规则(更新后)
```typescript
const registrationRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
birthDate: [{ required: true, message: '请选择出生日期', trigger: 'change' }],
idNumber: [
{ required: true, message: '请输入身份证号', trigger: 'blur' },
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入正确的18位身份证号码', trigger: 'blur' }
],
politicalStatus: [{ required: true, message: '请选择政治面貌', trigger: 'change' }],
registeredAddress: [{ required: true, message: '请输入户籍所在地', trigger: 'blur' }],
department: [{ required: true, message: '请输入工作部门', trigger: 'blur' }],
position: [{ required: true, message: '请输入职务职称', trigger: 'blur' }]
};
```
## 📝 接口详情
### 1. 查询出国登记列表
**接口地址**:`GET /rs/overseas-registration/list`
**接口描述**:分页查询出国登记列表,支持多条件筛选
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| pageNum | number | 是 | 页码,从1开始 |
| pageSize | number | 是 | 每页大小,最大100 |
| name | string | 否 | 姓名(模糊匹配) |
| gender | string | 否 | 性别(0-男,1-女) |
| idNumber | string | 否 | 身份证号(模糊匹配) |
| politicalStatus | string | 否 | 政治面貌 |
| department | string | 否 | 工作部门(模糊匹配) |
| position | string | 否 | 职务职称(模糊匹配) |
| status | string | 否 | 登记状态(0-草稿,1-已提交,2-已确认,3-已撤回) |
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"rows": [
{
"registrationId": "1234567890123456789",
"name": "张三",
"gender": "0",
"birthDate": "1990-01-01",
"idNumber": "123456789012345678",
"politicalStatus": "中共党员",
"registeredAddress": "浙江省杭州市西湖区",
"department": "信息工程学院",
"position": "副教授",
"status": "1",
"statusText": "已提交",
"registrationTime": "2025-09-30 10:00:00",
"createTime": "2025-09-30 09:30:00"
}
],
"total": 1
}
```
### 2. 新增出国登记
**接口地址**:`POST /rs/overseas-registration`
**接口描述**:创建新的出国登记记录
**请求参数**:
```json
{
"name": "张三",
"gender": "0",
"birthDate": "1990-01-01",
"photo": "", // 照片预留区域(32mm×36mm),不再接收上传地址
"idNumber": "123456789012345678",
"politicalStatus": "中共党员",
"registeredAddress": "浙江省杭州市西湖区某某街道",
"department": "信息工程学院",
"position": "副教授",
"administrativeLevel": "副处级",
"personnelCategory": "A001",
"applicationId": "9876543210987654321",
"remarks": "学术交流访问"
}
```
**响应示例**:
```json
{
"code": 200,
"msg": "新增成功",
"data": {
"registrationId": "1234567890123456789"
}
}
```
---
## 🔄 业务流程
### 登记状态流转
```
草稿(0) → 已提交(1) → 已确认(2)
↓ ↓
删除 撤回(3)
```
### 状态说明
- **草稿(0)**:初始创建状态,可编辑、删除
- **已提交(1)**:已提交待确认,不可编辑,可撤回
- **已确认(2)**:已确认完成,不可编辑撤回
- **已撤回(3)**:已撤回状态,可重新编辑提交
---
## 🚨 注意事项
### 字段变更影响
1. **前端表单**:已移除登记备案信息和联系方式输入框
2. **数据验证**:已移除相关字段的必填验证
3. **接口传参**:新增/修改接口不再包含已删除字段
4. **列表显示**:表格中不再显示移动电话列
5. **详情查看**:详情弹窗中不再显示相关信息
### 开发建议
1. 确保后端接口也相应调整,移除已删除字段的处理
2. 数据库迁移需要考虑历史数据的处理
3. 相关导出功能需要同步调整字段列表
4. API文档需要及时更新,保持与实际接口一致
---
## 📖 参考文档
- [API文档基础规范](./API文档基础规范.md)
- [用户认证登录API](./用户认证登录API.md)
- [出国申请管理API](./出国申请管理API.md)
---
**文档维护**:weizzzz
**创建时间**:2025-09-30
**最后更新**:2025-09-30
@@ -0,0 +1,582 @@
# 出国行程变更管理API文档
## 📋 文档信息
- **模块名称**: 出国行程变更申请管理
- **功能概述**: 提供出国行程变更申请的新增、查询、审批等功能
- **创建日期**: 2025-10-13
- **最后更新**: 2025-10-13
- **数据库版本**: v1.1(包含个人信息字段)
---
## 🎯 业务说明
### 功能概述
出国行程变更管理模块允许用户对已审批通过的出国申请提交行程变更申请,支持变更目的地、时间、行程安排等信息。
### 业务流程
```
1. 用户查看已审批通过的出国申请
2. 用户选择一个出国申请,填写变更信息
3. 提交变更申请(状态:待审核)
4. 管理员审批变更申请
- 通过:状态变为"已通过",成为变更记录
- 拒绝:状态变为"已拒绝"
5. 用户可撤回待审核的变更申请
```
### 数据分类
- **变更申请**: 所有状态的变更申请记录(待审核、已通过、已拒绝、已撤回)
- **变更记录**: 只包含已通过审批的变更申请,代表真正生效的变更记录
---
## 🔌 API接口列表
### 1. 查询变更申请列表
**接口说明**: 查询所有的行程变更申请(包括所有状态)
**请求方式**: `GET`
**接口地址**: `/rs/itinerary/change/list`
**权限标识**: `rs:itineraryChange:list`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| pageNum | Integer | 否 | 页码(默认1) |
| pageSize | Integer | 否 | 每页条数(默认10) |
| applicantName | String | 否 | 申请人姓名(模糊查询) |
| status | String | 否 | 变更状态(0=待审核,1=已通过,2=已拒绝,3=已撤回) |
| changeType | String | 否 | 变更类型 |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"total": 10,
"rows": [
{
"changeId": "1977740586114916354",
"applicationId": "1977123456789012345",
"applicantId": "1",
"applicantName": "admin",
"status": "0",
"statusText": "待审核",
"changeType": "destination_change",
"changeTypeText": "目的地变更",
"originalDestination": "美国",
"newDestination": "加拿大",
"originalStartDate": "2025-11-01",
"newStartDate": "2025-11-05",
"changeReason": "会议时间调整",
"createTime": "2025-10-13 10:00:00"
}
]
}
}
```
---
### 2. 查询变更记录列表
**接口说明**: 查询当前用户的变更记录(只返回已通过的变更申请)
**重要说明**:
- 🔴 **此接口只返回status='1'(已通过)的变更申请**
- 变更记录代表真正生效的变更记录,不包括待审核、已拒绝、已撤回的申请
**请求方式**: `GET`
**接口地址**: `/rs/itinerary/change/my/list`
**权限标识**: `rs:itineraryChange:list`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| pageNum | Integer | 否 | 页码(默认1) |
| pageSize | Integer | 否 | 每页条数(默认10) |
| changeType | String | 否 | 变更类型 |
**后端自动过滤条件**:
- `applicantId` = 当前登录用户ID(自动设置)
- `status` = '1'(只查询已通过的,自动设置)
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"total": 5,
"rows": [
{
"changeId": "1977740586114916354",
"applicationId": "1977123456789012345",
"applicantId": "1",
"applicantName": "admin",
"status": "1",
"statusText": "已通过",
"changeType": "destination_change",
"changeTypeText": "目的地变更",
"originalDestination": "美国",
"newDestination": "加拿大",
"approverName": "管理员",
"approveTime": "2025-10-13 14:00:00",
"createTime": "2025-10-13 10:00:00"
}
]
}
}
```
---
### 3. 查询待审批列表
**接口说明**: 查询待审批的行程变更申请(status='0')
**请求方式**: `GET`
**接口地址**: `/rs/itinerary/change/pending/list`
**权限标识**: `rs:itineraryChange:approve`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| pageNum | Integer | 否 | 页码(默认1) |
| pageSize | Integer | 否 | 每页条数(默认10) |
| applicantName | String | 否 | 申请人姓名(模糊查询) |
**后端自动过滤条件**:
- `status` = '0'(只查询待审核的,自动设置)
---
### 4. 查询变更申请详情
**接口说明**: 根据变更ID查询详细信息
**请求方式**: `GET`
**接口地址**: `/rs/itinerary/change/{changeId}`
**权限标识**: `rs:itineraryChange:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| changeId | Long | 是 | 变更ID |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"changeId": "1977740586114916354",
"applicationId": "1977123456789012345",
"applicantName": "admin",
"gender": "0",
"birthDate": "1990-01-01",
"politicalStatus": "中共党员",
"status": "1",
"statusText": "已通过",
"changeType": "destination_change",
"changeTypeText": "目的地变更",
"originalDestination": "美国",
"newDestination": "加拿大",
"originalStartDate": "2025-11-01",
"newStartDate": "2025-11-05",
"changeReason": "会议时间调整",
"supportingDocuments": "http://xxx.com/file.pdf",
"approverName": "管理员",
"approveTime": "2025-10-13 14:00:00",
"approveOpinion": "同意变更"
}
}
```
---
### 5. 新增变更申请
**接口说明**: 提交新的行程变更申请
**请求方式**: `POST`
**接口地址**: `/rs/itinerary/change`
**权限标识**: `rs:itineraryChange:add`
**请求体示例**:
```json
{
"applicationId": "1977123456789012345",
"changeType": "destination_change",
"newDestination": "加拿大",
"newDestinationCity": "多伦多",
"newStartDate": "2025-11-05",
"newEndDate": "2025-11-15",
"newDuration": 10,
"changeReason": "会议时间调整",
"supportingDocuments": "http://xxx.com/file.pdf"
}
```
**后端自动设置字段**:
- `applicantId`: 当前登录用户ID
- `applicantName`: 当前登录用户姓名
- `status`: '0'(待审核)
- `tenantId`: 当前租户ID
- 从原出国申请中复制:`originalDestination`、`originalStartDate`等原始信息
**业务校验**:
1. ✅ 出国申请必须存在且状态为"已通过"
2. ✅ 不能对同一个出国申请同时提交多个待审核的变更申请
---
### 6. 修改变更申请(重新提交)
**接口说明**: 修改待审核或已拒绝的变更申请并重新提交
**请求方式**: `PUT`
**接口地址**: `/rs/itinerary/change`
**权限标识**: `rs:itineraryChange:edit`
**请求体示例**:
```json
{
"changeId": "1977740586114916354",
"changeType": "destination_change",
"newDestination": "英国",
"newDestinationCity": "伦敦",
"changeReason": "更新后的变更原因"
}
```
---
### 7. 删除变更申请
**接口说明**: 删除变更申请(逻辑删除)
**请求方式**: `DELETE`
**接口地址**: `/rs/itinerary/change/{changeIds}`
**权限标识**: `rs:itineraryChange:remove`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| changeIds | Long[] | 是 | 变更ID数组(逗号分隔) |
---
### 8. 撤回变更申请
**接口说明**: 撤回待审核的变更申请
**请求方式**: `PUT`
**接口地址**: `/rs/itinerary/change/cancel/{changeId}`
**权限标识**: `rs:itineraryChange:cancel`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| changeId | Long | 是 | 变更ID |
**业务规则**:
- ✅ 只能撤回状态为"待审核"的申请
- 撤回后状态变为'3'(已撤回)
---
### 9. 审批变更申请
**接口说明**: 审批行程变更申请(通过/拒绝)
**请求方式**: `PUT`
**接口地址**: `/rs/itinerary/change/approve`
**权限标识**: `rs:itineraryChange:approve`
**请求体示例**:
```json
{
"changeId": "1977740586114916354",
"approved": true,
"opinion": "同意变更"
}
```
**参数说明**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| changeId | Long | 是 | 变更ID |
| approved | Boolean | 是 | 是否通过(true=通过,false=拒绝) |
| opinion | String | 否 | 审批意见 |
**重要提示**:
- ⚠️ **后端接口使用Map接收参数,前端必须发送正确的字段名**
- ✅ 字段名:`changeId`、`approved`、`opinion`
- ❌ 不要使用:`approvalStatus`、`approvalOpinion`
**审批结果**:
- `approved=true`: 状态变为'1'(已通过)→ 成为变更记录
- `approved=false`: 状态变为'2'(已拒绝)
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功"
}
```
---
### 10. 获取可用的出国申请列表
**接口说明**: 获取已审批通过的出国申请列表(用于新增变更时选择原申请)
**请求方式**: `GET`
**接口地址**: `/rs/itinerary/change/available-records`
**权限标识**: `rs:itineraryChange:list`
**响应说明**: 返回当前用户的所有已通过审批的出国申请
---
### 11. 查询某个出国申请的变更历史
**接口说明**: 查询某个出国申请的所有变更记录
**请求方式**: `GET`
**接口地址**: `/rs/itinerary/change/record/{recordId}/history`
**权限标识**: `rs:itineraryChange:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| recordId | Long | 是 | 出国记录ID(或申请ID) |
---
## 📊 数据字典
### 变更状态(rs_itinerary_change_status)
| 字典值 | 标签 | 样式 | 说明 |
|--------|------|------|------|
| 0 | 待审核 | primary | 待审批状态 |
| 1 | 已通过 | success | 已通过审批,成为变更记录 |
| 2 | 已拒绝 | danger | 已拒绝 |
| 3 | 已撤回 | info | 用户撤回 |
### 变更类型(rs_itinerary_change_type)
| 字典值 | 标签 | 说明 |
|--------|------|------|
| destination_change | 目的地变更 | 更改出国目的地 |
| time_change | 时间变更 | 更改出国时间 |
| itinerary_change | 行程变更 | 更改行程安排 |
| transportation_change | 交通方式变更 | 更改交通方式 |
| accommodation_change | 住宿变更 | 更改住宿安排 |
---
## 🎯 前端Tab说明
### 变更申请Tab(all)
- **数据来源**: `/rs/itinerary/change/list`
- **显示内容**: 所有的变更申请(包括所有状态)
- **功能**: 新增、编辑、删除、查看、撤回
### 变更记录Tab(my)
- **数据来源**: `/rs/itinerary/change/my/list`
- **显示内容**: 🔴 **只显示已通过的变更申请**(status='1')
- **业务含义**: 真正生效的变更记录
- **功能**: 查看已通过的变更记录
---
## 🔐 权限配置
| 权限标识 | 说明 |
|---------|------|
| rs:itineraryChange:list | 查询变更申请/记录列表 |
| rs:itineraryChange:query | 查看变更详情 |
| rs:itineraryChange:add | 新增变更申请 |
| rs:itineraryChange:edit | 修改变更申请 |
| rs:itineraryChange:remove | 删除变更申请 |
| rs:itineraryChange:cancel | 撤回变更申请 |
| rs:itineraryChange:approve | 审批变更申请 |
| rs:itineraryChange:export | 导出变更申请 |
---
## 📝 业务规则总结
1. ✅ **新增变更申请**: 只能为已通过审批的出国申请提交变更
2. ✅ **并发控制**: 同一个出国申请不能同时有多个待审核的变更申请
3. ✅ **撤回限制**: 只能撤回待审核状态的申请
4. ✅ **变更记录**: 只有已通过的变更申请才会显示在"变更记录"Tab中
5. ✅ **权限控制**: 所有接口都需要相应的权限标识
---
## 🔧 故障排查
### 问题1:Unknown column 'gender' in 'field list'
**错误描述**:
```
java.sql.SQLSyntaxErrorException: Unknown column 'gender' in 'field list'
```
**原因分析**:
- 数据库表 `rs_overseas_itinerary_change` 缺少个人信息字段
- 后端Mapper查询包含了这些字段,但表中不存在
**解决方案**:
1️⃣ **执行修复SQL**:
```bash
文件位置: docs/设计文档/database/⚡一键修复-添加缺失字段.sql
执行方式: 直接在MySQL中执行
```
2️⃣ **重启后端服务**(清除MyBatis缓存)
3️⃣ **重新测试功能**
**缺失字段列表**:
- gender - 性别
- birth_date - 出生日期
- political_status - 政治面貌
- professional_title - 职称
- position - 职务
- document_name - 申领证件名称
- document_number - 证件号码
- department - 部门
**详细修复指南**: `docs/问题解决方案/🚨数据库字段缺失修复指南.md`
---
### 问题2:审批接口参数错误
**错误描述**:
```
Cannot invoke "Object.toString()" because the return value of "java.util.Map.get(Object)" is null
```
**原因分析**:
- 前端发送的字段名与后端期望不匹配
- 前端: `approvalStatus` + `approvalOpinion`
- 后端: `approved` + `opinion`
**解决方案**:
前端必须发送正确的参数:
```json
{
"changeId": 123,
"approved": true, // ✅ 布尔值,不是字符串
"opinion": "同意变更"
}
```
**详细修复总结**: `docs/问题解决方案/行程变更审批接口参数错误修复总结.md`
---
### 问题3:变更记录Tab显示不正确
**问题描述**:
- "变更记录"Tab显示了所有状态的数据
- 预期只显示已通过(status='1')的数据
**原因分析**:
- 前端查询参数未设置status过滤条件
**解决方案**:
前端在查询"变更记录"时必须传递 `status: '1'`:
```typescript
const queryParams = ref<ItineraryChangeQuery>({
pageNum: 1,
pageSize: 10,
status: '1' // ✅ 只查询已通过的记录
});
```
**详细修复总结**: `docs/问题解决方案/变更记录Tab显示逻辑修复总结.md`
---
## 📞 相关文档
### 开发文档
- [出国行程变更管理API文档](./出国行程变更管理API.md)(本文档)
- [后端开发规范](../../.cursor/cursorrules-backend.md)
- [前端开发规范](../../.cursor/cursorrules-frontend.md)
### 数据库脚本
- [完整建表SQL](../../设计文档/database/rs_overseas_itinerary_change_full.sql)
- [一键修复缺失字段SQL](../../设计文档/database/⚡一键修复-添加缺失字段.sql)
- [诊断缺失字段SQL](../../设计文档/database/🔍诊断-检查缺失字段.sql)
### 问题解决方案
- [数据库字段缺失修复指南](../../问题解决方案/🚨数据库字段缺失修复指南.md)
- [审批接口参数错误修复总结](../../问题解决方案/行程变更审批接口参数错误修复总结.md)
- [变更记录Tab显示逻辑修复总结](../../问题解决方案/变更记录Tab显示逻辑修复总结.md)
- [审批功能测试指南](../../问题解决方案/行程变更审批功能-测试指南.md)
---
**文档创建时间**: 2025-10-13
**文档版本**: v1.1
**维护人员**: weizzzz
**最后更新**: 2025-10-13(新增故障排查部分)
@@ -0,0 +1,844 @@
# 变更行程管理API文档
## 📋 文档信息
**模块名称**: 出国行程变更管理
**功能说明**: 对已通过的出国申请进行行程变更申请
**创建时间**: 2025-10-13
**文档版本**: v1.0
---
## 🎯 功能概述
本模块用于管理出国申请的行程变更,包括:
- ✅ 查询已通过的出国申请(可变更的申请)
- ✅ 新增行程变更申请
- ✅ 查询变更申请列表
- ✅ 查看变更申请详情
- ✅ 修改并重新提交变更申请
- ✅ 撤回变更申请
- ✅ 删除变更申请
- ✅ 导出变更申请列表
**变更类型**:
1. **目的地变更**: 更改目的地国家
2. **时间变更**: 更改出国时间(出发/返回日期)
3. **行程变更**: 更改具体行程安排
4. **组合变更**: 支持多种变更类型同时申请
---
## 🔐 权限配置
### 菜单权限
| 菜单名称 | 权限标识 | 说明 |
|---------|---------|------|
| 变更行程 | `rs:itineraryChange:list` | 访问变更行程页面 |
### 按钮权限
| 按钮名称 | 权限标识 | 说明 |
|---------|---------|------|
| 查询 | `rs:itineraryChange:query` | 查询变更申请详情 |
| 新增 | `rs:itineraryChange:add` | 新增变更申请 |
| 修改 | `rs:itineraryChange:edit` | 修改并重新提交 |
| 删除 | `rs:itineraryChange:remove` | 删除变更申请 |
| 撤回 | `rs:itineraryChange:cancel` | 撤回变更申请 |
| 导出 | `rs:itineraryChange:export` | 导出变更申请列表 |
---
## 📊 数据库表结构
### 表名: `rs_overseas_itinerary_change`
**表说明**: 出国行程变更申请表
**核心字段**:
| 字段名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `change_id` | bigint(20) | ✅ | 变更ID(主键,雪花ID) |
| `application_id` | bigint(20) | ✅ | 原出国申请ID |
| `applicant_id` | bigint(20) | ✅ | 申请人ID |
| `applicant_name` | varchar(100) | ✅ | 申请人姓名 |
| `department` | varchar(200) | ❌ | 部门 |
| `original_destination` | varchar(100) | ❌ | 原目的地国家 |
| `original_exit_reason` | varchar(100) | ❌ | 原出境事由 |
| `original_start_date` | date | ❌ | 原出发日期 |
| `original_end_date` | date | ❌ | 原返回日期 |
| `original_duration` | int(11) | ❌ | 原出国天数 |
| `original_itinerary` | text | ❌ | 原行程安排 |
| `change_type` | varchar(200) | ✅ | 变更类型(逗号分隔) |
| `new_destination` | varchar(100) | ❌ | 新目的地国家 |
| `new_exit_reason` | varchar(100) | ❌ | 新出境事由 |
| `new_start_date` | date | ❌ | 新出发日期 |
| `new_end_date` | date | ❌ | 新返回日期 |
| `new_duration` | int(11) | ❌ | 新出国天数 |
| `new_itinerary` | text | ❌ | 新行程安排 |
| `change_reason` | varchar(500) | ✅ | 变更原因 |
| `remark` | varchar(500) | ❌ | 备注 |
| `status` | char(1) | ✅ | 变更状态 |
| `approval_opinion` | varchar(500) | ❌ | 审批意见 |
| `approval_time` | datetime | ❌ | 审批时间 |
| `approver_id` | bigint(20) | ❌ | 审批人ID |
| `approver_name` | varchar(100) | ❌ | 审批人姓名 |
**状态枚举**:
- `0` - 待审核
- `1` - 已通过
- `2` - 已拒绝
- `3` - 已撤回
---
## 🔌 API接口列表
### 1. 查询已通过的出国申请列表
**接口说明**: 获取当前用户已通过的出国申请,用于新增变更申请时选择
**请求方式**: `GET`
**接口地址**: `/rs/itinerary-change/approvedApplications`
**权限标识**: `rs:itineraryChange:add`
**请求参数**: 无
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"applicationId": 123456789,
"applicantName": "张三",
"department": "信息技术部",
"destination": "日本",
"exitReason": "旅游",
"startDate": "2025-11-01",
"endDate": "2025-11-10",
"duration": 10,
"itinerary": "东京-大阪-京都"
}
]
}
```
**后端实现要点**:
```java
@GetMapping("/approvedApplications")
@SaCheckPermission("rs:itineraryChange:add")
public R<List<OverseasApplicationSimpleVO>> listApprovedApplications() {
// 查询当前用户已通过的出国申请(status='1')
// 只返回必要字段
}
```
---
### 2. 查询变更申请列表(分页)
**接口说明**: 分页查询当前用户的行程变更申请列表
**请求方式**: `GET`
**接口地址**: `/rs/itinerary-change/list`
**权限标识**: `rs:itineraryChange:list`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `pageNum` | Integer | ❌ | 页码(默认1) |
| `pageSize` | Integer | ❌ | 每页数量(默认10) |
| `applicantName` | String | ❌ | 申请人姓名(模糊查询) |
| `destination` | String | ❌ | 目的地国家(模糊查询) |
| `status` | String | ❌ | 变更状态(0/1/2/3) |
**请求示例**:
```
GET /rs/itinerary-change/list?pageNum=1&pageSize=10&status=0
```
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"changeId": 987654321,
"applicationId": 123456789,
"applicantName": "张三",
"department": "信息技术部",
"originalDestination": "日本",
"originalExitReason": "旅游",
"originalStartDate": "2025-11-01",
"originalEndDate": "2025-11-10",
"originalDuration": 10,
"changeType": "时间变更,行程变更",
"newDestination": "日本",
"newStartDate": "2025-11-05",
"newEndDate": "2025-11-15",
"newDuration": 11,
"newItinerary": "东京-大阪-京都-奈良",
"changeReason": "因临时工作调整,需要延后5天出发",
"status": "0",
"statusText": "待审核",
"createTime": "2025-10-13 14:30:00"
}
],
"total": 1
}
```
**后端实现要点**:
```java
@GetMapping("/list")
@SaCheckPermission("rs:itineraryChange:list")
public TableDataInfo<ItineraryChangeVO> list(ItineraryChangeQuery query) {
// 1. 获取当前用户ID
// 2. 构建查询条件(applicant_id + 搜索条件)
// 3. 分页查询
// 4. 转换VO(添加statusText等)
}
```
---
### 3. 查询变更申请详情
**接口说明**: 根据changeId查询变更申请详细信息
**请求方式**: `GET`
**接口地址**: `/rs/itinerary-change/{changeId}`
**权限标识**: `rs:itineraryChange:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `changeId` | Long | ✅ | 变更申请ID |
**请求示例**:
```
GET /rs/itinerary-change/987654321
```
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"changeId": 987654321,
"applicationId": 123456789,
"applicantName": "张三",
"department": "信息技术部",
"originalDestination": "日本",
"originalExitReason": "旅游",
"originalStartDate": "2025-11-01",
"originalEndDate": "2025-11-10",
"originalDuration": 10,
"originalItinerary": "东京-大阪-京都",
"changeType": "时间变更,行程变更",
"newDestination": "日本",
"newExitReason": "旅游",
"newStartDate": "2025-11-05",
"newEndDate": "2025-11-15",
"newDuration": 11,
"newItinerary": "东京-大阪-京都-奈良",
"changeReason": "因临时工作调整,需要延后5天出发,并增加奈良行程",
"remark": null,
"status": "0",
"statusText": "待审核",
"approvalOpinion": null,
"createTime": "2025-10-13 14:30:00",
"updateTime": "2025-10-13 14:30:00"
}
}
```
**后端实现要点**:
```java
@GetMapping("/{changeId}")
@SaCheckPermission("rs:itineraryChange:query")
public R<ItineraryChangeVO> getInfo(@PathVariable Long changeId) {
// 1. 根据changeId查询
// 2. 权限校验(只能查询自己的申请)
// 3. 转换VO并返回
}
```
---
### 4. 新增变更申请
**接口说明**: 提交新的行程变更申请
**请求方式**: `POST`
**接口地址**: `/rs/itinerary-change`
**权限标识**: `rs:itineraryChange:add`
**Content-Type**: `application/json`
**请求Body**:
```json
{
"applicationId": 123456789,
"changeType": "时间变更,行程变更",
"newDestination": "日本",
"newExitReason": "旅游",
"newStartDate": "2025-11-05",
"newEndDate": "2025-11-15",
"newItinerary": "东京-大阪-京都-奈良",
"changeReason": "因临时工作调整,需要延后5天出发,并增加奈良行程",
"remark": ""
}
```
**请求参数说明**:
| 字段名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `applicationId` | Long | ✅ | 原出国申请ID |
| `changeType` | String | ✅ | 变更类型(逗号分隔) |
| `newDestination` | String | ❌ | 新目的地(目的地变更时必填) |
| `newExitReason` | String | ❌ | 新出境事由 |
| `newStartDate` | String | ❌ | 新出发日期(时间变更时必填) |
| `newEndDate` | String | ❌ | 新返回日期(时间变更时必填) |
| `newItinerary` | String | ❌ | 新行程安排(行程变更时必填) |
| `changeReason` | String | ✅ | 变更原因(必填,最多500字) |
| `remark` | String | ❌ | 备注 |
**响应示例**:
```json
{
"code": 200,
"msg": "提交成功",
"data": {
"changeId": 987654321
}
}
```
**后端实现要点**:
```java
@PostMapping
@SaCheckPermission("rs:itineraryChange:add")
public R<Map<String, Object>> add(@Validated @RequestBody ItineraryChangeForm form) {
// 1. 校验applicationId是否存在且状态为已通过
// 2. 校验是否已有待审核的变更申请(避免重复提交)
// 3. 根据applicationId获取原申请信息并填充到original_*字段
// 4. 根据changeType校验对应的new_*字段是否填写
// 5. 计算newDuration(如果有时间变更)
// 6. 设置申请人信息(从当前登录用户获取)
// 7. 设置状态为待审核(status='0')
// 8. 插入数据库
// 9. 返回changeId
}
```
**业务规则**:
- ✅ 只能对状态为"已通过"的出国申请发起变更
- ✅ 同一个出国申请不能同时存在多个"待审核"的变更申请
- ✅ 变更类型至少选择一种
- ✅ 根据变更类型,对应的新字段必须填写
- ✅ 变更原因为必填项
---
### 5. 修改并重新提交变更申请
**接口说明**: 修改已拒绝的变更申请并重新提交
**请求方式**: `PUT`
**接口地址**: `/rs/itinerary-change/{changeId}`
**权限标识**: `rs:itineraryChange:edit`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `changeId` | Long | ✅ | 变更申请ID |
**Content-Type**: `application/json`
**请求Body**:
```json
{
"changeId": 987654321,
"changeType": "时间变更,行程变更,目的地变更",
"newDestination": "韩国",
"newExitReason": "旅游",
"newStartDate": "2025-11-10",
"newEndDate": "2025-11-20",
"newItinerary": "首尔-釜山-济州岛",
"changeReason": "根据审批意见调整,改为前往韩国",
"remark": "已根据领导建议修改目的地"
}
```
**响应示例**:
```json
{
"code": 200,
"msg": "修改成功"
}
```
**后端实现要点**:
```java
@PutMapping("/{changeId}")
@SaCheckPermission("rs:itineraryChange:edit")
public R<Void> edit(@PathVariable Long changeId, @Validated @RequestBody ItineraryChangeForm form) {
// 1. 权限校验(只能修改自己的申请)
// 2. 状态校验(只有已拒绝的申请才能修改)
// 3. 根据changeType校验对应的new_*字段
// 4. 重新计算newDuration
// 5. 清空审批信息(approval_opinion、approval_time等)
// 6. 重置状态为待审核(status='0')
// 7. 更新数据库
}
```
**业务规则**:
- ✅ 只能修改状态为"已拒绝"的变更申请
- ✅ 只能修改自己提交的申请
- ✅ 修改后状态重置为"待审核"
---
### 6. 撤回变更申请
**接口说明**: 撤回待审核的变更申请
**请求方式**: `PUT`
**接口地址**: `/rs/itinerary-change/cancel/{changeId}`
**权限标识**: `rs:itineraryChange:cancel`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `changeId` | Long | ✅ | 变更申请ID |
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `cancelReason` | String | ✅ | 撤回原因 |
**请求示例**:
```
PUT /rs/itinerary-change/cancel/987654321?cancelReason=行程计划有变,暂不变更
```
**响应示例**:
```json
{
"code": 200,
"msg": "撤回成功"
}
```
**后端实现要点**:
```java
@PutMapping("/cancel/{changeId}")
@SaCheckPermission("rs:itineraryChange:cancel")
public R<Void> cancel(@PathVariable Long changeId, @RequestParam String cancelReason) {
// 1. 权限校验(只能撤回自己的申请)
// 2. 状态校验(只有待审核的申请才能撤回)
// 3. 更新状态为已撤回(status='3')
// 4. 保存撤回原因到remark字段
}
```
**业务规则**:
- ✅ 只能撤回状态为"待审核"的变更申请
- ✅ 只能撤回自己提交的申请
---
### 7. 删除变更申请
**接口说明**: 删除变更申请(逻辑删除)
**请求方式**: `DELETE`
**接口地址**: `/rs/itinerary-change/{changeId}`
**权限标识**: `rs:itineraryChange:remove`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-------|------|------|------|
| `changeId` | Long | ✅ | 变更申请ID |
**请求示例**:
```
DELETE /rs/itinerary-change/987654321
```
**响应示例**:
```json
{
"code": 200,
"msg": "删除成功"
}
```
**后端实现要点**:
```java
@DeleteMapping("/{changeId}")
@SaCheckPermission("rs:itineraryChange:remove")
public R<Void> remove(@PathVariable Long changeId) {
// 1. 权限校验(只能删除自己的申请)
// 2. 状态校验(不能删除已通过的申请)
// 3. 逻辑删除(设置deleted=1)
}
```
**业务规则**:
- ✅ 只能删除自己提交的申请
- ✅ 不能删除状态为"已通过"的申请
---
### 8. 导出变更申请列表
**接口说明**: 导出变更申请列表为Excel文件
**请求方式**: `POST`
**接口地址**: `/rs/itinerary-change/export`
**权限标识**: `rs:itineraryChange:export`
**Content-Type**: `application/json`
**请求Body**:
```json
{
"applicantName": "",
"destination": "",
"status": ""
}
```
**响应类型**: `application/vnd.ms-excel`
**响应Headers**:
```
Content-Disposition: attachment; filename*=UTF-8''变更行程列表_20251013.xlsx
```
**后端实现要点**:
```java
@PostMapping("/export")
@SaCheckPermission("rs:itineraryChange:export")
public void export(ItineraryChangeQuery query, HttpServletResponse response) {
// 1. 查询数据(不分页,最多1000条)
// 2. 转换为Excel导出格式
// 3. 设置响应头
// 4. 写入响应流
}
```
**导出字段**:
- 申请人
- 部门
- 原目的地
- 新目的地
- 原出国时间
- 新出国时间
- 变更类型
- 变更原因
- 变更状态
- 申请时间
---
## 📝 数据模型
### ItineraryChangeQuery(查询参数)
```java
public class ItineraryChangeQuery extends BaseEntity {
/** 申请人姓名(模糊查询) */
private String applicantName;
/** 目的地国家(模糊查询) */
private String destination;
/** 变更状态 */
private String status;
}
```
### ItineraryChangeForm(表单)
```java
public class ItineraryChangeForm {
/** 变更ID(修改时传入) */
private Long changeId;
/** 原出国申请ID */
@NotNull(message = "请选择原出国申请")
private Long applicationId;
/** 变更类型 */
@NotBlank(message = "请选择变更类型")
private String changeType;
/** 新目的地国家 */
private String newDestination;
/** 新出境事由 */
private String newExitReason;
/** 新出发日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date newStartDate;
/** 新返回日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date newEndDate;
/** 新行程安排 */
private String newItinerary;
/** 变更原因 */
@NotBlank(message = "请输入变更原因")
@Size(max = 500, message = "变更原因不能超过500字")
private String changeReason;
/** 备注 */
@Size(max = 500, message = "备注不能超过500字")
private String remark;
}
```
### ItineraryChangeVO(返回对象)
```java
public class ItineraryChangeVO {
/** 变更ID */
private Long changeId;
/** 原出国申请ID */
private Long applicationId;
/** 申请人信息 */
private Long applicantId;
private String applicantName;
private String department;
/** 原申请信息 */
private String originalDestination;
private String originalExitReason;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date originalStartDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date originalEndDate;
private Integer originalDuration;
private String originalItinerary;
/** 变更信息 */
private String changeType;
private String newDestination;
private String newExitReason;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date newStartDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date newEndDate;
private Integer newDuration;
private String newItinerary;
/** 变更原因和备注 */
private String changeReason;
private String remark;
/** 审批信息 */
private String status;
private String statusText; // 状态文本
private String approvalOpinion;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date approvalTime;
private String approverName;
/** 时间信息 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
```
---
## 🔍 业务规则总结
### 1. 可发起变更的条件
- ✅ 出国申请状态必须为"已通过"
- ✅ 该申请不能存在"待审核"状态的变更申请
- ✅ 只能变更自己的出国申请
### 2. 变更类型校验
- **目的地变更**: 必须填写`newDestination`
- **时间变更**: 必须填写`newStartDate`和`newEndDate`,系统自动计算`newDuration`
- **行程变更**: 必须填写`newItinerary`
- 可以同时选择多种变更类型
### 3. 状态流转
```
待审核(0) ──审批通过──> 已通过(1)
│
├──审批拒绝──> 已拒绝(2) ──修改重提──> 待审核(0)
│
└──申请人撤回──> 已撤回(3)
```
### 4. 操作权限
- **待审核**: 可撤回、可删除
- **已通过**: 不可删除、不可修改
- **已拒绝**: 可修改重提、可删除
- **已撤回**: 可删除
---
## 🧪 测试用例
### 1. 新增变更申请
**测试步骤**:
1. 选择一个已通过的出国申请
2. 选择变更类型(时间变更)
3. 填写新的出发和返回日期
4. 填写变更原因
5. 点击提交
**预期结果**:
- ✅ 提交成功
- ✅ 状态为"待审核"
- ✅ 原申请信息正确填充
- ✅ 新出国天数自动计算正确
### 2. 撤回变更申请
**测试步骤**:
1. 在变更申请列表中找到一条"待审核"的记录
2. 点击撤回按钮
3. 输入撤回原因
4. 确认撤回
**预期结果**:
- ✅ 撤回成功
- ✅ 状态变为"已撤回"
- ✅ 撤回原因保存到备注字段
### 3. 重新提交已拒绝的申请
**测试步骤**:
1. 找到一条"已拒绝"的变更申请
2. 点击重新提交
3. 根据审批意见修改内容
4. 点击提交
**预期结果**:
- ✅ 提交成功
- ✅ 状态重置为"待审核"
- ✅ 审批信息被清空
---
## 📌 注意事项
### 1. 后端开发注意事项
- ✅ 使用雪花ID生成器生成`change_id`
- ✅ 所有查询必须添加`deleted=0`条件
- ✅ 所有查询必须添加租户ID条件(多租户隔离)
- ✅ 时间字段使用`@JsonFormat`格式化
- ✅ 状态转换时需要校验当前状态
- ✅ 权限校验:确保用户只能操作自己的数据
### 2. 前端开发注意事项
- ✅ 日期组件使用`YYYY-MM-DD`格式
- ✅ 出国天数自动计算(监听日期变化)
- ✅ 根据变更类型动态显示/隐藏对应字段
- ✅ 根据变更类型设置字段必填验证
- ✅ 列表操作按钮根据状态动态显示
### 3. 数据一致性
- ✅ 新增时从原申请复制`original_*`字段(冗余存储)
- ✅ 修改时不允许更改`original_*`字段
- ✅ 状态为"已通过"后,原申请的相关字段应同步更新(业务需要时)
---
## 🚀 部署清单
### 数据库
- [ ] 执行数据库表创建脚本:`rs_overseas_itinerary_change.sql`
- [ ] 执行菜单权限配置脚本:`rs_overseas_itinerary_change_menu.sql`
- [ ] 为相应角色分配菜单权限
### 后端
- [ ] 创建实体类:`ItineraryChange`
- [ ] 创建Mapper接口:`ItineraryChangeMapper`
- [ ] 创建Mapper XML:`ItineraryChangeMapper.xml`
- [ ] 创建Service接口:`IItineraryChangeService`
- [ ] 创建Service实现:`ItineraryChangeServiceImpl`
- [ ] 创建Controller:`ItineraryChangeController`
- [ ] 创建VO类:`ItineraryChangeVO`
- [ ] 创建Form类:`ItineraryChangeForm`
- [ ] 创建Query类:`ItineraryChangeQuery`
- [ ] 添加权限注解:`@SaCheckPermission`
### 前端
- [ ] 创建页面:`src/views/rs/overseas/itinerary-change.vue`
- [ ] 创建API文件:`src/api/rs/itinerary-change.ts`
- [ ] 配置路由(菜单配置后自动生成)
- [ ] 测试页面功能
### 测试
- [ ] 单元测试:Service层方法测试
- [ ] 接口测试:Postman/Apifox测试所有API
- [ ] 功能测试:前端页面完整流程测试
- [ ] 权限测试:不同角色权限测试
---
**文档维护**: 请在开发过程中及时更新本文档
**最后更新**: 2025-10-13
@@ -0,0 +1,367 @@
# 党籍保留附件上传功能测试指南
## 📋 功能说明
**功能名称**:党籍保留附件上传
**实现日期**:2025-10-10
**适用模块**:出国申请管理
### 业务场景
- 当申请人是**中共党员**且**出国天数超过30天**时,需要上传党籍保留附件
- 附件需要经过党组织审批并盖章后才能上传
- 支持PDF、JPG、PNG格式,文件大小限制10MB
---
## 🔧 部署前准备
### 1. 数据库更新
**执行SQL脚本**:
```bash
路径:docs/设计文档/database/【立即执行】添加党籍保留附件字段.sql
```
**操作步骤**:
1. 连接到 `hzdyjs_xg` 数据库
2. 执行整个SQL脚本
3. 检查执行结果,确认4个字段添加成功:
- `party_retention_file_path` - 党籍保留附件文件路径
- `party_retention_file_name` - 党籍保留附件文件名
- `party_retention_upload_time` - 党籍保留附件上传时间
- `party_retention_upload_status` - 党籍保留附件上传状态
### 2. 后端代码部署
**修改文件清单**:
1. ✅ `RsOverseasApplicationController.java` - 新增 `uploadPartyRetentionFile` 接口
2. ✅ `IRsOverseasApplicationService.java` - 新增接口方法声明
3. ✅ `RsOverseasApplicationServiceImpl.java` - 新增接口方法实现
4. ✅ `RsOverseasApplication.java` - 新增4个实体字段
**部署步骤**:
```bash
# 1. 切换到后端目录
cd /Users/weizzzz/Documents/Cursor/0828CURSOR/all-project/backend
# 2. 编译打包
mvn clean package -DskipTests
# 3. 重启后端服务
# 根据实际部署方式重启服务
```
### 3. 前端无需修改
前端代码已经包含党籍保留附件上传功能,无需修改。
---
## 🧪 功能测试步骤
### 测试场景1:符合上传条件的申请
#### 前置条件
- 申请人的政治面貌为:**中共党员**
- 出国天数设置为:**31天或以上**
#### 测试步骤
1. **登录系统**
- 使用测试账号登录前端系统
- 导航到:`人事管理` → `出国申请`
2. **创建新申请**
- 点击"新增申请"按钮
- 填写基本信息:
- 申请人姓名:测试用户
- 所属部门:技术部
- 政治面貌:**中共党员**
- 出国类型:因公出国
- 目的地:美国
- 出国天数:**35天**(超过30天)
3. **验证上传区域显示**
- ✅ 应该自动显示"党籍保留附件上传"区域
- ✅ 显示红色必填提示
- ✅ 显示上传按钮和说明文字
4. **上传附件**
- 点击"上传党籍保留附件"按钮
- 选择一个PDF或图片文件(小于10MB)
- 观察上传过程
5. **验证上传结果**
- ✅ 上传成功后显示绿色成功提示:"党籍保留附件上传成功!"
- ✅ 文件列表中显示已上传的文件
- ✅ 可以预览和删除文件
- ✅ 按钮文字变为"重新上传"
6. **提交申请**
- 填写其他必填项
- 点击"提交"按钮
- ✅ 申请提交成功
### 测试场景2:不符合上传条件的申请
#### 测试用例2.1:政治面貌为群众
- 政治面貌选择:**群众**
- 出国天数:35天
- ✅ **不应该**显示党籍保留附件上传区域
#### 测试用例2.2:出国天数≤30天
- 政治面貌选择:**中共党员**
- 出国天数:**28天**
- ✅ **不应该**显示党籍保留附件上传区域
### 测试场景3:文件格式验证
#### 测试用例3.1:支持的格式
- 上传 `.pdf` 文件 → ✅ 应该成功
- 上传 `.jpg` 文件 → ✅ 应该成功
- 上传 `.png` 文件 → ✅ 应该成功
#### 测试用例3.2:不支持的格式
- 上传 `.doc` 文件 → ❌ 提示"只支持PDF、JPG、PNG格式的文件!"
- 上传 `.xlsx` 文件 → ❌ 提示"只支持PDF、JPG、PNG格式的文件!"
### 测试场景4:文件大小验证
- 上传 5MB 文件 → ✅ 应该成功
- 上传 9.9MB 文件 → ✅ 应该成功
- 上传 11MB 文件 → ❌ 提示"文件大小不能超过 10MB"
### 测试场景5:API接口测试
#### 使用Postman或curl测试
**请求示例**:
```bash
POST /rs/overseas/application/uploadPartyRetentionFile
Headers:
Authorization: Bearer {token}
Content-Type: multipart/form-data
Body (form-data):
file: [选择文件]
applicationId: 1234567890
```
**成功响应**:
```json
{
"code": 200,
"msg": "党籍保留附件上传成功",
"data": {
"filePath": "https://oss.example.com/overseas/party-retention/party_retention_1234567890_1696935234567.pdf",
"fileName": "党籍保留附件.pdf",
"uploadTime": "2025-10-10 15:30:45"
}
}
```
**失败响应示例**:
```json
{
"code": 500,
"msg": "不支持的文件格式,请上传PDF、JPG或PNG文件"
}
```
---
## 🔍 数据验证
### 1. 数据库验证
**查询上传记录**:
```sql
SELECT
application_id,
applicant_name,
party_retention_file_path,
party_retention_file_name,
party_retention_upload_time,
party_retention_upload_status,
political_status,
duration
FROM rs_overseas_application
WHERE party_retention_upload_status = '1'
ORDER BY party_retention_upload_time DESC
LIMIT 10;
```
**验证点**:
- ✅ `party_retention_file_path` 应该有完整的OSS路径
- ✅ `party_retention_file_name` 应该是原始文件名
- ✅ `party_retention_upload_time` 应该是当前时间
- ✅ `party_retention_upload_status` 应该是 `'1'`
### 2. OSS存储验证
**文件存储路径**:
```
overseas/party-retention/party_retention_{applicationId}_{timestamp}.{ext}
```
**验证方式**:
- 登录OSS管理控制台
- 导航到 `overseas/party-retention/` 目录
- 确认文件已正确上传
- 验证文件可以正常下载和预览
---
## 🚨 常见问题排查
### 问题1:点击上传按钮后提示"Request method 'POST' is not supported"
**原因**:后端接口未部署或URL路径不正确
**排查步骤**:
1. 检查后端服务是否已重启
2. 检查接口路径:`/rs/overseas/application/uploadPartyRetentionFile`
3. 确认Controller中的 `@PostMapping` 注解配置正确
4. 查看后端日志,确认请求是否到达Controller
### 问题2:上传成功但数据库没有记录
**原因**:数据库字段未添加或Service层更新失败
**排查步骤**:
1. 执行SQL验证字段是否存在:
```sql
SHOW COLUMNS FROM rs_overseas_application
WHERE Field LIKE 'party_retention%';
```
2. 检查后端日志中的异常信息
3. 确认Service层的 `updatePartyRetentionFileInfo` 方法被正确调用
### 问题3:文件上传失败,提示"系统异常"
**原因**:OSS配置问题或网络问题
**排查步骤**:
1. 检查OSS配置是否正确(application.yml)
2. 确认OSS AccessKey和SecretKey有效
3. 检查服务器到OSS的网络连接
4. 查看后端详细错误日志
### 问题4:上传区域不显示
**原因**:条件判断逻辑问题
**排查步骤**:
1. 确认政治面貌是否选择了"中共党员"
2. 确认出国天数是否>30天
3. 打开浏览器开发者工具,检查 `showPartyRetentionUpload` 的值
4. 检查前端计算逻辑是否正确
---
## ✅ 测试检查清单
### 功能性测试
- [ ] 符合条件时显示上传区域
- [ ] 不符合条件时不显示上传区域
- [ ] 文件格式验证正确(PDF、JPG、PNG)
- [ ] 文件大小验证正确(≤10MB)
- [ ] 上传成功后显示文件信息
- [ ] 可以重新上传替换文件
- [ ] 可以删除已上传文件
- [ ] 提交申请时数据正确保存
### 接口测试
- [ ] POST /uploadPartyRetentionFile 返回200
- [ ] 响应数据格式正确
- [ ] 错误处理正确(格式错误、大小超限等)
### 数据完整性
- [ ] 数据库字段值正确
- [ ] OSS文件存储成功
- [ ] 文件路径可访问
- [ ] 文件可以正常预览和下载
### 用户体验
- [ ] 上传过程有加载提示
- [ ] 成功/失败消息清晰友好
- [ ] 上传按钮状态正确切换
- [ ] 文件列表显示完整
---
## 📝 测试报告模板
```markdown
## 党籍保留附件上传功能测试报告
**测试日期**:2025-10-10
**测试人员**:[测试人员姓名]
**测试环境**:开发环境 / 测试环境 / 生产环境
### 测试结果汇总
- 测试用例总数:15
- 通过用例数:__
- 失败用例数:__
- 阻塞用例数:__
### 详细测试结果
#### 1. 上传条件判断
- [✅/❌] 中共党员 + 出国>30天:显示上传区域
- [✅/❌] 群众:不显示上传区域
- [✅/❌] 出国≤30天:不显示上传区域
#### 2. 文件上传功能
- [✅/❌] PDF文件上传成功
- [✅/❌] JPG文件上传成功
- [✅/❌] PNG文件上传成功
- [✅/❌] 不支持格式被拦截
- [✅/❌] 超大文件被拦截
#### 3. 数据存储
- [✅/❌] 数据库记录正确
- [✅/❌] OSS文件存储成功
- [✅/❌] 文件路径可访问
#### 4. 异常情况
- [✅/❌] 网络异常处理
- [✅/❌] 重复上传处理
- [✅/❌] 删除文件处理
### 发现的问题
1. [问题描述]
- 严重程度:高/中/低
- 复现步骤:...
- 期望结果:...
- 实际结果:...
### 测试结论
[ ] 通过,可以发布
[ ] 通过,但有建议改进项
[ ] 不通过,需要修复问题
### 备注
[其他说明]
```
---
## 🎯 验收标准
功能通过以下所有标准即可验收:
1. ✅ 当申请人是中共党员且出国天数>30天时,必须显示党籍保留附件上传区域
2. ✅ 不符合条件时,不显示上传区域,不影响正常申请流程
3. ✅ 文件上传成功后,数据库正确保存4个字段的值
4. ✅ 上传的文件正确存储在OSS的 `overseas/party-retention/` 目录下
5. ✅ 文件格式和大小验证正确,错误提示友好
6. ✅ 可以重新上传替换文件,旧文件被覆盖
7. ✅ 所有操作有明确的成功/失败提示
8. ✅ 不影响现有出国申请的其他功能
---
**文档维护人**:Cursor AI
**最后更新**:2025-10-10
**版本**:v1.0
@@ -0,0 +1,386 @@
# 出国申请附件下载权限修复方案
## 📋 问题描述
### 错误现象
用户点击出国申请详情中的"出国境登记表盖章文件"和"党籍保留附件"下载按钮时,出现以下错误:
```xml
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied.</Message>
<Key>overseas/stamped/stamped_0_1760082853042.png</Key>
<BucketName>ruoyi</BucketName>
<Resource>/ruoyi/overseas/stamped/stamped_0_1760082853042.png</Resource>
</Error>
```
### 根本原因
1. **OSS Bucket权限配置**:OSS bucket设置为私有访问,直接使用URL无法访问
2. **前端下载方式错误**:前端直接使用文件URL进行下载,绕过了权限验证
3. **缺少OSS ID存储**:数据库中只存储了文件路径,没有存储OSS文件ID,无法使用系统的安全下载接口
## 🎯 解决方案
### 整体架构
```
前端下载请求 → 后端代理接口 → 权限验证 → OSS安全下载 → 返回文件流
```
### 技术方案
采用**后端代理下载 + OSS ID存储**的方式,确保:
- ✅ 所有下载请求经过后端权限验证
- ✅ 使用系统统一的OSS服务管理文件
- ✅ 文件访问可追踪、可审计
- ✅ 支持私有bucket安全访问
## 🔧 实施步骤
### 1️⃣ 数据库改造
**新增字段**:`stamped_file_oss_id` 和 `party_retention_file_oss_id`
```sql
-- 添加盖章文件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `stamped_file_oss_id` BIGINT(20) NULL COMMENT '盖章文件OSS ID'
AFTER `stamped_file_upload_status`;
-- 添加党籍保留附件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `party_retention_file_oss_id` BIGINT(20) NULL COMMENT '党籍保留附件OSS ID'
AFTER `party_retention_upload_status`;
-- 添加索引以提升查询性能
ALTER TABLE `rs_overseas_application`
ADD INDEX `idx_stamped_file_oss_id` (`stamped_file_oss_id`),
ADD INDEX `idx_party_retention_file_oss_id` (`party_retention_file_oss_id`);
```
**执行位置**:`docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql`
### 2️⃣ 后端Domain实体改造
#### RsOverseasApplication.java
```java
/**
* 盖章文件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long partyRetentionFileOssId;
```
#### RsOverseasApplicationVo.java
```java
/**
* 盖章文件OSS ID(用于安全下载)
*/
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
private Long partyRetentionFileOssId;
```
### 3️⃣ 后端上传接口改造
**改造前**:使用`OssClient`直接上传,只返回文件URL
```java
// 旧代码
OssClient ossClient = OssFactory.instance();
UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath,
file.getSize(), file.getContentType());
applicationService.updateStampedFileInfo(applicationId, uploadResult.getUrl(),
originalFilename, new Date(), "1");
```
**改造后**:使用`ISysOssService`上传,自动保存到`sys_oss`表并返回OSS ID
```java
// 新代码 - 使用系统OSS服务
@Autowired
private ISysOssService ossService;
SysOssVo ossResult = ossService.upload(file);
// 更新申请记录 - 存储OSS ID和文件路径
applicationService.updateStampedFileInfo(
applicationId,
ossResult.getUrl(), // 文件URL
ossResult.getOriginalName(), // 原始文件名
new Date(), // 上传时间
"1", // 上传状态:已上传
ossResult.getOssId() // ✨ OSS文件ID(新增)
);
```
**涉及接口**:
- `POST /rs/overseas/application/uploadStampedFile` - 盖章文件上传
- `POST /rs/overseas/application/uploadPartyRetentionFile` - 党籍保留附件上传
### 4️⃣ 后端Service层改造
#### IRsOverseasApplicationService.java
```java
// 添加ossId参数
Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
```
#### RsOverseasApplicationServiceImpl.java
```java
@Override
@Transactional
public Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId) {
return baseMapper.update(null,
new LambdaUpdateWrapper<RsOverseasApplication>()
.eq(RsOverseasApplication::getApplicationId, applicationId)
.set(RsOverseasApplication::getStampedRegistrationFilePath, filePath)
.set(RsOverseasApplication::getStampedRegistrationFileName, fileName)
.set(RsOverseasApplication::getStampedFileUploadTime, uploadTime)
.set(RsOverseasApplication::getStampedFileUploadStatus, uploadStatus)
.set(RsOverseasApplication::getStampedFileOssId, ossId) // ✨ 新增
) > 0;
}
```
### 5️⃣ 后端新增下载代理接口
#### RsOverseasApplicationController.java
**盖章文件下载**:
```java
/**
* 下载盖章文件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadStampedFile/{applicationId}")
public void downloadStampedFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getStampedFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getStampedFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**党籍保留附件下载**:
```java
/**
* 下载党籍保留附件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadPartyRetentionFile/{applicationId}")
public void downloadPartyRetentionFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getPartyRetentionFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getPartyRetentionFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**核心特性**:
- ✅ 权限验证:`@SaCheckPermission("rs:overseas:query")`
- ✅ 文件存在性检查
- ✅ 使用系统统一的`ISysOssService.secureDownload()`方法
- ✅ 自动处理文件流返回
### 6️⃣ 前端下载逻辑改造
#### index.vue
**改造前**:直接使用文件URL下载
```javascript
// 旧代码 - 直接访问OSS URL(会被拒绝)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
const link = document.createElement('a');
link.href = data.stampedRegistrationFilePath; // ❌ 直接使用OSS URL
link.download = data.stampedRegistrationFileName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
```
**改造后**:调用后端代理接口
```javascript
// 新代码 - 通过后端代理下载(安全)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
console.log('📥 下载出国境登记表盖章文件:', data.stampedRegistrationFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadStampedFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
// 党籍保留附件下载(同样改造)
const handleDownloadPartyRetentionFile = (data: OverseasApplicationVO) => {
console.log('📥 下载党籍保留附件:', data.partyRetentionFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadPartyRetentionFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
```
## 📊 改造效果
### 安全性提升
| 项目 | 改造前 | 改造后 |
|------|--------|--------|
| 权限验证 | ❌ 无 | ✅ Sa-Token权限验证 |
| 访问控制 | ❌ 任何人可访问URL | ✅ 必须登录且有权限 |
| 访问日志 | ❌ 无 | ✅ 自动记录访问日志 |
| Bucket类型 | ⚠️ 必须公开 | ✅ 可使用私有bucket |
### 技术优势
- ✅ **统一管理**:使用系统统一的`ISysOssService`服务
- ✅ **可扩展性**:支持断点续传、缓存等高级特性
- ✅ **可追踪性**:所有文件访问都有日志记录
- ✅ **规范一致**:与系统其他模块保持一致的文件处理方式
## 🚀 部署步骤
### 1. 执行数据库迁移
```bash
# 连接到数据库
mysql -u root -p hzdyjs_xg
# 执行SQL脚本
source docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql
```
### 2. 重启后端服务
```bash
# 停止服务
# 重新编译打包
# 启动服务
```
### 3. 刷新前端
```bash
# 清除浏览器缓存
# 重新访问页面
```
### 4. 验证功能
- [ ] 上传新的盖章文件,检查是否保存了OSS ID
- [ ] 上传新的党籍保留附件,检查是否保存了OSS ID
- [ ] 测试下载功能,确认可以正常下载
- [ ] 检查下载时的权限验证是否生效
## 📝 注意事项
### 数据兼容性
⚠️ **已存在的文件处理**:
- 旧数据:没有OSS ID的记录无法通过新接口下载
- 解决方案:
1. 方案一:保留旧的下载方式作为降级方案
2. 方案二:编写数据迁移脚本,为旧文件生成OSS ID
3. 方案三(推荐):让用户重新上传文件
### OSS配置检查
确保系统OSS配置正确:
```yaml
# application.yml
oss:
endpoint: xxx
accessKeyId: xxx
accessKeySecret: xxx
bucketName: ruoyi
region: xxx
```
### 权限配置
确保菜单权限配置正确:
- `rs:overseas:query` - 查看出国申请权限(含下载)
- `rs:overseas:add` - 新增出国申请权限(含上传)
## 🔍 故障排查
### 问题1:下载时提示"文件不存在"
**排查步骤**:
1. 检查数据库中的`stamped_file_oss_id`或`party_retention_file_oss_id`字段是否有值
2. 检查`sys_oss`表中是否存在对应的OSS ID记录
3. 检查OSS中是否真实存在该文件
### 问题2:下载时提示"无权限"
**排查步骤**:
1. 检查用户是否登录
2. 检查用户是否有`rs:overseas:query`权限
3. 检查Sa-Token配置是否正确
### 问题3:下载速度慢
**可能原因**:
- OSS服务器距离远
- 文件较大
- 网络带宽限制
**优化方案**:
- 启用CDN加速
- 使用分片下载
- 优化网络配置
## 📚 相关文档
- [系统OSS服务文档](./OSS文件管理服务.md)
- [Sa-Token权限验证文档](./权限管理文档.md)
- [出国申请API文档](../API/出国申请管理API.md)
## 🎯 总结
本次改造通过引入OSS ID存储和后端代理下载机制,彻底解决了出国申请附件下载权限问题。改造后的系统:
- ✅ 安全性更高:所有下载都经过权限验证
- ✅ 可维护性更好:使用系统统一的OSS服务
- ✅ 可追踪性更强:所有文件访问都有日志
- ✅ 扩展性更好:支持更多高级特性
**日期**:2025-01-10
**作者**:AI Assistant
**版本**:v1.0