Files
hzdyjs/🚀【立即执行】变更行程菜单-部署指南.md
T
2025-10-14 16:41:19 +08:00

16 KiB
Raw Blame History

🚀 出国行程变更申请菜单 - 立即部署指南

📋 部署概览

当前完成度: 60%
下一步任务: 后端API开发
预计开发时间: 7小时


第一步:数据库部署(15分钟)

1. 执行数据表创建脚本

# 进入MySQL
mysql -u root -p

# 选择数据库
use hzdyjs_xg;

# 执行建表脚本
source /Users/weizzzz/Documents/Cursor/0828CURSOR/all-project/docs/设计文档/database/rs_overseas_itinerary_change.sql

验证表创建:

-- 查看表结构
DESC rs_overseas_itinerary_change;

-- 查看索引
SHOW INDEX FROM rs_overseas_itinerary_change;

2. 执行菜单权限配置脚本

⚠️ 注意:执行前需要确认人事管理菜单的实际menu_id!

-- 1. 查询人事管理菜单ID
SELECT menu_id, menu_name, parent_id FROM sys_menu WHERE menu_name = '人事管理';

如果parent_id不是105,需要修改SQL脚本:

-- 打开菜单脚本文件,全局替换parent_id
-- 将 parent_id = 105 替换为实际的ID

执行菜单配置:

source /Users/weizzzz/Documents/Cursor/0828CURSOR/all-project/docs/设计文档/database/rs_overseas_itinerary_change_menu.sql

验证菜单创建:

-- 查询变更行程菜单及其子菜单
SELECT 
  menu_id, 
  menu_name, 
  parent_id, 
  order_num, 
  path, 
  component, 
  perms, 
  menu_type,
  visible,
  status
FROM sys_menu 
WHERE menu_id >= 2050 AND menu_id <= 2057
ORDER BY menu_id;

预期结果:应该看到8条菜单记录


3. 为管理员角色分配权限

-- 查询管理员角色ID(通常为1)
SELECT role_id, role_name FROM sys_role WHERE role_name LIKE '%管理员%';

-- 为管理员分配菜单权限(假设role_id=1)
INSERT INTO sys_role_menu VALUES (1, 2050);  -- 变更行程菜单
INSERT INTO sys_role_menu VALUES (1, 2051);  -- 变更行程管理页面
INSERT INTO sys_role_menu VALUES (1, 2052);  -- 查询权限
INSERT INTO sys_role_menu VALUES (1, 2053);  -- 新增权限
INSERT INTO sys_role_menu VALUES (1, 2054);  -- 修改权限
INSERT INTO sys_role_menu VALUES (1, 2055);  -- 删除权限
INSERT INTO sys_role_menu VALUES (1, 2056);  -- 撤回权限
INSERT INTO sys_role_menu VALUES (1, 2057);  -- 导出权限

-- 验证权限分配
SELECT rm.*, m.menu_name 
FROM sys_role_menu rm 
LEFT JOIN sys_menu m ON rm.menu_id = m.menu_id
WHERE rm.menu_id >= 2050 AND rm.menu_id <= 2057;

4. 清理浏览器缓存

前端菜单有缓存,需要清理:

  1. 退出系统登录
  2. 清除浏览器缓存(Ctrl+Shift+Delete)
  3. 重新登录
  4. 查看人事管理菜单下是否出现"变更行程"菜单

第二步:后端API开发(4小时)

1. 创建实体类(30分钟)

参考文件: backend/src/main/java/com/hzdyjs/modules/rs/domain/RsOverseasApplication.java

需要创建:

backend/src/main/java/com/hzdyjs/modules/rs/domain/
└── RsOverseasItineraryChange.java

关键点:

  • ✅ 使用@TableName("rs_overseas_itinerary_change")
  • ✅ 主键使用@TableId(type = IdType.ASSIGN_ID)(雪花ID)
  • ✅ 逻辑删除字段使用@TableLogic
  • ✅ 时间字段使用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  • ✅ 继承BaseEntity(包含创建者、更新者等字段)

2. 创建VO/Form/Query类(30分钟)

需要创建:

backend/src/main/java/com/hzdyjs/modules/rs/domain/
├── vo/
│   └── ItineraryChangeVO.java          # 返回对象
├── form/
│   └── ItineraryChangeForm.java        # 表单对象
└── query/
    └── ItineraryChangeQuery.java       # 查询对象

