变更行程管理API.md 21 KB

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

请求参数: 无

响应示例:

{
  "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. 新增变更申请

测试步骤:

  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