模块名称: 出国行程变更管理
功能说明: 对已通过的出国申请进行行程变更申请
创建时间: 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 - 已撤回接口说明: 获取当前用户已通过的出国申请,用于新增变更申请时选择
请求方式: 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')
// 只返回必要字段
}
接口说明: 分页查询当前用户的行程变更申请列表
请求方式: 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等)
}
接口说明: 根据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并返回
}
接口说明: 提交新的行程变更申请
请求方式: 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
}
业务规则:
接口说明: 修改已拒绝的变更申请并重新提交
请求方式: 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. 更新数据库
}
业务规则:
接口说明: 撤回待审核的变更申请
请求方式: 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字段
}
业务规则:
接口说明: 删除变更申请(逻辑删除)
请求方式: 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)
}
业务规则:
接口说明: 导出变更申请列表为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. 写入响应流
}
导出字段:
public class ItineraryChangeQuery extends BaseEntity {
/** 申请人姓名(模糊查询) */
private String applicantName;
/** 目的地国家(模糊查询) */
private String destination;
/** 变更状态 */
private String status;
}
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;
}
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;
}
newDestinationnewStartDate和newEndDate,系统自动计算newDurationnewItinerary待审核(0) ──审批通过──> 已通过(1)
│
├──审批拒绝──> 已拒绝(2) ──修改重提──> 待审核(0)
│
└──申请人撤回──> 已撤回(3)
测试步骤:
预期结果:
测试步骤:
预期结果:
测试步骤:
预期结果:
change_iddeleted=0条件@JsonFormat格式化YYYY-MM-DD格式original_*字段(冗余存储)original_*字段rs_overseas_itinerary_change.sqlrs_overseas_itinerary_change_menu.sqlItineraryChangeItineraryChangeMapperItineraryChangeMapper.xmlIItineraryChangeServiceItineraryChangeServiceImplItineraryChangeControllerItineraryChangeVOItineraryChangeFormItineraryChangeQuery@SaCheckPermissionsrc/views/rs/overseas/itinerary-change.vuesrc/api/rs/itinerary-change.ts文档维护: 请在开发过程中及时更新本文档
最后更新: 2025-10-13