参考API文档:docs/开发文档/API/变更行程管理API.md(第592-707行)

关键验证注解:

  • @NotNull - 非空校验
  • @NotBlank - 非空字符串校验
  • @Size(max = 500) - 长度校验
  • @JsonFormat - 日期格式化

3. 创建Mapper层(30分钟)

需要创建:

backend/src/main/java/com/hzdyjs/modules/rs/mapper/
├── ItineraryChangeMapper.java
└── xml/
    └── ItineraryChangeMapper.xml

Mapper接口:

@Mapper
public interface ItineraryChangeMapper extends BaseMapper<RsOverseasItineraryChange> {
    // 自定义查询方法(如果需要)
    List<ItineraryChangeVO> selectChangeList(ItineraryChangeQuery query);
}

Mapper XML:

  • 配置resultMap
  • 编写自定义SQL(如果需要)
  • 多表关联查询(如果需要)

4. 创建Service层(1小时)

需要创建:

backend/src/main/java/com/hzdyjs/modules/rs/service/
├── IItineraryChangeService.java
└── impl/
    └── ItineraryChangeServiceImpl.java

Service接口方法:

public interface IItineraryChangeService {
    // 查询已通过的出国申请列表
    List<OverseasApplicationSimpleVO> listApprovedApplications();
    
    // 查询变更申请列表(分页)
    TableDataInfo<ItineraryChangeVO> selectChangeList(ItineraryChangeQuery query);
    
    // 查询变更申请详情
    ItineraryChangeVO selectChangeById(Long changeId);
    
    // 新增变更申请
    Long insertChange(ItineraryChangeForm form);
    
    // 修改并重新提交
    int updateChange(Long changeId, ItineraryChangeForm form);
    
    // 撤回变更申请
    int cancelChange(Long changeId, String cancelReason);
    
    // 删除变更申请
    int deleteChangeById(Long changeId);
    
    // 导出变更申请列表
    List<ItineraryChangeVO> selectChangeList(ItineraryChangeQuery query);
}

Service实现关键逻辑:

新增变更申请:

@Override
public Long insertChange(ItineraryChangeForm form) {
    // 1. 校验applicationId是否存在且状态为已通过
    RsOverseasApplication application = applicationMapper.selectById(form.getApplicationId());
    if (application == null || !"1".equals(application.getStatus())) {
        throw new ServiceException("出国申请不存在或状态不是已通过");
    }
    
    // 2. 校验是否已有待审核的变更申请
    LambdaQueryWrapper<RsOverseasItineraryChange> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(RsOverseasItineraryChange::getApplicationId, form.getApplicationId())
           .eq(RsOverseasItineraryChange::getStatus, "0");
    Long count = baseMapper.selectCount(wrapper);
    if (count > 0) {
        throw new ServiceException("该出国申请已存在待审核的变更申请,请先处理完毕");
    }
    
    // 3. 复制原申请信息到original_*字段
    RsOverseasItineraryChange change = new RsOverseasItineraryChange();
    change.setApplicationId(application.getApplicationId());
    change.setOriginalDestination(application.getDestination());
    change.setOriginalExitReason(application.getExitReason());
    // ... 其他original字段
    
    // 4. 填充变更信息
    BeanUtils.copyProperties(form, change);
    
    // 5. 计算新出国天数
    if (form.getNewStartDate() != null && form.getNewEndDate() != null) {
        long days = ChronoUnit.DAYS.between(
            form.getNewStartDate().toInstant(),
            form.getNewEndDate().toInstant()
        ) + 1;
        change.setNewDuration((int) days);
    }
    
    // 6. 设置申请人信息(从当前登录用户获取)
    LoginUser loginUser = SecurityUtils.getLoginUser();
    change.setApplicantId(loginUser.getUserId());
    change.setApplicantName(loginUser.getUsername());
    change.setDepartment(loginUser.getDeptName());
    
    // 7. 设置状态为待审核
    change.setStatus("0");
    
    // 8. 插入数据库
    baseMapper.insert(change);
    
    return change.getChangeId();
}

5. 创建Controller层(1小时)

需要创建:

backend/src/main/java/com/hzdyjs/modules/rs/controller/
└── ItineraryChangeController.java

Controller示例:

@RestController
@RequestMapping("/rs/itinerary-change")
public class ItineraryChangeController extends BaseController {
    
    @Autowired
    private IItineraryChangeService itineraryChangeService;
    
