21 KiB
变更行程管理API文档
📋 文档信息
模块名称: 出国行程变更管理
功能说明: 对已通过的出国申请进行行程变更申请
创建时间: 2025-10-13
文档版本: v1.0
🎯 功能概述
本模块用于管理出国申请的行程变更,包括:
- ✅ 查询已通过的出国申请(可变更的申请)
- ✅ 新增行程变更申请
- ✅ 查询变更申请列表
- ✅ 查看变更申请详情
- ✅ 修改并重新提交变更申请
- ✅ 撤回变更申请
- ✅ 删除变更申请
- ✅ 导出变更申请列表
变更类型:
- 目的地变更: 更改目的地国家
- 时间变更: 更改出国时间(出发/返回日期)
- 行程变更: 更改具体行程安排
- 组合变更: 支持多种变更类型同时申请
🔐 权限配置
菜单权限
| 菜单名称 | 权限标识 | 说明 |
|---|---|---|
| 变更行程 | 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
请求参数: 无
响应示例:
{
"code": 200,
"msg": "查询成功",
"data": [
{
"applicationId": 123456789,
"applicantName": "张三",
"department": "信息技术部",
"destination": "日本",
"exitReason": "旅游",
"startDate": "2025-11-01",
"endDate": "2025-11-10",
"duration": 10,
"itinerary": "东京-大阪-京都"
}
]
}
后端实现要点:
@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
响应示例:
{
"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
}
后端实现要点:
@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
响应示例:
{
"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"
}
}
后端实现要点:
@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:
{
"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 | ❌ | 备注 |
响应示例:
{
"code": 200,
"msg": "提交成功",
"data": {
"changeId": 987654321
}
}
后端实现要点:
@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:
{
"changeId": 987654321,
"changeType": "时间变更,行程变更,目的地变更",
"newDestination": "韩国",
"newExitReason": "旅游",
"newStartDate": "2025-11-10",
"newEndDate": "2025-11-20",
"newItinerary": "首尔-釜山-济州岛",
"changeReason": "根据审批意见调整,改为前往韩国",
"remark": "已根据领导建议修改目的地"
}
响应示例:
{
"code": 200,
"msg": "修改成功"
}
后端实现要点:
@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=行程计划有变,暂不变更
响应示例:
{
"code": 200,
"msg": "撤回成功"
}
后端实现要点:
@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
响应示例:
{
"code": 200,
"msg": "删除成功"
}
后端实现要点:
@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:
{
"applicantName": "",
"destination": "",
"status": ""
}
响应类型: application/vnd.ms-excel
响应Headers:
Content-Disposition: attachment; filename*=UTF-8''变更行程列表_20251013.xlsx
后端实现要点:
@PostMapping("/export")
@SaCheckPermission("rs:itineraryChange:export")
public void export(ItineraryChangeQuery query, HttpServletResponse response) {
// 1. 查询数据(不分页,最多1000条)
// 2. 转换为Excel导出格式
// 3. 设置响应头
// 4. 写入响应流
}
导出字段:
- 申请人
- 部门
- 原目的地
- 新目的地
- 原出国时间
- 新出国时间
- 变更类型
- 变更原因
- 变更状态
- 申请时间
📝 数据模型
ItineraryChangeQuery(查询参数)
public class ItineraryChangeQuery extends BaseEntity {
/** 申请人姓名(模糊查询) */
private String applicantName;
/** 目的地国家(模糊查询) */
private String destination;
/** 变更状态 */
private String status;
}
ItineraryChangeForm(表单)
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(返回对象)
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. 新增变更申请
测试步骤:
- 选择一个已通过的出国申请
- 选择变更类型(时间变更)
- 填写新的出发和返回日期
- 填写变更原因
- 点击提交
预期结果:
- ✅ 提交成功
- ✅ 状态为"待审核"
- ✅ 原申请信息正确填充
- ✅ 新出国天数自动计算正确
2. 撤回变更申请
测试步骤:
- 在变更申请列表中找到一条"待审核"的记录
- 点击撤回按钮
- 输入撤回原因
- 确认撤回
预期结果:
- ✅ 撤回成功
- ✅ 状态变为"已撤回"
- ✅ 撤回原因保存到备注字段
3. 重新提交已拒绝的申请
测试步骤:
- 找到一条"已拒绝"的变更申请
- 点击重新提交
- 根据审批意见修改内容
- 点击提交
预期结果:
- ✅ 提交成功
- ✅ 状态重置为"待审核"
- ✅ 审批信息被清空
📌 注意事项
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