# 变更行程管理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> 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 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 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> 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 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 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 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