    /**
     * 查询已通过的出国申请列表
     */
    @GetMapping("/approvedApplications")
    @SaCheckPermission("rs:itineraryChange:add")
    public R<List<OverseasApplicationSimpleVO>> listApprovedApplications() {
        List<OverseasApplicationSimpleVO> list = itineraryChangeService.listApprovedApplications();
        return R.ok(list);
    }
    
    /**
     * 查询变更申请列表(分页)
     */
    @GetMapping("/list")
    @SaCheckPermission("rs:itineraryChange:list")
    public TableDataInfo<ItineraryChangeVO> list(ItineraryChangeQuery query) {
        startPage();
        List<ItineraryChangeVO> list = itineraryChangeService.selectChangeList(query);
        return getDataTable(list);
    }
    
    /**
     * 查询变更申请详情
     */
    @GetMapping("/{changeId}")
    @SaCheckPermission("rs:itineraryChange:query")
    public R<ItineraryChangeVO> getInfo(@PathVariable Long changeId) {
        return R.ok(itineraryChangeService.selectChangeById(changeId));
    }
    
    /**
     * 新增变更申请
     */
    @PostMapping
    @SaCheckPermission("rs:itineraryChange:add")
    public R<Map<String, Object>> add(@Validated @RequestBody ItineraryChangeForm form) {
        Long changeId = itineraryChangeService.insertChange(form);
        Map<String, Object> result = new HashMap<>();
        result.put("changeId", changeId);
        return R.ok(result, "提交成功");
    }
    
    /**
     * 修改并重新提交
     */
    @PutMapping("/{changeId}")
    @SaCheckPermission("rs:itineraryChange:edit")
    public R<Void> edit(@PathVariable Long changeId, @Validated @RequestBody ItineraryChangeForm form) {
        itineraryChangeService.updateChange(changeId, form);
        return R.ok("修改成功");
    }
    
    /**
     * 撤回变更申请
     */
    @PutMapping("/cancel/{changeId}")
    @SaCheckPermission("rs:itineraryChange:cancel")
    public R<Void> cancel(@PathVariable Long changeId, @RequestParam String cancelReason) {
        itineraryChangeService.cancelChange(changeId, cancelReason);
        return R.ok("撤回成功");
    }
    
    /**
     * 删除变更申请
     */
    @DeleteMapping("/{changeId}")
    @SaCheckPermission("rs:itineraryChange:remove")
    public R<Void> remove(@PathVariable Long changeId) {
        itineraryChangeService.deleteChangeById(changeId);
        return R.ok("删除成功");
    }
    
    /**
     * 导出变更申请列表
     */
    @PostMapping("/export")
    @SaCheckPermission("rs:itineraryChange:export")
    public void export(ItineraryChangeQuery query, HttpServletResponse response) {
        List<ItineraryChangeVO> list = itineraryChangeService.selectChangeList(query);
        ExcelUtil.exportExcel(list, "变更行程列表", ItineraryChangeVO.class, response);
    }
}

第三步:前端API对接(1小时)

1. 创建API文件

创建文件: web/src/api/rs/itinerary-change.ts

import request from '@/utils/request'

// 查询已通过的出国申请列表
export function listApprovedApplications() {
  return request({
    url: '/rs/itinerary-change/approvedApplications',
    method: 'get'
  })
}

// 查询变更申请列表
export function listItineraryChange(query: any) {
  return request({
    url: '/rs/itinerary-change/list',
    method: 'get',
    params: query
  })
}

// 查询变更申请详情
export function getItineraryChange(changeId: number) {
  return request({
    url: `/rs/itinerary-change/${changeId}`,
    method: 'get'
  })
}

// 新增变更申请
export function addItineraryChange(data: any) {
  return request({
    url: '/rs/itinerary-change',
    method: 'post',
    data: data
  })
}

// 修改并重新提交
export function updateItineraryChange(changeId: number, data: any) {
  return request({
    url: `/rs/itinerary-change/${changeId}`,
    method: 'put',
    data: data
  })
}

// 撤回变更申请
export function cancelItineraryChange(changeId: number, cancelReason: string) {
  return request({
    url: `/rs/itinerary-change/cancel/${changeId}`,
    method: 'put',
    params: { cancelReason }
  })
}

// 删除变更申请
export function delItineraryChange(changeId: number) {
  return request({
    url: `/rs/itinerary-change/${changeId}`,
    method: 'delete'
  })
}

// 导出变更申请列表
export function exportItineraryChange(query: any) {
  return request({
    url: '/rs/itinerary-change/export',
    method: 'post',
    data: query,
    responseType: 'blob'
  })
}

2. 修改前端页面导入API

在itinerary-change.vue文件顶部添加:

import {
  listApprovedApplications,
  listItineraryChange,
  getItineraryChange,
  addItineraryChange,
  updateItineraryChange,
  cancelItineraryChange,
  delItineraryChange,
  exportItineraryChange
} from '@/api/rs/itinerary-change'

然后替换所有的API调用。


第四步:功能测试(2小时)

1. 后端接口测试(1小时)

使用Postman或Apifox测试所有接口:

测试清单:

  • GET /rs/itinerary-change/approvedApplications
  • GET /rs/itinerary-change/list
  • GET /rs/itinerary-change/{changeId}
  • POST /rs/itinerary-change
  • PUT /rs/itinerary-change/{changeId}
  • PUT /rs/itinerary-change/cancel/{changeId}
  • DELETE /rs/itinerary-change/{changeId}
  • POST /rs/itinerary-change/export

测试要点:

  • ✅ 权限验证(无权限返回403)
  • ✅ 参数校验(必填参数缺失)
  • ✅ 业务校验(状态校验、重复提交校验等)
  • ✅ 数据正确性(返回数据格式正确)

2. 前后端联调测试(1小时)

测试流程:

  1. 新增变更申请:

    • 打开变更行程页面
    • 点击"新增变更申请"
    • 选择已通过的出国申请
    • 验证原申请信息自动填充
    • 选择变更类型,填写变更信息
    • 提交并验证成功
  2. 查询列表:

    • 验证列表数据正确显示
    • 测试搜索功能
    • 测试分页功能
  3. 撤回申请:

    • 找到待审核的申请
    • 点击撤回按钮
    • 输入撤回原因
    • 验证状态变为"已撤回"
  4. 删除申请:

    • 删除非已通过状态的申请
    • 验证删除成功
  5. 导出功能:

    • 点击导出按钮
    • 验证Excel文件下载
    • 检查导出数据正确性

开发时间估算

任务 预计时间 说明
数据库部署 0.5小时 执行SQL脚本、权限配置
后端API开发 4小时 Entity/VO/Mapper/Service/Controller
前端API对接 1小时 创建API文件、修改页面引用
功能测试 2小时 接口测试、联调测试
总计 7.5小时 约1个工作日

参考文档

  1. API文档: docs/开发文档/API/变更行程管理API.md
  2. 功能报告: ✅变更行程管理功能完成报告.md
  3. 数据库表: docs/设计文档/database/rs_overseas_itinerary_change.sql
  4. 菜单配置: docs/设计文档/database/rs_overseas_itinerary_change_menu.sql
  5. 前端页面: web/src/views/rs/overseas/itinerary-change.vue
  6. 后端规范: .cursor/cursorrules-backend.md
  7. 前端规范: .cursor/cursorrules-frontend.md

常见问题

Q1: 菜单不显示怎么办?

A:

  1. 检查菜单SQL是否执行成功
  2. 检查角色是否分配了菜单权限
  3. 清除浏览器缓存,重新登录
  4. 检查菜单的visible字段是否为'0'(显示)

Q2: 接口返回403权限不足?

A:

  1. 检查Controller方法是否添加了@SaCheckPermission注解
  2. 检查权限标识是否正确(rs:itineraryChange:xxx)
  3. 检查用户角色是否分配了相应的菜单权限

Q3: 前端API调用失败?

A:

  1. 检查后端服务是否启动
  2. 检查接口地址是否正确
  3. 打开浏览器开发者工具,查看Network请求详情
  4. 检查请求参数格式是否正确

Q4: 新增申请时提示"已存在待审核的变更申请"?

A:

  • 这是正常的业务校验
  • 需要先处理完待审核的申请(撤回或等待审批)
  • 然后才能提交新的变更申请

下一步建议

完成核心功能开发后,可以考虑以下增强功能:

  1. 审批流程集成 - 集成Warm-Flow工作流
  2. 消息通知 - 审批结果通知
  3. 数据统计 - 变更申请统计图表
  4. 移动端适配 - uni-app移动端页面

开发愉快! 🎉

如有问题,请参考API文档或联系开发团队。