“培训审批流程完成版v0.1”

This commit is contained in:
weizhong committed 2025-11-06 00:02:04 +08:00
1 parent f006de24f4
commit 2e4d35b41e
28 files changed
+238 -5596

No files matched your search

@@ -387,6 +387,10 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS
} else if (StpUtil.hasRole("rs-admin")) {
// 人事管理员可以查看所有申请
hasPermission = true;
} else if (hasAnyApprovalPermission()) {
// 审批人可以查看待审批的申请
hasPermission = true;
log.info("✅ 审批人 {} 有权查看申请 {}", currentUserId, applicationId);
} else {
// 默认情况:只能查看自己创建的申请
hasPermission = application.getCreateBy() != null &&
@@ -989,4 +993,45 @@ public class RsTrainingApplicationServiceImpl implements IRsTrainingApplicationS
return hasPermission;
}
/**
* 检查用户是否有任何审批级别的权限
*
* @return true-有审批权限,false-无审批权限
*/
private boolean hasAnyApprovalPermission() {
try {
// 检查是否有通用审批权限
if (StpUtil.hasPermission("rs:training:approve")) {
log.debug("用户拥有通用审批权限:rs:training:approve");
return true;
}
// 检查7个审批级别的权限(1-7级,2a-教务处,2b-学生处)
String[] approvalPermissions = {
"rs:training:approve1", // 第1级:部门负责人
"rs:training:approve2a", // 第2a级:教务处
"rs:training:approve2b", // 第2b级:学生处
"rs:training:approve3", // 第3级:计划财务处
"rs:training:approve4", // 第4级:人事处
"rs:training:approve5", // 第5级:业务分管领导
"rs:training:approve6", // 第6级:人事分管院领导
"rs:training:approve7" // 第7级:院长
};
for (String permission : approvalPermissions) {
if (StpUtil.hasPermission(permission)) {
log.debug("用户拥有审批权限:{}", permission);
return true;
}
}
log.debug("用户没有任何培训审批权限");
return false;
} catch (Exception e) {
log.error("检查审批权限失败", e);
return false;
}
}
}
@@ -273,48 +273,226 @@ public class RsTrainingApprovalServiceImpl implements IRsTrainingApprovalService
}
/**
* 处理审批
* 处理审批(7级审批流转逻辑)
*
* @param applicationId 申请ID
* @param status 审批状态
* @param status 审批状态(1-通过,2-拒绝)
* @param opinion 审批意见
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int processApproval(Long applicationId, String status, String opinion) {
log.info("🔄 开始处理审批 - applicationId: {}, status: {}", applicationId, status);
// 获取申请信息
RsTrainingApplication application = trainingApplicationMapper.selectById(applicationId);
if (application == null) {
log.error("❌ 申请不存在 - applicationId: {}", applicationId);
return 0;
}
// 验证申请状态
if (!"0".equals(application.getStatus())) {
log.error("❌ 只有待审核状态的申请才能审批 - applicationId: {}, status: {}",
applicationId, application.getStatus());
throw new RuntimeException("只有待审核状态的申请才能审批");
}
// 获取当前用户信息
Long userId = LoginHelper.getUserId();
String userName = LoginHelper.getUsername();
// 🔴 获取当前审批级别
String currentApprovalLevel = application.getApprovalLevel() != null ? application.getApprovalLevel() : "1";
log.info("📋 当前审批级别: {}, 培训类型: {}", currentApprovalLevel, application.getTrainingType());
// 🔴 验证审批人权限级别
if (!validateApproverLevel(userId, currentApprovalLevel)) {
log.error("❌ 权限验证失败 - 用户: {}, 需要第{}级审批权限", userName, currentApprovalLevel);
throw new RuntimeException(String.format(
"您没有第%s级审批权限,当前申请需要第%s级审批人审批",
currentApprovalLevel, currentApprovalLevel));
}
log.info("✅ 审批人 {} 拥有第{}级审批权限验证通过", userName, currentApprovalLevel);
// 创建审批记录 - MyBatis-Plus自动生成雪花ID
RsTrainingApproval approval = new RsTrainingApproval();
// 不需要手动设置ID,MyBatis-Plus会自动生成雪花ID
approval.setApplicationId(applicationId);
approval.setApproverId(userId);
approval.setApproverName(userName);
approval.setApprovalLevel(1); // 假设默认是第一级审批
// 🔴 设置正确的审批级别(从申请记录中获取)
try {
approval.setApprovalLevel(Integer.parseInt(currentApprovalLevel.replaceAll("[^0-9]", "")));
} catch (NumberFormatException e) {
// 如果级别中包含字母(如2a、2b),取第一个数字
approval.setApprovalLevel(Integer.parseInt(currentApprovalLevel.substring(0, 1)));
}
approval.setStatus(status); // 审批状态:1-通过,2-拒绝
approval.setOpinion(opinion); // 审批意见
approval.setApprovalTime(DateUtils.getNowDate());
approval.setCreateTime(DateUtils.getNowDate());
// 手动设置多租户字段
if (approval.getTenantId() == null || approval.getTenantId().isEmpty()) {
approval.setTenantId(LoginHelper.getTenantId());
}
// 插入审批记录 - 无论通过还是拒绝,都创建独立审批记录
int result = trainingApprovalMapper.insert(approval);
System.out.println("创建审批记录 - applicationId: " + applicationId + ", status: " + status + ", result: " + result);
log.info("✅ 创建审批记录成功 - approvalId: {}, applicationId: {}, status: {}",
approval.getApprovalId(), applicationId, status);
// 更新申请状态
trainingApplicationService.updateApplicationStatus(applicationId, status);
// 🔴 核心改进:根据审批结果更新申请状态和审批级别
if ("1".equals(status)) {
// 审批通过 - 流转到下一级
String nextLevel = getNextApprovalLevel(currentApprovalLevel, application.getTrainingType());
if (nextLevel != null) {
// 进入下一级审批
application.setApprovalLevel(nextLevel);
application.setCurrentApproverId(null); // 不设置固定审批人ID
application.setCurrentApprover(getApprovalLevelDescription(nextLevel));
log.info("✅ 审批流转到第{}级 - applicationId: {}, 下一级审批人: {}",
nextLevel, applicationId, application.getCurrentApprover());
} else {
// 所有级别审批完成
application.setStatus("1"); // 最终通过
application.setApprovalTime(DateUtils.getNowDate());
log.info("✅ 所有审批级别已完成,申请最终通过 - applicationId: {}", applicationId);
// 创建培训记录
try {
trainingApplicationService.updateApplicationStatus(applicationId, "1");
log.info("✅ 创建培训记录成功 - applicationId: {}", applicationId);
} catch (Exception e) {
log.error("❌ 创建培训记录失败 - applicationId: {}, 错误: {}", applicationId, e.getMessage(), e);
throw new RuntimeException("审批处理失败:创建培训记录时发生错误 - " + e.getMessage());
}
}
} else if ("2".equals(status)) {
// 审批拒绝
application.setStatus("2");
application.setRejectTime(DateUtils.getNowDate());
application.setApprovalTime(DateUtils.getNowDate());
log.info("❌ 审批被拒绝 - applicationId: {}", applicationId);
}
// 更新申请记录到数据库
application.setUpdateTime(DateUtils.getNowDate());
trainingApplicationMapper.updateById(application);
log.info("✅ 更新申请记录成功 - applicationId: {}, status: {}, approvalLevel: {}",
applicationId, application.getStatus(), application.getApprovalLevel());
return result;
}
/**
* 获取下一级审批级别(根据培训类型动态路由)
*
* 7级审批制度:
* 1-部门负责人 → 2a-教务处/2b-学生处/3-计划财务处(根据培训类型)
* 2a-教务处 → 3-计划财务处
* 2b-学生处 → 3-计划财务处
* 3-计划财务处 → 4-人事处
* 4-人事处 → 5-业务分管领导
* 5-业务分管领导 → 6-人事分管院领导
* 6-人事分管院领导 → 7-院长
* 7-院长 → 审批完成
*
* @param currentLevel 当前审批级别
* @param trainingType 培训类型
* @return 下一级审批级别
*/
private String getNextApprovalLevel(String currentLevel, String trainingType) {
switch (currentLevel) {
case "1":
// 部门负责人审批后,根据培训类型分流
if ("教学".equals(trainingType)) {
return "2a"; // 教务处
} else if ("德育".equals(trainingType)) {
return "2b"; // 学生处
} else {
// 行政、管理、其他 → 直接到计划财务处
return "3";
}
case "2a": // 教务处 → 计划财务处
case "2b": // 学生处 → 计划财务处
return "3";
case "3":
return "4"; // 计划财务处 → 人事处
case "4":
return "5"; // 人事处 → 业务分管领导
case "5":
return "6"; // 业务分管领导 → 人事分管院领导
case "6":
return "7"; // 人事分管院领导 → 院长
case "7":
return null; // 院长审批完成
default:
return null;
}
}
/**
* 根据审批级别获取审批环节描述
*
* @param approvalLevel 审批级别
* @return 审批环节描述
*/
private String getApprovalLevelDescription(String approvalLevel) {
switch (approvalLevel) {
case "1":
return "待部门负责人审批";
case "2a":
return "待教务处审批";
case "2b":
return "待学生处审批";
case "3":
return "待计划财务处审批";
case "4":
return "待人事处审批";
case "5":
return "待业务分管领导审批";
case "6":
return "待人事分管院领导审批";
case "7":
return "待院长审批";
default:
return "待审批";
}
}
/**
* 验证审批人权限级别
*
* @param approverId 审批人ID
* @param approvalLevel 审批级别
* @return 是否有权限
*/
private boolean validateApproverLevel(Long approverId, String approvalLevel) {
// 检查通用审批权限(超级管理员)
if (StpUtil.hasPermission("rs:training:approve")) {
log.info("✅ 用户 {} 拥有通用审批权限 rs:training:approve", approverId);
return true;
}
// 检查分级审批权限
String requiredPermission = "rs:training:approve" + approvalLevel;
boolean hasPermission = StpUtil.hasPermission(requiredPermission);
if (hasPermission) {
log.info("✅ 用户 {} 拥有第{}级审批权限 {}", approverId, approvalLevel, requiredPermission);
} else {
log.warn("❌ 用户 {} 缺少第{}级审批权限 {},权限验证失败",
approverId, approvalLevel, requiredPermission);
}
return hasPermission;
}
/**
* 应用数据权限过滤
*/
@@ -100,8 +100,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
a.approval_id, a.application_id, a.approver_id, a.approver_name,
a.approval_level, a.status, a.approval_time, a.opinion,
a.tenant_id, a.create_by, a.create_time, a.update_by, a.update_time, a.remark,
-- 审批记录列表显示字段
app.user_name as application_user_name,
-- 审批记录列表显示字段(使用用户昵称作为申请人姓名)
COALESCE(u.nick_name, app.user_name) as application_user_name,
COALESCE(app.course_name, c.title) as course_title,
-- 详情弹窗显示字段(使用别名避免重复)
app.user_name as user_name,
@@ -125,6 +125,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
FROM rs_training_approval a
LEFT JOIN rs_training_application app ON a.application_id = app.application_id AND app.deleted = '0'
LEFT JOIN rs_training_course c ON app.course_id = c.course_id AND c.deleted = '0'
LEFT JOIN sys_user u ON app.user_id = u.user_id AND u.deleted = '0'
<where>
a.deleted = '0'
<if test="applicationId != null">
@@ -205,6 +206,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
FROM rs_training_approval a
LEFT JOIN rs_training_application app ON a.application_id = app.application_id AND app.deleted = '0'
LEFT JOIN rs_training_course c ON app.course_id = c.course_id AND c.deleted = '0'
LEFT JOIN sys_user u ON app.user_id = u.user_id AND u.deleted = '0'
<where>
a.deleted = '0'
<if test="applicationId != null">
@@ -1,367 +0,0 @@
# 党籍保留附件上传功能测试指南
## 📋 功能说明
**功能名称**:党籍保留附件上传
**实现日期**:2025-10-10
**适用模块**:出国申请管理
### 业务场景
- 当申请人是**中共党员**且**出国天数超过30天**时,需要上传党籍保留附件
- 附件需要经过党组织审批并盖章后才能上传
- 支持PDF、JPG、PNG格式,文件大小限制10MB
---
## 🔧 部署前准备
### 1. 数据库更新
**执行SQL脚本**:
```bash
路径:docs/设计文档/database/【立即执行】添加党籍保留附件字段.sql
```
**操作步骤**:
1. 连接到 `hzdyjs_xg` 数据库
2. 执行整个SQL脚本
3. 检查执行结果,确认4个字段添加成功:
- `party_retention_file_path` - 党籍保留附件文件路径
- `party_retention_file_name` - 党籍保留附件文件名
- `party_retention_upload_time` - 党籍保留附件上传时间
- `party_retention_upload_status` - 党籍保留附件上传状态
### 2. 后端代码部署
**修改文件清单**:
1. ✅ `RsOverseasApplicationController.java` - 新增 `uploadPartyRetentionFile` 接口
2. ✅ `IRsOverseasApplicationService.java` - 新增接口方法声明
3. ✅ `RsOverseasApplicationServiceImpl.java` - 新增接口方法实现
4. ✅ `RsOverseasApplication.java` - 新增4个实体字段
**部署步骤**:
```bash
# 1. 切换到后端目录
cd /Users/weizzzz/Documents/Cursor/0828CURSOR/all-project/backend
# 2. 编译打包
mvn clean package -DskipTests
# 3. 重启后端服务
# 根据实际部署方式重启服务
```
### 3. 前端无需修改
前端代码已经包含党籍保留附件上传功能,无需修改。
---
## 🧪 功能测试步骤
### 测试场景1:符合上传条件的申请
#### 前置条件
- 申请人的政治面貌为:**中共党员**
- 出国天数设置为:**31天或以上**
#### 测试步骤
1. **登录系统**
- 使用测试账号登录前端系统
- 导航到:`人事管理` → `出国申请`
2. **创建新申请**
- 点击"新增申请"按钮
- 填写基本信息:
- 申请人姓名:测试用户
- 所属部门:技术部
- 政治面貌:**中共党员**
- 出国类型:因公出国
- 目的地:美国
- 出国天数:**35天**(超过30天)
3. **验证上传区域显示**
- ✅ 应该自动显示"党籍保留附件上传"区域
- ✅ 显示红色必填提示
- ✅ 显示上传按钮和说明文字
4. **上传附件**
- 点击"上传党籍保留附件"按钮
- 选择一个PDF或图片文件(小于10MB)
- 观察上传过程
5. **验证上传结果**
- ✅ 上传成功后显示绿色成功提示:"党籍保留附件上传成功!"
- ✅ 文件列表中显示已上传的文件
- ✅ 可以预览和删除文件
- ✅ 按钮文字变为"重新上传"
6. **提交申请**
- 填写其他必填项
- 点击"提交"按钮
- ✅ 申请提交成功
### 测试场景2:不符合上传条件的申请
#### 测试用例2.1:政治面貌为群众
- 政治面貌选择:**群众**
- 出国天数:35天
- ✅ **不应该**显示党籍保留附件上传区域
#### 测试用例2.2:出国天数≤30天
- 政治面貌选择:**中共党员**
- 出国天数:**28天**
- ✅ **不应该**显示党籍保留附件上传区域
### 测试场景3:文件格式验证
#### 测试用例3.1:支持的格式
- 上传 `.pdf` 文件 → ✅ 应该成功
- 上传 `.jpg` 文件 → ✅ 应该成功
- 上传 `.png` 文件 → ✅ 应该成功
#### 测试用例3.2:不支持的格式
- 上传 `.doc` 文件 → ❌ 提示"只支持PDF、JPG、PNG格式的文件!"
- 上传 `.xlsx` 文件 → ❌ 提示"只支持PDF、JPG、PNG格式的文件!"
### 测试场景4:文件大小验证
- 上传 5MB 文件 → ✅ 应该成功
- 上传 9.9MB 文件 → ✅ 应该成功
- 上传 11MB 文件 → ❌ 提示"文件大小不能超过 10MB"
### 测试场景5:API接口测试
#### 使用Postman或curl测试
**请求示例**:
```bash
POST /rs/overseas/application/uploadPartyRetentionFile
Headers:
Authorization: Bearer {token}
Content-Type: multipart/form-data
Body (form-data):
file: [选择文件]
applicationId: 1234567890
```
**成功响应**:
```json
{
"code": 200,
"msg": "党籍保留附件上传成功",
"data": {
"filePath": "https://oss.example.com/overseas/party-retention/party_retention_1234567890_1696935234567.pdf",
"fileName": "党籍保留附件.pdf",
"uploadTime": "2025-10-10 15:30:45"
}
}
```
**失败响应示例**:
```json
{
"code": 500,
"msg": "不支持的文件格式,请上传PDF、JPG或PNG文件"
}
```
---
## 🔍 数据验证
### 1. 数据库验证
**查询上传记录**:
```sql
SELECT
application_id,
applicant_name,
party_retention_file_path,
party_retention_file_name,
party_retention_upload_time,
party_retention_upload_status,
political_status,
duration
FROM rs_overseas_application
WHERE party_retention_upload_status = '1'
ORDER BY party_retention_upload_time DESC
LIMIT 10;
```
**验证点**:
- ✅ `party_retention_file_path` 应该有完整的OSS路径
- ✅ `party_retention_file_name` 应该是原始文件名
- ✅ `party_retention_upload_time` 应该是当前时间
- ✅ `party_retention_upload_status` 应该是 `'1'`
### 2. OSS存储验证
**文件存储路径**:
```
overseas/party-retention/party_retention_{applicationId}_{timestamp}.{ext}
```
**验证方式**:
- 登录OSS管理控制台
- 导航到 `overseas/party-retention/` 目录
- 确认文件已正确上传
- 验证文件可以正常下载和预览
---
## 🚨 常见问题排查
### 问题1:点击上传按钮后提示"Request method 'POST' is not supported"
**原因**:后端接口未部署或URL路径不正确
**排查步骤**:
1. 检查后端服务是否已重启
2. 检查接口路径:`/rs/overseas/application/uploadPartyRetentionFile`
3. 确认Controller中的 `@PostMapping` 注解配置正确
4. 查看后端日志,确认请求是否到达Controller
### 问题2:上传成功但数据库没有记录
**原因**:数据库字段未添加或Service层更新失败
**排查步骤**:
1. 执行SQL验证字段是否存在:
```sql
SHOW COLUMNS FROM rs_overseas_application
WHERE Field LIKE 'party_retention%';
```
2. 检查后端日志中的异常信息
3. 确认Service层的 `updatePartyRetentionFileInfo` 方法被正确调用
### 问题3:文件上传失败,提示"系统异常"
**原因**:OSS配置问题或网络问题
**排查步骤**:
1. 检查OSS配置是否正确(application.yml)
2. 确认OSS AccessKey和SecretKey有效
3. 检查服务器到OSS的网络连接
4. 查看后端详细错误日志
### 问题4:上传区域不显示
**原因**:条件判断逻辑问题
**排查步骤**:
1. 确认政治面貌是否选择了"中共党员"
2. 确认出国天数是否>30天
3. 打开浏览器开发者工具,检查 `showPartyRetentionUpload` 的值
4. 检查前端计算逻辑是否正确
---
## ✅ 测试检查清单
### 功能性测试
- [ ] 符合条件时显示上传区域
- [ ] 不符合条件时不显示上传区域
- [ ] 文件格式验证正确(PDF、JPG、PNG)
- [ ] 文件大小验证正确(≤10MB)
- [ ] 上传成功后显示文件信息
- [ ] 可以重新上传替换文件
- [ ] 可以删除已上传文件
- [ ] 提交申请时数据正确保存
### 接口测试
- [ ] POST /uploadPartyRetentionFile 返回200
- [ ] 响应数据格式正确
- [ ] 错误处理正确(格式错误、大小超限等)
### 数据完整性
- [ ] 数据库字段值正确
- [ ] OSS文件存储成功
- [ ] 文件路径可访问
- [ ] 文件可以正常预览和下载
### 用户体验
- [ ] 上传过程有加载提示
- [ ] 成功/失败消息清晰友好
- [ ] 上传按钮状态正确切换
- [ ] 文件列表显示完整
---
## 📝 测试报告模板
```markdown
## 党籍保留附件上传功能测试报告
**测试日期**:2025-10-10
**测试人员**:[测试人员姓名]
**测试环境**:开发环境 / 测试环境 / 生产环境
### 测试结果汇总
- 测试用例总数:15
- 通过用例数:__
- 失败用例数:__
- 阻塞用例数:__
### 详细测试结果
#### 1. 上传条件判断
- [✅/❌] 中共党员 + 出国>30天:显示上传区域
- [✅/❌] 群众:不显示上传区域
- [✅/❌] 出国≤30天:不显示上传区域
#### 2. 文件上传功能
- [✅/❌] PDF文件上传成功
- [✅/❌] JPG文件上传成功
- [✅/❌] PNG文件上传成功
- [✅/❌] 不支持格式被拦截
- [✅/❌] 超大文件被拦截
#### 3. 数据存储
- [✅/❌] 数据库记录正确
- [✅/❌] OSS文件存储成功
- [✅/❌] 文件路径可访问
#### 4. 异常情况
- [✅/❌] 网络异常处理
- [✅/❌] 重复上传处理
- [✅/❌] 删除文件处理
### 发现的问题
1. [问题描述]
- 严重程度:高/中/低
- 复现步骤:...
- 期望结果:...
- 实际结果:...
### 测试结论
[ ] 通过,可以发布
[ ] 通过,但有建议改进项
[ ] 不通过,需要修复问题
### 备注
[其他说明]
```
---
## 🎯 验收标准
功能通过以下所有标准即可验收:
1. ✅ 当申请人是中共党员且出国天数>30天时,必须显示党籍保留附件上传区域
2. ✅ 不符合条件时,不显示上传区域,不影响正常申请流程
3. ✅ 文件上传成功后,数据库正确保存4个字段的值
4. ✅ 上传的文件正确存储在OSS的 `overseas/party-retention/` 目录下
5. ✅ 文件格式和大小验证正确,错误提示友好
6. ✅ 可以重新上传替换文件,旧文件被覆盖
7. ✅ 所有操作有明确的成功/失败提示
8. ✅ 不影响现有出国申请的其他功能
---
**文档维护人**:Cursor AI
**最后更新**:2025-10-10
**版本**:v1.0
@@ -1,87 +0,0 @@
# 出国申请用户操作指引
## 📋 **概述**
本指引将帮助您完成出国申请的完整流程,从新增申请到审批完成。
---
## 🔄 **完整业务流程**
### **第一阶段:提交申请**
1. 访问:`http://localhost/rs/overseas`
2. 点击:**"新增申请"** 按钮
3. 填写:申请表单信息
4. 点击:**"确定"** 按钮
5. 系统提示:**"申请提交成功!您的申请已进入审批流程,请耐心等待审批结果。"**
**📌 重要说明**:申请立即进入 **待审核状态**,无需额外操作!
### **第二阶段:审批处理**
1. 访问审批页面:`http://localhost/rs/overseasApproval`
2. 有审批权限的用户可以在 **"待审批申请"** 选项卡中看到您的申请
3. 申请状态显示:**"待审核"**(橙色标签)
4. 审批人进行审批操作
### **第三阶段:审批结果**
1. 审批完成后,申请状态会更新为:**"已通过"** 或 **"已拒绝"**
2. 您可以在申请列表中查看最新状态
3. 如果通过,系统会自动创建出国记录
---
## 🎯 **状态说明**
| 状态 | 显示文本 | 标签颜色 | 说明 | 可用操作 |
|------|----------|----------|------|----------|
| `0` | 待审核 | 橙色 | 申请已提交,等待审批 | 查看、取消、删除 |
| `1` | 已通过 | 绿色 | 申请审批通过 | 查看 |
| `2` | 已拒绝 | 红色 | 申请被拒绝 | 查看 |
| `3` | 已取消 | 灰色 | 申请被取消 | 查看 |
| `4` | 已完成 | 绿色 | 出国活动已完成 | 查看 |
---
## ⚠️ **常见问题**
### **Q1: 新增申请后多久能在审批页面看到?**
**A**: 新增申请后立即进入审批流程,有审批权限的用户可以立即在审批页面看到您的申请。
### **Q2: 如何知道申请是否提交成功?**
**A**:
- 系统提示:"申请提交成功!您的申请已进入审批流程,请耐心等待审批结果。"
- 申请列表中显示"待审核"标签(橙色)
### **Q3: 提交后可以修改申请吗?**
**A**: 申请提交后不能修改,只能取消申请。如需修改,请先取消当前申请,然后重新提交。
### **Q4: 谁可以审批我的申请?**
**A**: 拥有出国申请审批权限(`rs:overseas:application:approve`)的用户可以在审批页面看到并处理您的申请。
---
## 🛠️ **操作按钮说明**
### **申请列表页面按钮**
- **👁️ 查看**:查看申请详情(所有状态都可用)
- **❌ 取消**:取消申请(仅待审核状态可用)
- **🗑️ 删除**:删除申请(待审核状态可用)
### **审批页面按钮**
- **👁️ 查看**:查看申请详情
- **✅ 通过**:审批通过
- **❌ 拒绝**:审批拒绝
---
## 📞 **技术支持**
如果您在使用过程中遇到问题,请联系系统管理员或查看相关技术文档:
- **API文档**: `docs/开发文档/API/出国申请管理API.md`
- **技术规范**: `.cursor/cursorrules-frontend.md`
- **变更记录**: `docs/变更管理/code-adjust-record-2025-08-03-weizzzz.md`
---
**✅ 完成时间**: 2025-08-03
**📝 版本**: v2.0
**🔄 最后更新**: 简化业务流程,新增申请直接进入审批状态
@@ -1,386 +0,0 @@
# 出国申请附件下载权限修复方案
## 📋 问题描述
### 错误现象
用户点击出国申请详情中的"出国境登记表盖章文件"和"党籍保留附件"下载按钮时,出现以下错误:
```xml
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied.</Message>
<Key>overseas/stamped/stamped_0_1760082853042.png</Key>
<BucketName>ruoyi</BucketName>
<Resource>/ruoyi/overseas/stamped/stamped_0_1760082853042.png</Resource>
</Error>
```
### 根本原因
1. **OSS Bucket权限配置**:OSS bucket设置为私有访问,直接使用URL无法访问
2. **前端下载方式错误**:前端直接使用文件URL进行下载,绕过了权限验证
3. **缺少OSS ID存储**:数据库中只存储了文件路径,没有存储OSS文件ID,无法使用系统的安全下载接口
## 🎯 解决方案
### 整体架构
```
前端下载请求 → 后端代理接口 → 权限验证 → OSS安全下载 → 返回文件流
```
### 技术方案
采用**后端代理下载 + OSS ID存储**的方式,确保:
- ✅ 所有下载请求经过后端权限验证
- ✅ 使用系统统一的OSS服务管理文件
- ✅ 文件访问可追踪、可审计
- ✅ 支持私有bucket安全访问
## 🔧 实施步骤
### 1️⃣ 数据库改造
**新增字段**:`stamped_file_oss_id` 和 `party_retention_file_oss_id`
```sql
-- 添加盖章文件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `stamped_file_oss_id` BIGINT(20) NULL COMMENT '盖章文件OSS ID'
AFTER `stamped_file_upload_status`;
-- 添加党籍保留附件OSS ID字段
ALTER TABLE `rs_overseas_application`
ADD COLUMN `party_retention_file_oss_id` BIGINT(20) NULL COMMENT '党籍保留附件OSS ID'
AFTER `party_retention_upload_status`;
-- 添加索引以提升查询性能
ALTER TABLE `rs_overseas_application`
ADD INDEX `idx_stamped_file_oss_id` (`stamped_file_oss_id`),
ADD INDEX `idx_party_retention_file_oss_id` (`party_retention_file_oss_id`);
```
**执行位置**:`docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql`
### 2️⃣ 后端Domain实体改造
#### RsOverseasApplication.java
```java
/**
* 盖章文件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
@ExcelIgnore
private Long partyRetentionFileOssId;
```
#### RsOverseasApplicationVo.java
```java
/**
* 盖章文件OSS ID(用于安全下载)
*/
private Long stampedFileOssId;
/**
* 党籍保留附件OSS ID(用于安全下载)
*/
private Long partyRetentionFileOssId;
```
### 3️⃣ 后端上传接口改造
**改造前**:使用`OssClient`直接上传,只返回文件URL
```java
// 旧代码
OssClient ossClient = OssFactory.instance();
UploadResult uploadResult = ossClient.upload(file.getInputStream(), filePath,
file.getSize(), file.getContentType());
applicationService.updateStampedFileInfo(applicationId, uploadResult.getUrl(),
originalFilename, new Date(), "1");
```
**改造后**:使用`ISysOssService`上传,自动保存到`sys_oss`表并返回OSS ID
```java
// 新代码 - 使用系统OSS服务
@Autowired
private ISysOssService ossService;
SysOssVo ossResult = ossService.upload(file);
// 更新申请记录 - 存储OSS ID和文件路径
applicationService.updateStampedFileInfo(
applicationId,
ossResult.getUrl(), // 文件URL
ossResult.getOriginalName(), // 原始文件名
new Date(), // 上传时间
"1", // 上传状态:已上传
ossResult.getOssId() // ✨ OSS文件ID(新增)
);
```
**涉及接口**:
- `POST /rs/overseas/application/uploadStampedFile` - 盖章文件上传
- `POST /rs/overseas/application/uploadPartyRetentionFile` - 党籍保留附件上传
### 4️⃣ 后端Service层改造
#### IRsOverseasApplicationService.java
```java
// 添加ossId参数
Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
```
#### RsOverseasApplicationServiceImpl.java
```java
@Override
@Transactional
public Boolean updateStampedFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId) {
return baseMapper.update(null,
new LambdaUpdateWrapper<RsOverseasApplication>()
.eq(RsOverseasApplication::getApplicationId, applicationId)
.set(RsOverseasApplication::getStampedRegistrationFilePath, filePath)
.set(RsOverseasApplication::getStampedRegistrationFileName, fileName)
.set(RsOverseasApplication::getStampedFileUploadTime, uploadTime)
.set(RsOverseasApplication::getStampedFileUploadStatus, uploadStatus)
.set(RsOverseasApplication::getStampedFileOssId, ossId) // ✨ 新增
) > 0;
}
```
### 5️⃣ 后端新增下载代理接口
#### RsOverseasApplicationController.java
**盖章文件下载**:
```java
/**
* 下载盖章文件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadStampedFile/{applicationId}")
public void downloadStampedFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getStampedFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getStampedFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**党籍保留附件下载**:
```java
/**
* 下载党籍保留附件(安全下载)
*/
@SaCheckPermission("rs:overseas:query")
@GetMapping("/downloadPartyRetentionFile/{applicationId}")
public void downloadPartyRetentionFile(@PathVariable Long applicationId, HttpServletResponse response) {
try {
RsOverseasApplicationVo application = applicationService.queryById(applicationId);
if (application == null || application.getPartyRetentionFileOssId() == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":404,\"msg\":\"文件不存在\"}");
return;
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getPartyRetentionFileOssId(), response);
} catch (Exception e) {
e.printStackTrace();
}
}
```
**核心特性**:
- ✅ 权限验证:`@SaCheckPermission("rs:overseas:query")`
- ✅ 文件存在性检查
- ✅ 使用系统统一的`ISysOssService.secureDownload()`方法
- ✅ 自动处理文件流返回
### 6️⃣ 前端下载逻辑改造
#### index.vue
**改造前**:直接使用文件URL下载
```javascript
// 旧代码 - 直接访问OSS URL(会被拒绝)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
const link = document.createElement('a');
link.href = data.stampedRegistrationFilePath; // ❌ 直接使用OSS URL
link.download = data.stampedRegistrationFileName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
```
**改造后**:调用后端代理接口
```javascript
// 新代码 - 通过后端代理下载(安全)
const handleDownloadStampedFile = (data: OverseasApplicationVO) => {
console.log('📥 下载出国境登记表盖章文件:', data.stampedRegistrationFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadStampedFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
// 党籍保留附件下载(同样改造)
const handleDownloadPartyRetentionFile = (data: OverseasApplicationVO) => {
console.log('📥 下载党籍保留附件:', data.partyRetentionFileName);
if (!data.applicationId) {
ElMessage.warning('申请ID不存在');
return;
}
// ✅ 使用后端代理下载接口
const downloadUrl = `/api/rs/overseas/application/downloadPartyRetentionFile/${data.applicationId}`;
window.open(downloadUrl, '_blank');
ElMessage.success('文件下载已开始');
};
```
## 📊 改造效果
### 安全性提升
| 项目 | 改造前 | 改造后 |
|------|--------|--------|
| 权限验证 | ❌ 无 | ✅ Sa-Token权限验证 |
| 访问控制 | ❌ 任何人可访问URL | ✅ 必须登录且有权限 |
| 访问日志 | ❌ 无 | ✅ 自动记录访问日志 |
| Bucket类型 | ⚠️ 必须公开 | ✅ 可使用私有bucket |
### 技术优势
- ✅ **统一管理**:使用系统统一的`ISysOssService`服务
- ✅ **可扩展性**:支持断点续传、缓存等高级特性
- ✅ **可追踪性**:所有文件访问都有日志记录
- ✅ **规范一致**:与系统其他模块保持一致的文件处理方式
## 🚀 部署步骤
### 1. 执行数据库迁移
```bash
# 连接到数据库
mysql -u root -p hzdyjs_xg
# 执行SQL脚本
source docs/设计文档/database/【立即执行】出国申请表添加OSS_ID字段.sql
```
### 2. 重启后端服务
```bash
# 停止服务
# 重新编译打包
# 启动服务
```
### 3. 刷新前端
```bash
# 清除浏览器缓存
# 重新访问页面
```
### 4. 验证功能
- [ ] 上传新的盖章文件,检查是否保存了OSS ID
- [ ] 上传新的党籍保留附件,检查是否保存了OSS ID
- [ ] 测试下载功能,确认可以正常下载
- [ ] 检查下载时的权限验证是否生效
## 📝 注意事项
### 数据兼容性
⚠️ **已存在的文件处理**:
- 旧数据:没有OSS ID的记录无法通过新接口下载
- 解决方案:
1. 方案一:保留旧的下载方式作为降级方案
2. 方案二:编写数据迁移脚本,为旧文件生成OSS ID
3. 方案三(推荐):让用户重新上传文件
### OSS配置检查
确保系统OSS配置正确:
```yaml
# application.yml
oss:
endpoint: xxx
accessKeyId: xxx
accessKeySecret: xxx
bucketName: ruoyi
region: xxx
```
### 权限配置
确保菜单权限配置正确:
- `rs:overseas:query` - 查看出国申请权限(含下载)
- `rs:overseas:add` - 新增出国申请权限(含上传)
## 🔍 故障排查
### 问题1:下载时提示"文件不存在"
**排查步骤**:
1. 检查数据库中的`stamped_file_oss_id`或`party_retention_file_oss_id`字段是否有值
2. 检查`sys_oss`表中是否存在对应的OSS ID记录
3. 检查OSS中是否真实存在该文件
### 问题2:下载时提示"无权限"
**排查步骤**:
1. 检查用户是否登录
2. 检查用户是否有`rs:overseas:query`权限
3. 检查Sa-Token配置是否正确
### 问题3:下载速度慢
**可能原因**:
- OSS服务器距离远
- 文件较大
- 网络带宽限制
**优化方案**:
- 启用CDN加速
- 使用分片下载
- 优化网络配置
## 📚 相关文档
- [系统OSS服务文档](./OSS文件管理服务.md)
- [Sa-Token权限验证文档](./权限管理文档.md)
- [出国申请API文档](../API/出国申请管理API.md)
## 🎯 总结
本次改造通过引入OSS ID存储和后端代理下载机制,彻底解决了出国申请附件下载权限问题。改造后的系统:
- ✅ 安全性更高:所有下载都经过权限验证
- ✅ 可维护性更好:使用系统统一的OSS服务
- ✅ 可追踪性更强:所有文件访问都有日志
- ✅ 扩展性更好:支持更多高级特性
**日期**:2025-01-10
**作者**:AI Assistant
**版本**:v1.0
@@ -1,211 +0,0 @@
# rs_overseas_record_status_backup 表字符集乱码修复报告
## 📋 问题概述
**发现时间**: 2025-01-08
**问题类型**: 数据库字符集配置错误导致中文乱码
**影响范围**: `rs_overseas_record_status_backup` 表的 `old_status_text` 字段
**严重程度**: 🔴 高 - 影响中文数据存储和显示
---
## 🔍 问题详情
### 问题现象
- `rs_overseas_record_status_backup` 表中的 `old_status_text` 字段存在中文乱码
- 中文状态文本无法正确存储和显示
### 问题根因
通过检查表结构发现字符集配置不一致:
**错误配置**:
```sql
`old_status_text` varchar(12) CHARACTER SET latin1 NOT NULL DEFAULT ''
```
**表默认字符集**:
```sql
DEFAULT CHARSET=utf8mb4
```
### 违规说明
该配置违反了项目数据库设计规范:
- **项目要求**: 统一使用 `utf8mb4` 字符集
- **实际情况**: `old_status_text` 字段单独使用 `latin1` 字符集
- **问题影响**: `latin1` 不支持中文字符,导致数据乱码
---
## ✅ 修复方案
### 1. 字符集修复SQL
```sql
-- 修复字符集问题
ALTER TABLE `rs_overseas_record_status_backup`
MODIFY COLUMN `old_status_text` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '原状态文本';
```
### 2. 验证修复结果
```sql
-- 查看表结构验证修复
SHOW FULL COLUMNS FROM rs_overseas_record_status_backup;
-- 检查字符集设置
SELECT
COLUMN_NAME,
DATA_TYPE,
CHARACTER_SET_NAME,
COLLATION_NAME,
COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'hzdyjs_xg'
AND TABLE_NAME = 'rs_overseas_record_status_backup'
AND COLUMN_NAME = 'old_status_text';
```
### 3. 数据内容修复(重要!)
```sql
-- 设置正确的字符集连接(关键步骤)
SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
-- 清空原有乱码数据
DELETE FROM rs_overseas_record_status_backup;
-- 根据状态码重新生成正确的中文状态文本
INSERT INTO rs_overseas_record_status_backup (record_id, completion_status, old_status_text, create_time) VALUES
(2, '1', '已完成', NOW()),
(3, '1', '已完成', NOW()),
(4, '0', '进行中', NOW());
```
### 4. 数据验证测试
```sql
-- 设置字符集(每次连接都需要)
SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
-- 验证中文数据正确性
SELECT record_id, completion_status, old_status_text,
CHAR_LENGTH(old_status_text) as char_len,
LENGTH(old_status_text) as byte_len,
HEX(old_status_text) as hex_value
FROM rs_overseas_record_status_backup;
-- 期望结果:
-- "已完成": 3字符, 9字节, HEX: E5B7B2E5AE8CE68890
-- "进行中": 3字符, 9字节, HEX: E8BF9BE8A18CE4B8AD
```
---
## 📊 修复执行清单
### ✅ 已完成
- [x] 检测到字符集配置问题
- [x] 分析问题根本原因
- [x] 创建修复SQL脚本
- [x] 更新主SQL文件中的表定义
- [x] 创建问题修复报告
### ✅ 已执行完成
- [x] 备份现有数据(生产环境建议)
- [x] 执行字符集修复SQL
- [x] 验证修复结果
- [x] 测试中文数据存储
---
## 🚨 重要说明
### 数据安全提醒
1. **生产环境操作前务必备份**:
```sql
CREATE TABLE rs_overseas_record_status_backup_20250108 AS
SELECT * FROM rs_overseas_record_status_backup;
```
2. **乱码数据处理**:
- 如果表中已有乱码数据,修复字符集后需要重新录入正确的中文数据
- 原有的乱码数据可能无法自动修复
3. **业务影响**:
- 修复过程中不会影响表的其他功能
- 仅影响 `old_status_text` 字段的中文显示
---
## 📁 相关文件
- **修复脚本**: `docs/设计文档/database/字符集修复/rs_overseas_record_status_backup_charset_fix.sql`
- **主SQL文件**: `docs/设计文档/database/hzdyjs_xg0820.sql` (已更新)
- **数据库规范**: `docs/设计文档/database/mysql-database-standard.md`
---
## 🎯 预防措施
### 今后建表规范
1. **统一字符集**: 所有字段统一使用 `utf8mb4`
2. **避免单独设置**: 不要为单个字段单独设置字符集
3. **遵循规范**: 严格按照 `mysql-database-standard.md` 执行
### 检查命令
```sql
-- 检查数据库中所有非utf8mb4字符集的字段
SELECT
TABLE_SCHEMA,
TABLE_NAME,
COLUMN_NAME,
CHARACTER_SET_NAME,
COLLATION_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'hzdyjs_xg'
AND CHARACTER_SET_NAME != 'utf8mb4'
AND CHARACTER_SET_NAME IS NOT NULL;
```
---
**修复状态**: ✅ 已完成 - 字符集和数据内容修复成功,中文数据可以正常存储和显示
**执行时间**: 2025-01-08 15:30
**修复过程**:
1. **字符集修复**: 已成功从 `latin1_swedish_ci` 修改为 `utf8mb4_general_ci`
2. **数据内容修复**: 发现原有数据为双重编码乱码,已根据状态码重新生成正确内容
3. **连接字符集修复**: 使用 `SET NAMES utf8mb4` 确保客户端连接使用正确字符集
**验证结果**:
- ✅ 表字段字符集: `utf8mb4_general_ci`
- ✅ 中文数据显示: "已完成"(3字符,9字节)、"进行中"(3字符,9字节)
- ✅ UTF-8编码验证: E5B7B2E5AE8CE68890 → "已完成"
- ✅ 状态码映射: 0→进行中, 1→已完成, 2→已拒绝
---
## 💡 经验总结
### 🔑 关键发现
1. **服务器字符集问题**: MySQL服务器默认使用 `latin1` 字符集,即使客户端指定 `utf8mb4` 也会被覆盖
2. **双重编码乱码**: 原有数据已经是双重编码的乱码,无法通过简单的字符集转换修复
3. **连接字符集设置**: 必须在每次连接时使用 `SET NAMES utf8mb4` 确保正确的字符集
### 🛠️ 标准修复流程
```sql
-- 1. 修复表字段字符集
ALTER TABLE table_name MODIFY COLUMN column_name varchar(length) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 2. 设置连接字符集(关键步骤)
SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
-- 3. 重新生成正确数据(如果原数据已乱码)
-- 根据业务逻辑重新生成中文内容
-- 4. 验证修复效果
SELECT column_name, HEX(column_name), CHAR_LENGTH(column_name) FROM table_name;
```
### 📋 预防措施
- **数据库配置**: 确保MySQL服务器默认字符集为 `utf8mb4`
- **建表规范**: 所有表和字段统一使用 `utf8mb4` 字符集
- **连接配置**: 应用程序连接时必须设置正确的字符集
- **数据验证**: 定期检查字符集一致性,防止乱码问题
**🎯 修复完成**: 该问题已彻底解决,为后续类似问题提供了标准解决方案。
@@ -1,149 +0,0 @@
# 出国申请生成登记表按钮不显示问题解决方案
## 📋 问题描述
用户反馈:在出国申请列表页面中,没有看到"生成登记表"按钮以及相关的PDF生成界面功能(下载、打印、全屏预览)。
## 🔍 问题分析
### 1. 前端实现检查 ✅
经过检查,前端实现是**完整**的:
#### 按钮实现
- **位置**: `web/src/views/rs/overseas/index.vue` 第119-126行
- **代码**:
```vue
<el-tooltip content="生成登记表" placement="top" v-if="canGenerateRegistrationForm(scope.row)">
<el-button
link
type="success"
icon="Document"
@click="handleGenerateRegistrationForm(scope.row)"
v-hasPermi="['rs:overseas:generate']"
></el-button>
</el-tooltip>
```
#### 弹窗界面 ✅
- **位置**: 第610-750行
- **功能**: 完整的PDF生成界面,包含申请信息预览、生成选项、文档预览
- **操作按钮**: 下载PDF、直接打印、全屏预览 (第712-736行)
#### API接口 ✅
- **API文件**: `web/src/api/rs/overseas.ts`
- **接口**: `generateRegistrationForm`、`downloadRegistrationFormPdf`、`previewRegistrationFormPdf`
- **导入**: 已在页面中正确导入 (第787-788行)
#### 业务逻辑 ✅
- **显示条件**: `canGenerateRegistrationForm()` - 已提交、已通过或已完成的申请可生成
- **处理函数**: `handleGenerateForm`、`handleDownloadPdf`、`handlePrintPdf`、`handleFullPreviewPdf`
### 2. 后端实现检查 ✅
后端功能也是完整的:
- **控制器**: `RsOverseasApplicationController.generateRegistrationForm()`
- **服务层**: `RegistrationFormServiceImpl` (已修复编译错误)
- **权限注解**: `@SaCheckPermission("rs:overseas:generate")`
### 3. 问题根本原因 🚨
**权限配置缺失!**
- **前端权限检查**: `v-hasPermi="['rs:overseas:generate']"`
- **数据库权限**: 缺少 `rs:overseas:generate` 权限配置
- **结果**: 按钮因权限不足而隐藏
## 🛠️ 解决方案
### 方案1: 数据库权限配置 (推荐)
#### 步骤1: 执行权限配置SQL
```sql
-- 使用简化版配置脚本
-- 文件位置: backend/ruoyi-modules/ruoyi-rs/出国申请生成登记表权限配置_简化版.sql
-- 1. 查看出国申请菜单ID
SELECT menu_id, menu_name, perms FROM sys_menu WHERE menu_name LIKE '%出国%' AND menu_type = 'C';
-- 2. 设置菜单ID (根据查询结果)
SET @overseas_menu_id = 你的菜单ID;
-- 3. 插入权限
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, query_param,
is_frame, is_cache, menu_type, visible, status, perms, icon,
create_dept, create_by, create_time, update_by, update_time, remark
) VALUES (
'生成登记表', @overseas_menu_id, 6, '', '', '', 1, 0, 'F', '0', '0',
'rs:overseas:generate', '#', 103, 1, NOW(), 1, NOW(), '出国申请生成登记表权限'
);
-- 4. 分配给管理员角色
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE perms = 'rs:overseas:generate';
```
#### 步骤2: 重新登录系统
权限配置后需要重新登录以刷新权限缓存。
### 方案2: 临时测试方案
如果需要快速测试,可以临时移除权限检查:
```vue
<!-- 临时移除 v-hasPermi 进行测试 -->
<el-tooltip content="生成登记表" placement="top" v-if="canGenerateRegistrationForm(scope.row)">
<el-button
link
type="success"
icon="Document"
@click="handleGenerateRegistrationForm(scope.row)"
></el-button>
</el-tooltip>
```
## 📁 相关文件
### 权限配置脚本
- `backend/ruoyi-modules/ruoyi-rs/出国申请生成登记表权限配置.sql` - 完整版配置
- `backend/ruoyi-modules/ruoyi-rs/出国申请生成登记表权限配置_简化版.sql` - 快速配置
### 前端文件
- `web/src/views/rs/overseas/index.vue` - 主页面
- `web/src/api/rs/overseas.ts` - API接口
### 后端文件
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApplicationController.java`
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RegistrationFormServiceImpl.java`
## ✅ 验证步骤
1. **执行权限配置SQL**
2. **重新登录系统**
3. **进入出国申请页面**
4. **查看操作列是否显示"生成登记表"按钮** (绿色Document图标)
5. **点击按钮测试弹窗功能**
6. **测试PDF生成、下载、打印、预览功能**
## 📝 预期结果
权限配置完成后,用户应该能够看到:
1. ✅ **生成登记表按钮**: 绿色Document图标,tooltip显示"生成登记表"
2. ✅ **生成弹窗**: 包含申请信息预览和生成选项
3. ✅ **PDF操作**: 下载PDF、直接打印、全屏预览三个按钮
4. ✅ **完整用户体验**: 从生成到下载的完整流程
## 🔄 后续优化
1. **权限细分**: 可考虑添加 `rs:overseas:download` 和 `rs:overseas:preview` 权限
2. **角色配置**: 为不同角色分配相应权限
3. **权限管理**: 在系统管理-菜单管理中配置权限
---
**解决时间**: 2025-09-27
**问题状态**: 已识别根本原因,提供完整解决方案
**影响范围**: 出国申请模块PDF生成功能
**优先级**: 高 (影响核心业务功能)
@@ -1,202 +0,0 @@
# 培训申请companions字段JSON反序列化错误修复报告
## 🚨 问题描述
**错误信息**:
```
提交失败: 请求参数格式错误:Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`) at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 425] (through reference chain: org.dromara.rs.domain.RsTrainingApplication["companions"])
```
**问题原因**:
- 前端 `companions` 字段是数组类型:`(string | number)[]`
- 后端实体类中是 `String` 类型,期望JSON字符串格式
- 前端直接发送数组,后端无法反序列化为字符串
## 🎯 修复方案
### 方案选择
采用**前端转换方案**:在前端提交时将数组转换为JSON字符串,后端保持字符串存储。
**优势**:
- ✅ 简单有效,不影响数据库结构
- ✅ 保持后端逻辑简洁
- ✅ 兼容现有数据格式
## 🛠️ 修复步骤
### 1. 前端修复 ✅
#### 📝 修复提交逻辑
**文件**: `web/src/views/rs/training/index.vue`
**修改位置**: 表单提交时的companions字段处理
```typescript
// 修复前
companions: applicationForm.companions,
// 修复后
companions: JSON.stringify(applicationForm.companions || []),
```
#### 📝 修复显示逻辑
**文件**: `web/src/views/rs/training/index.vue`
**修改位置**: `getCompanionNames` 函数
```typescript
// 修复前 - 只处理数组
function getCompanionNames(companions: (string | number)[] | null | undefined): string
// 修复后 - 同时处理数组和JSON字符串
function getCompanionNames(companions: (string | number)[] | string | null | undefined): string {
// 如果是字符串,尝试解析JSON
if (typeof companions === 'string') {
try {
companionArray = JSON.parse(companions);
} catch (e) {
return '';
}
}
// ... 其他逻辑
}
```
#### 📝 修复编辑回填逻辑
**文件**: `web/src/views/rs/training/index.vue`
**修改位置**: `handleEditApplication` 函数
```typescript
// 新增:处理同行人员字段 - 从JSON字符串解析为数组
if (row.companions) {
try {
if (typeof row.companions === 'string') {
applicationForm.companions = JSON.parse(row.companions);
} else if (Array.isArray(row.companions)) {
applicationForm.companions = row.companions;
} else {
applicationForm.companions = [];
}
} catch (e) {
console.warn('解析companions字段失败:', e);
applicationForm.companions = [];
}
} else {
applicationForm.companions = [];
}
```
### 2. 后端代码清理 ✅
#### 📝 清理实体类
**文件**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java`
**操作**:
- ✅ 移除了复杂的自定义序列化器
- ✅ 保持简单的 `String companions` 字段定义
- ✅ 清理了不必要的导入
### 3. 编译验证 ✅
**执行结果**:
```bash
cd backend && mvn clean compile -DskipTests
# BUILD SUCCESS - 编译通过
```
## 📊 修复效果
### 数据流转过程
#### 🔄 提交流程
```
前端表单数组 → JSON.stringify() → 后端String字段 → 数据库TEXT字段
[1, 2, 3] → "[1,2,3]" → companions: "[1,2,3]" → 存储到数据库
```
#### 🔄 显示流程
```
数据库TEXT字段 → 后端String字段 → JSON.parse() → 前端显示
"[1,2,3]" → companions: "[1,2,3]" → [1,2,3] → 显示同行人姓名
```
#### 🔄 编辑流程
```
数据库TEXT字段 → 后端String字段 → JSON.parse() → 前端表单数组
"[1,2,3]" → companions: "[1,2,3]" → [1,2,3] → 表单回填选中
```
## ✅ 测试验证
### 需要测试的功能点
#### 1. 申请培训提交 🧪
- [ ] 选择同行人员后提交申请
- [ ] 不选择同行人员提交申请
- [ ] 验证数据库中companions字段存储格式
#### 2. 申请列表显示 🧪
- [ ] 申请列表中同行人员正确显示
- [ ] 申请详情页面同行人员显示
#### 3. 编辑申请功能 🧪
- [ ] 编辑现有申请,同行人员正确回填
- [ ] 修改同行人员后重新提交
- [ ] 验证修改后的数据正确保存
## 🚀 部署步骤
### 1. 重启后端服务
```bash
cd backend/ruoyi-admin
mvn spring-boot:run -Dspring-boot.run.profiles=dev
```
### 2. 前端无需重启
前端代码已修改,刷新页面即可生效。
### 3. 功能测试
- 访问:http://localhost/rs/training
- 点击"申请培训"按钮
- 填写表单并选择同行人员
- 提交申请验证是否成功
## 🔍 排查要点
### 如果仍有问题,检查以下方面:
#### 1. 网络请求
- 检查浏览器开发者工具Network面板
- 确认companions字段发送格式为字符串:`"[1,2,3]"`
#### 2. 后端日志
- 查看后端控制台是否有反序列化错误
- 检查实体类字段类型是否为String
#### 3. 数据库存储
- 查看数据库中companions字段存储的实际内容
- 确认格式为JSON字符串而非数组
## 📝 修复总结
### ✅ 成功解决的问题
1. **JSON反序列化错误** - 前端数组无法反序列化为后端字符串
2. **数据格式不匹配** - 前后端数据格式统一为JSON字符串
3. **编辑回填异常** - 正确解析JSON字符串为前端数组
4. **显示逻辑错误** - 兼容字符串和数组两种格式
### 🎯 技术要点
- **前端处理**: 提交时 `JSON.stringify()` 转换
- **后端存储**: 保持 `String` 类型,存储JSON格式
- **显示解析**: 前端 `JSON.parse()` 解析显示
- **编辑回填**: 智能识别格式并正确转换
### 📈 预期效果
- ✅ 申请培训功能完全可用
- ✅ 同行人员选择和显示正常
- ✅ 编辑申请功能正常工作
- ✅ 数据库存储格式统一规范
---
**修复状态**: ✅ 代码修复完成,待功能验证
**修复时间**: 2025-01-03
**影响范围**: 培训申请模块的同行人员功能
**风险等级**: 🟢 低风险(仅影响单个字段,有完整回滚方案)
@@ -1,198 +0,0 @@
# 培训申请字段修复执行计划
## 📋 问题概述
培训申请界面新增了多个字段,但远程数据库 `hzdyjs_xg` 中的 `rs_training_application` 表未同步更新,导致提交申请时数据保存失败。
## 🎯 修复目标
1. 在远程数据库中添加缺失的11个字段
2. 更新后端实体类以支持新字段
3. 修正前端接口类型定义
4. 验证功能完整性
## 📦 涉及文件
- **数据库脚本**: `docs/设计文档/database/rs_training_application_字段补充.sql`
- **后端实体类**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/domain/RsTrainingApplication.java`
- **前端接口定义**: `web/src/api/rs/training.ts`
- **技术文档**: `docs/技术问题解决记录/培训申请表字段缺失修复SQL.md`
## 🚀 执行步骤
### 步骤1: 备份数据(生产环境必须)
```sql
-- 连接远程数据库
mysql -h 106.14.255.148 -u hzdyjs_xg -p'hzdyjs@2024' hzdyjs_xg
-- 备份现有数据
CREATE TABLE rs_training_application_backup_20250103 AS
SELECT * FROM rs_training_application;
-- 验证备份
SELECT COUNT(*) FROM rs_training_application_backup_20250103;
```
### 步骤2: 执行字段添加SQL
```sql
-- 执行字段添加脚本
source /path/to/rs_training_application_字段补充.sql
-- 或者直接执行:
ALTER TABLE `rs_training_application`
ADD COLUMN `course_id` bigint(20) DEFAULT NULL COMMENT '课程ID' AFTER `application_id`,
ADD COLUMN `current_title` varchar(100) DEFAULT NULL COMMENT '现职称' AFTER `position`,
ADD COLUMN `current_position` varchar(50) DEFAULT NULL COMMENT '现岗位' AFTER `current_title`,
ADD COLUMN `work_start_time` date DEFAULT NULL COMMENT '参加工作时间' AFTER `current_position`,
ADD COLUMN `phone` varchar(20) DEFAULT NULL COMMENT '联系电话' AFTER `work_start_time`,
ADD COLUMN `organizer_unit` varchar(200) DEFAULT NULL COMMENT '主办单位' AFTER `phone`,
ADD COLUMN `training_type` varchar(50) DEFAULT NULL COMMENT '培训类型' AFTER `organizer_unit`,
ADD COLUMN `training_fee` decimal(10,2) DEFAULT '0.00' COMMENT '培训费用' AFTER `training_type`,
ADD COLUMN `companions` text DEFAULT NULL COMMENT '同行人员列表(JSON格式)' AFTER `training_fee`,
ADD COLUMN `training_purpose` text DEFAULT NULL COMMENT '培训目的' AFTER `companions`,
ADD COLUMN `attachment_ids` text DEFAULT NULL COMMENT '附件ID列表(逗号分隔)' AFTER `training_purpose`;
```
### 步骤3: 验证数据库结构
```sql
-- 查看表结构
DESC rs_training_application;
-- 检查新增字段
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'hzdyjs_xg'
AND TABLE_NAME = 'rs_training_application'
AND COLUMN_NAME IN ('course_id', 'current_title', 'current_position', 'work_start_time', 'phone', 'organizer_unit', 'training_type', 'training_fee', 'companions', 'training_purpose', 'attachment_ids');
```
### 步骤4: 重启后端服务
```bash
# 切换到后端目录
cd backend
# 停止当前服务(如果正在运行)
# kill -9 $(lsof -t -i:8080)
# 重新编译和启动
mvn clean compile
mvn spring-boot:run -Dspring-boot.run.profiles=dev
```
### 步骤5: 前端功能测试
1. **访问培训申请页面**: http://localhost/rs/training
2. **点击"申请培训"按钮**
3. **填写完整表单**:
- 基本情况:现职称、现岗位、参加工作时间、联系电话
- 培训情况:主办单位、培训类型、培训费用、同行人、培训目的
- 上传附件
4. **提交申请并验证**:
- 检查是否提交成功
- 查看申请列表是否显示
- 验证数据库中是否正确保存
## ✅ 验证清单
### 数据库验证
- [ ] 所有11个新字段已成功添加
- [ ] 字段类型和长度正确
- [ ] 字段注释完整
- [ ] 表结构与本地保持一致
### 后端验证
- [ ] 实体类包含所有新字段
- [ ] 服务正常启动无报错
- [ ] API接口可以正常接收新字段数据
### 前端验证
- [ ] 申请培训表单所有字段可正常填写
- [ ] 表单提交成功无报错
- [ ] 申请列表正确显示新数据
- [ ] 申请详情页面显示完整信息
### 数据完整性验证
```sql
-- 插入测试数据验证
INSERT INTO rs_training_application (
application_id, course_id, course_name, user_id, user_name,
department, position, current_title, current_position,
work_start_time, phone, organizer_unit, training_type,
training_fee, companions, training_purpose, attachment_ids,
reason, status, apply_time, location, duration,
start_time, end_time, tenant_id, create_dept,
create_by, create_time, remark, deleted
) VALUES (
888888888, 0, '验证测试培训', 1, '测试用户',
'人文学院', '教师', '副教授', '教学',
'2020-01-01', '13800138000', '浙江大学', '教学',
1500.00, '[{"teacherId": 2, "teacherName": "李四"}]', '提升教学能力', '1001,1002',
'参加培训提升能力', '0', NOW(), '杭州', 40,
'2025-02-01', '2025-02-05', '000000', 101,
1, NOW(), '验证测试', '0'
);
-- 查询验证数据
SELECT * FROM rs_training_application WHERE application_id = 888888888;
-- 清理测试数据
DELETE FROM rs_training_application WHERE application_id = 888888888;
```
## 🚨 注意事项
### 执行前准备
1. **数据备份**: 生产环境必须先备份数据
2. **服务停机**: 考虑在低峰期执行,避免影响用户
3. **权限确认**: 确保有足够的数据库操作权限
### 潜在风险
1. **字段冲突**: 如果表中已存在同名字段会报错
2. **数据长度**: 新数据可能超出字段长度限制
3. **索引影响**: 大表添加字段可能耗时较长
### 回滚计划
如果出现问题,可以执行回滚:
```sql
-- 删除新增字段(紧急回滚)
ALTER TABLE `rs_training_application`
DROP COLUMN `course_id`,
DROP COLUMN `current_title`,
DROP COLUMN `current_position`,
DROP COLUMN `work_start_time`,
DROP COLUMN `phone`,
DROP COLUMN `organizer_unit`,
DROP COLUMN `training_type`,
DROP COLUMN `training_fee`,
DROP COLUMN `companions`,
DROP COLUMN `training_purpose`,
DROP COLUMN `attachment_ids`;
-- 恢复备份数据(完全回滚)
DROP TABLE rs_training_application;
RENAME TABLE rs_training_application_backup_20250103 TO rs_training_application;
```
## 📊 预期结果
1. **数据库结构完整**: rs_training_application表包含所有必需字段
2. **功能正常**: 申请培训功能完全可用
3. **数据一致性**: 前后端字段映射正确
4. **性能稳定**: 不影响系统整体性能
## 📝 执行记录
### 执行日期: ___________
### 执行人员: ___________
#### 执行结果:
- [ ] 数据库字段添加成功
- [ ] 后端服务启动正常
- [ ] 前端功能测试通过
- [ ] 数据完整性验证通过
#### 遇到的问题:
_____________________
#### 解决方案:
_____________________
---
**创建时间**: 2025-01-03
**状态**: 待执行
**优先级**: 高
@@ -1,317 +0,0 @@
# 培训申请查看详情字段缺失问题分析与解决方案
## 🚨 问题描述
**问题现象**:
通过申请培训按钮弹窗填写数据内容提交后,再次点击界面列表中的查看按键弹窗中以下字段内容没有调取显示:
- 现职称 (currentTitle)
- 现岗位 (currentPosition)
- 参加工作时间 (workStartTime)
- 联系电话 (phone)
- 培训类型 (trainingType)
- 培训费用 (trainingFee)
- 同行人 (companions)
**影响范围**:
- 申请培训功能:提交时这些字段可能无法正确保存
- 查看详情功能:已有数据无法正确显示
- 编辑功能:字段回填异常
## 🔍 问题分析
### 根本原因
**数据库表结构与代码不匹配**:前端代码和后端实体类已经支持新字段,但数据库表和MyBatis映射配置缺少相应字段。
### 具体原因分析
#### 1. 数据库表结构缺失 ❌
**当前表结构** (`rs_training_application`):
```sql
-- 缺少以下字段:
-- current_title - 现职称
-- current_position - 现岗位
-- work_start_time - 参加工作时间
-- phone - 联系电话
-- organizer_unit - 主办单位
-- training_type - 培训类型
-- training_fee - 培训费用
-- companions - 同行人员
-- training_purpose - 培训目的
-- attachment_ids - 附件ID列表
```
#### 2. MyBatis映射配置不完整 ❌
**原始映射配置**:
```xml
<!-- 缺少新字段的映射关系 -->
<resultMap type="org.dromara.rs.domain.RsTrainingApplication" id="RsTrainingApplicationResult">
<!-- 只包含了基础字段,缺少新增的字段映射 -->
</resultMap>
<sql id="selectRsTrainingApplicationVo">
SELECT
a.application_id, a.course_id, a.user_name, a.department, a.position, a.reason,
/* 缺少新字段的查询 */
FROM rs_training_application a
</sql>
```
#### 3. 前端代码已支持 ✅
**前端表单和显示逻辑**:
- ✅ 申请表单包含所有新字段
- ✅ 查看详情模板包含字段显示
- ✅ 编辑回填逻辑已完善
#### 4. 后端实体类已支持 ✅
**实体类字段定义**:
```java
@Data
public class RsTrainingApplication extends BaseEntity {
private String currentTitle; // ✅ 现职称
private String currentPosition; // ✅ 现岗位
private Date workStartTime; // ✅ 参加工作时间
private String phone; // ✅ 联系电话
private String trainingType; // ✅ 培训类型
private BigDecimal trainingFee; // ✅ 培训费用
private String companions; // ✅ 同行人员
// ... 其他字段
}
```
## 🛠️ 解决方案
### 方案一:数据库表结构补充 ✅
#### 执行SQL脚本
**文件位置**: `培训申请字段修复脚本.sql`
**关键SQL语句**:
```sql
-- 添加现职称
ALTER TABLE `rs_training_application`
ADD COLUMN `current_title` varchar(100) DEFAULT NULL COMMENT '现职称' AFTER `position`;
-- 添加现岗位
ALTER TABLE `rs_training_application`
ADD COLUMN `current_position` varchar(50) DEFAULT NULL COMMENT '现岗位' AFTER `current_title`;
-- 添加参加工作时间
ALTER TABLE `rs_training_application`
ADD COLUMN `work_start_time` date DEFAULT NULL COMMENT '参加工作时间' AFTER `current_position`;
-- 添加联系电话
ALTER TABLE `rs_training_application`
ADD COLUMN `phone` varchar(20) DEFAULT NULL COMMENT '联系电话' AFTER `work_start_time`;
-- 添加主办单位
ALTER TABLE `rs_training_application`
ADD COLUMN `organizer_unit` varchar(200) DEFAULT NULL COMMENT '主办单位' AFTER `phone`;
-- 添加培训类型
ALTER TABLE `rs_training_application`
ADD COLUMN `training_type` varchar(50) DEFAULT NULL COMMENT '培训类型' AFTER `organizer_unit`;
-- 添加培训费用
ALTER TABLE `rs_training_application`
ADD COLUMN `training_fee` decimal(10,2) DEFAULT '0.00' COMMENT '培训费用' AFTER `training_type`;
-- 添加同行人员
ALTER TABLE `rs_training_application`
ADD COLUMN `companions` text DEFAULT NULL COMMENT '同行人员列表(JSON格式)' AFTER `training_fee`;
-- 添加培训目的
ALTER TABLE `rs_training_application`
ADD COLUMN `training_purpose` text DEFAULT NULL COMMENT '培训目的' AFTER `companions`;
-- 添加附件ID列表
ALTER TABLE `rs_training_application`
ADD COLUMN `attachment_ids` text DEFAULT NULL COMMENT '附件ID列表(逗号分隔)' AFTER `training_purpose`;
```
### 方案二:MyBatis映射配置修复 ✅
#### 更新ResultMap映射
**文件**: `backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsTrainingApplicationMapper.xml`
**修复前**:
```xml
<resultMap type="org.dromara.rs.domain.RsTrainingApplication" id="RsTrainingApplicationResult">
<!-- 只包含基础字段 -->
</resultMap>
```
**修复后**:
```xml
<resultMap type="org.dromara.rs.domain.RsTrainingApplication" id="RsTrainingApplicationResult">
<id property="applicationId" column="application_id"/>
<result property="courseId" column="course_id"/>
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="department" column="department"/>
<result property="position" column="position"/>
<!-- 新增字段映射 -->
<result property="currentTitle" column="current_title"/>
<result property="currentPosition" column="current_position"/>
<result property="workStartTime" column="work_start_time"/>
<result property="phone" column="phone"/>
<result property="organizerUnit" column="organizer_unit"/>
<result property="trainingType" column="training_type"/>
<result property="trainingFee" column="training_fee"/>
<result property="companions" column="companions"/>
<result property="trainingPurpose" column="training_purpose"/>
<result property="attachmentIds" column="attachment_ids"/>
<!-- 其他字段 -->
</resultMap>
```
#### 更新SQL查询语句
**修复前**:
```xml
<sql id="selectRsTrainingApplicationVo">
SELECT
a.application_id, a.course_id, a.user_name, a.department, a.position, a.reason,
a.status, a.apply_time, a.current_approver, a.location, a.duration, a.start_time, a.end_time
/* 缺少新字段 */
FROM rs_training_application a
</sql>
```
**修复后**:
```xml
<sql id="selectRsTrainingApplicationVo">
SELECT
a.application_id, a.course_id, a.course_name as custom_course_name, a.user_id, a.user_name, a.department, a.position,
a.current_title, a.current_position, a.work_start_time, a.phone, a.organizer_unit,
a.training_type, a.training_fee, a.companions, a.training_purpose, a.attachment_ids,
a.reason, a.status, a.apply_time, a.current_approver, a.location, a.duration, a.start_time, a.end_time,
a.tenant_id, a.create_dept, a.reject_time, a.create_by, a.create_time, a.update_by, a.update_time,
CASE
WHEN a.course_id = 0 THEN a.course_name
ELSE c.title
END as course_name
FROM rs_training_application a
LEFT JOIN rs_training_course c ON a.course_id = c.course_id
</sql>
```
### 方案三:代码编译验证 ✅
**编译结果**:
```bash
cd backend && mvn clean compile -DskipTests
# BUILD SUCCESS - 编译通过
```
## 📊 修复效果
### 数据流转过程
#### 🔄 申请提交流程
```
前端表单 → 后端实体类 → MyBatis映射 → 数据库字段
currentTitle: "副教授" → currentTitle → current_title → 存储到数据库
```
#### 🔄 查看详情流程
```
数据库字段 → MyBatis映射 → 后端实体类 → 前端显示
current_title → currentTitle → currentTitle → "副教授"
```
#### 🔄 编辑回填流程
```
数据库查询 → 实体类赋值 → 前端表单回填
SELECT current_title → applicationDetail.currentTitle → 表单显示
```
## ✅ 执行步骤
### 1. 数据库字段补充 🔧
```bash
# 连接数据库执行SQL脚本
mysql -h [host] -u [username] -p [database] < 培训申请字段修复脚本.sql
```
### 2. 重启后端服务 🔄
```bash
cd backend/ruoyi-admin
mvn spring-boot:run -Dspring-boot.run.profiles=dev
```
### 3. 前端刷新页面 🔄
前端代码已修改,刷新页面即可生效。
### 4. 功能测试 🧪
- 访问:http://localhost/rs/training
- 点击"申请培训"按钮
- 填写完整表单(包括现职称、现岗位等字段)
- 提交申请
- 点击"查看"按钮验证字段显示
## 🔍 验证要点
### 数据库验证
```sql
-- 检查字段是否添加成功
DESC rs_training_application;
-- 检查新增字段
SELECT COLUMN_NAME, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'rs_training_application'
AND COLUMN_NAME IN ('current_title', 'current_position', 'work_start_time', 'phone', 'training_type', 'training_fee', 'companions');
-- 测试查询包含新字段
SELECT
application_id, user_name, current_title, current_position,
work_start_time, phone, training_type, training_fee, companions
FROM rs_training_application
LIMIT 5;
```
### 功能验证
1. **申请培训** ✅
- 填写所有字段并提交
- 检查数据库是否正确保存
2. **查看详情** ✅
- 点击查看按钮
- 确认所有字段正确显示
3. **编辑申请** ✅
- 编辑现有申请
- 确认字段正确回填
## 📝 修复总结
### ✅ 成功解决的问题
1. **数据库表结构不完整** - 添加了所有缺失的字段
2. **MyBatis映射配置缺失** - 更新了ResultMap和SQL查询
3. **查看详情字段显示为空** - 完整的数据查询和映射
4. **编辑回填功能异常** - 确保字段能正确加载
### 🎯 技术要点
- **数据库设计**: 合理的字段类型和默认值
- **MyBatis配置**: 完整的字段映射关系
- **实体类定义**: 已包含所有必要字段
- **前端模板**: 支持完整的字段显示
### 📈 预期效果
- ✅ 申请培训时所有字段正确保存
- ✅ 查看详情时所有字段正确显示
- ✅ 编辑申请时字段正确回填
- ✅ 系统功能完整可用
### 🔄 后续维护
- 定期检查数据库表结构与代码的一致性
- 新增字段时同步更新数据库、实体类、映射配置
- 完善单元测试覆盖新增字段
---
**修复状态**: ✅ 完整解决方案已准备就绪
**修复时间**: 2025-01-03
**影响范围**: 培训申请模块完整功能
**风险等级**: 🟢 低风险(向后兼容,不影响现有数据)
**下一步**: 执行数据库字段补充SQL脚本,重启服务,验证功能
@@ -1,204 +0,0 @@
# 培训申请表字段缺失修复SQL脚本
## 📋 问题描述
培训申请菜单下的培训申请选项卡中点击申请培训按钮弹出的界面中新增了多个字段,但是远程数据库 `hzdyjs_xg` 中的 `rs_training_application` 表中未增加这些字段,导致通过申请培训填写内容后提交无法保存。
## 🔍 字段分析
根据前端申请培训界面的分析,发现以下新增字段需要在数据库中补充:
### **前端新增字段清单**
1. **基本情况字段**:
- `current_title` - 现职称
- `current_position` - 现岗位
- `work_start_time` - 参加工作时间
- `phone` - 联系电话
2. **培训情况字段**:
- `organizer_unit` - 主办单位
- `training_type` - 培训类型
- `training_fee` - 培训费用
- `companions` - 同行人(存储为JSON字符串)
- `training_purpose` - 培训目的
3. **缺失的必要字段**:
- `course_id` - 课程ID(关联字段)
- `attachment_ids` - 附件ID列表
## 🛠️ 修复SQL脚本
### **步骤1:添加基本情况字段**
```sql
-- 添加基本情况相关字段
ALTER TABLE `rs_training_application`
ADD COLUMN `current_title` varchar(100) DEFAULT NULL COMMENT '现职称' AFTER `position`,
ADD COLUMN `current_position` varchar(50) DEFAULT NULL COMMENT '现岗位' AFTER `current_title`,
ADD COLUMN `work_start_time` date DEFAULT NULL COMMENT '参加工作时间' AFTER `current_position`,
ADD COLUMN `phone` varchar(20) DEFAULT NULL COMMENT '联系电话' AFTER `work_start_time`;
```
### **步骤2:添加培训情况字段**
```sql
-- 添加培训情况相关字段
ALTER TABLE `rs_training_application`
ADD COLUMN `organizer_unit` varchar(200) DEFAULT NULL COMMENT '主办单位' AFTER `phone`,
ADD COLUMN `training_type` varchar(50) DEFAULT NULL COMMENT '培训类型' AFTER `organizer_unit`,
ADD COLUMN `training_fee` decimal(10,2) DEFAULT '0.00' COMMENT '培训费用' AFTER `training_type`,
ADD COLUMN `companions` text DEFAULT NULL COMMENT '同行人员列表(JSON格式)' AFTER `training_fee`,
ADD COLUMN `training_purpose` text DEFAULT NULL COMMENT '培训目的' AFTER `companions`;
```
### **步骤3:添加缺失的必要字段**
```sql
-- 添加课程ID字段(如果不存在)
ALTER TABLE `rs_training_application`
ADD COLUMN `course_id` bigint(20) DEFAULT NULL COMMENT '课程ID' AFTER `application_id`;
-- 添加附件ID字段
ALTER TABLE `rs_training_application`
ADD COLUMN `attachment_ids` text DEFAULT NULL COMMENT '附件ID列表(逗号分隔)' AFTER `training_purpose`;
```
### **步骤4:完整的一次性执行脚本**
```sql
-- 一次性添加所有缺失字段的完整脚本
ALTER TABLE `rs_training_application`
ADD COLUMN `course_id` bigint(20) DEFAULT NULL COMMENT '课程ID' AFTER `application_id`,
ADD COLUMN `current_title` varchar(100) DEFAULT NULL COMMENT '现职称' AFTER `position`,
ADD COLUMN `current_position` varchar(50) DEFAULT NULL COMMENT '现岗位' AFTER `current_title`,
ADD COLUMN `work_start_time` date DEFAULT NULL COMMENT '参加工作时间' AFTER `current_position`,
ADD COLUMN `phone` varchar(20) DEFAULT NULL COMMENT '联系电话' AFTER `work_start_time`,
ADD COLUMN `organizer_unit` varchar(200) DEFAULT NULL COMMENT '主办单位' AFTER `phone`,
ADD COLUMN `training_type` varchar(50) DEFAULT NULL COMMENT '培训类型' AFTER `organizer_unit`,
ADD COLUMN `training_fee` decimal(10,2) DEFAULT '0.00' COMMENT '培训费用' AFTER `training_type`,
ADD COLUMN `companions` text DEFAULT NULL COMMENT '同行人员列表(JSON格式)' AFTER `training_fee`,
ADD COLUMN `training_purpose` text DEFAULT NULL COMMENT '培训目的' AFTER `companions`,
ADD COLUMN `attachment_ids` text DEFAULT NULL COMMENT '附件ID列表(逗号分隔)' AFTER `training_purpose`;
```
## 📊 字段映射关系
### **前端 → 数据库字段映射**
| 前端字段 | 数据库字段 | 类型 | 说明 |
|---------|-----------|------|------|
| `applicationForm.currentTitle` | `current_title` | `varchar(100)` | 现职称 |
| `applicationForm.currentPosition` | `current_position` | `varchar(50)` | 现岗位 |
| `applicationForm.workStartTime` | `work_start_time` | `date` | 参加工作时间 |
| `applicationForm.phone` | `phone` | `varchar(20)` | 联系电话 |
| `applicationForm.organizerUnit` | `organizer_unit` | `varchar(200)` | 主办单位 |
| `applicationForm.trainingType` | `training_type` | `varchar(50)` | 培训类型 |
| `applicationForm.trainingFee` | `training_fee` | `decimal(10,2)` | 培训费用 |
| `applicationForm.companions` | `companions` | `text` | 同行人员(JSON) |
| `applicationForm.trainingPurpose` | `training_purpose` | `text` | 培训目的 |
| `applicationForm.attachmentIds` | `attachment_ids` | `text` | 附件ID列表 |
## 🔄 后端代码调整建议
### **实体类需要增加的字段**
在 `RsTrainingApplication.java` 中需要添加:
```java
/**
* 现职称
*/
private String currentTitle;
/**
* 现岗位
*/
private String currentPosition;
/**
* 参加工作时间
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private Date workStartTime;
/**
* 联系电话
*/
private String phone;
/**
* 主办单位
*/
private String organizerUnit;
/**
* 培训类型
*/
private String trainingType;
/**
* 培训费用
*/
private BigDecimal trainingFee;
/**
* 同行人员(JSON格式)
*/
private String companions;
/**
* 培训目的
*/
private String trainingPurpose;
/**
* 附件ID列表
*/
private String attachmentIds;
```
## ✅ 验证步骤
### **1. 执行SQL后验证表结构**
```sql
-- 查看表结构确认字段是否添加成功
DESC rs_training_application;
```
### **2. 测试数据插入**
```sql
-- 测试插入新字段数据
INSERT INTO rs_training_application (
application_id, course_id, course_name, user_id, user_name,
department, position, current_title, current_position,
work_start_time, phone, organizer_unit, training_type,
training_fee, companions, training_purpose, attachment_ids,
reason, status, apply_time, location, duration,
start_time, end_time, tenant_id, create_dept,
create_by, create_time, remark, deleted
) VALUES (
123456789, 0, '测试培训', 1, '张三',
'人文学院', '教师', '副教授', '教学',
'2020-01-01', '13800138000', '浙江大学', '教学',
1500.00, '[{"teacherId": 2, "teacherName": "李四"}]', '提升教学能力', '1001,1002',
'参加培训提升能力', '0', NOW(), '杭州', 40,
'2025-02-01', '2025-02-05', '000000', 101,
1, NOW(), '测试备注', '0'
);
```
### **3. 前端功能测试**
- 填写申请培训表单的所有新增字段
- 提交表单验证是否能成功保存
- 查看申请详情确认所有字段都正确显示
## 📝 注意事项
1. **备份数据**:执行SQL前请备份 `rs_training_application` 表
2. **字段长度**:根据实际业务需求调整varchar字段长度
3. **索引优化**:可以考虑为常用查询字段添加索引
4. **数据类型**:`companions` 字段存储JSON格式的同行人员信息
5. **兼容性**:确保修改后的表结构与前端、后端代码完全匹配
## 🎯 执行顺序
1. 执行完整SQL脚本添加所有缺失字段
2. 更新后端实体类 `RsTrainingApplication.java`
3. 重启后端服务
4. 测试前端申请培训功能
5. 验证数据库数据正确性
---
**创建时间**: 2025-01-03
**问题状态**: 待修复
**预计影响**: 解决培训申请提交失败问题
@@ -1,165 +0,0 @@
# 培训记录同行人字段显示修复
## 问题描述
**问题现象**:培训申请菜单下的培训记录选项卡中的同行人字段一直显示"加载中...",未能实时显示同行人姓名。
**影响范围**:`/rs/training/index.vue` 培训记录选项卡中的同行人员列显示
**发生时间**:2025-01-23
## 问题分析
### 根本原因
1. **教师列表未及时加载**:页面初始化和选项卡切换时没有加载教师列表
2. **同行人显示依赖教师列表**:`getCompanionNames` 方法需要教师列表来将教师ID转换为姓名
3. **异步加载时序问题**:培训记录数据加载完成,但教师列表还未加载
### 代码逻辑分析
```javascript
// 问题代码:getCompanionNames方法中的逻辑
if (teacherList.value.length === 0) {
return '加载中...'; // 教师列表为空时显示加载中
}
```
## 修复方案
### 1. 选项卡切换时加载教师列表
**文件**:`/web/src/views/rs/training/index.vue`
**方法**:`handleTrainingSubTabClick`
```javascript
function handleTrainingSubTabClick() {
if (trainingSubTab.value === 'application') {
getApplicationList();
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
} else if (trainingSubTab.value === 'record') {
getRecordList();
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
} else {
getTrainingList();
}
}
```
### 2. 页面初始化时加载教师列表
**方法**:`onMounted`
```javascript
onMounted(() => {
console.log('组件挂载,开始初始化');
// 采用培训审批页面的成功模式:同时加载所有数据
getTrainingList();
getApplicationList();
getRecordList();
loadCourseOptions();
getDepartmentList();
// 加载教师列表(用于同行人显示)
loadTeacherListForDisplay();
});
```
### 3. 组件激活时确保教师列表已加载
**方法**:`onActivated`
```javascript
onActivated(() => {
console.log('组件被激活,重新加载当前选项卡数据');
// ... 其他代码 ...
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
});
```
### 4. 培训记录数据加载完成后确保教师列表已加载
**方法**:`getRecordList`
```javascript
listTrainingRecord(queryParams)
.then((response) => {
recordList.value = response.rows;
recordTotal.value = response.total;
recordLoading.value = false;
// 确保教师列表已加载(用于同行人显示)
if (teacherList.value.length === 0) {
loadTeacherListForDisplay();
}
})
```
## 修复效果
### 修复前
- 同行人字段一直显示"加载中..."
- 用户无法看到具体的同行人姓名
- 影响用户体验和数据可读性
### 修复后
- 页面初始化时立即加载教师列表
- 切换到培训记录选项卡时确保教师列表已加载
- 同行人字段正确显示教师姓名,格式为"张三、李四"
- 无同行人时显示"无"
## 测试验证
### 测试步骤
1. 登录系统,进入人事管理 -> 培训管理
2. 切换到"培训记录"选项卡
3. 检查同行人员列是否正确显示教师姓名
4. 刷新页面,再次检查显示效果
5. 切换到其他选项卡再回到培训记录,验证显示正常
### 预期结果
- 同行人员列显示具体的教师姓名
- 不再出现"加载中..."的持续显示
- 页面响应速度正常,无明显延迟
## 相关文件
### 修改文件
- `/web/src/views/rs/training/index.vue`
### 相关API
- `listClassTeacherCounselorRel()` - 获取教师列表
- `listTrainingRecord()` - 获取培训记录列表
### 相关方法
- `loadTeacherListForDisplay()` - 加载教师列表用于显示
- `getCompanionNames()` - 获取同行人姓名列表
- `formatCompanions()` - 格式化同行人员显示
## 经验总结
### 问题类型
- **异步数据加载时序问题**:需要确保依赖数据在使用前已加载完成
- **页面生命周期管理**:在适当的生命周期钩子中加载必要的数据
### 最佳实践
1. **数据依赖关系明确**:明确标识哪些显示字段依赖外部数据
2. **多点加载确保**:在多个关键节点确保依赖数据已加载
3. **用户体验优化**:避免长时间显示"加载中..."状态
4. **防御性编程**:在使用数据前检查数据是否已加载
### 预防措施
1. 在页面初始化时一次性加载所有必要的基础数据
2. 在数据使用点进行防御性检查和自动加载
3. 合理使用生命周期钩子管理数据加载时机
## 修复日期
2025-01-23
## 修复人员
Cursor AI Assistant
@@ -1,250 +0,0 @@
# 撤销申请页面图片加载问题修复记录
## 问题描述
**现象**: 在已有撤销申请时,再次打开撤销申请界面(`revokeApplication.vue`),界面中的图片没有做加载显示,看控制台没有调用接口。
**影响范围**: 移动端撤销申请页面的图片附件显示功能
## 问题分析
### 原因分析
1. **缺少图片blob处理**: `revokeApplication.vue` 在 `handleExistingEvidenceData` 方法中只是简单地将 OSS 数据转换为附件列表,但没有像 `RevokeApplicationView.vue` 查看页面那样处理图片的 blob 数据获取。
2. **图片URL处理不完整**: 对于图片文件,需要特殊处理URL前缀并获取blob数据才能正确显示。
3. **缺少图片预加载机制**: 没有预加载机制确保图片能够正确显示。
### 参考实现
参考了 `RevokeApplicationView.vue` 中的图片处理机制:
- `processAllImageFiles()` 方法处理图片blob数据
- `getImageBlobUrl()` 方法获取图片blob URL
- `preloadAllImages()` 方法预加载图片
## 解决方案
### 1. 增强附件数据处理 (`handleExistingEvidenceData`)
```javascript
// 处理现有证明材料附件数据时
const attachmentFiles = response.data.map((item, index) => {
// 使用fileSuffix字段判断文件类型
const fileType = this.getFileTypeBySuffix(item.fileSuffix);
const isImage = this.isImageFile({
name: item.originalName,
type: fileType,
});
// 处理图片URL - 确保有正确的API前缀
let processedUrl = item.url;
if (isImage && item.url && item.url.startsWith('/resource/')) {
// 图片文件需要添加/api前缀
processedUrl = `/api${item.url}`;
}
// 构造完整的文件名(包含扩展名)
let fullFileName = item.originalName || `证明材料${index + 1}`;
if (
item.fileSuffix &&
!fullFileName
.toLowerCase()
.endsWith(`.${item.fileSuffix.toLowerCase()}`)
) {
fullFileName = `${fullFileName}.${item.fileSuffix}`;
}
return {
uid: `existing_${index}_${Date.now()}`,
name: fullFileName,
url: processedUrl,
ossId: item.ossId,
status: 'success',
type: fileType,
size: item.fileSize || 0,
};
});
this.attachmentList = attachmentFiles;
// 🚀 关键步骤:处理图片文件以确保正确显示
await this.processAllImageFiles();
// 强制刷新组件显示
this.$nextTick(() => {
// 预加载图片以确保能正确显示
this.preloadAllImages();
});
```
### 2. 添加图片处理核心方法
#### `processAllImageFiles()` - 处理所有图片文件
```javascript
async processAllImageFiles() {
console.log('=== 处理所有图片文件 ===');
// 处理附件列表中的图片
const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
for (const file of imageFiles) {
if (file.url && !file.blobUrl) {
try {
console.log(`处理图片文件: ${file.name}`);
const blobUrl = await this.getImageBlobUrl(file.url);
if (blobUrl) {
file.blobUrl = blobUrl;
file.fileType = 'image';
file.type = file.type && file.type.startsWith('image') ? file.type : 'image';
file.isImage = true;
console.log(`图片文件处理成功: ${file.name} - ${blobUrl}`);
}
} catch (error) {
console.error(`处理图片文件失败: ${file.name}`, error);
}
}
}
}
```
#### `getImageBlobUrl()` - 获取图片blob数据
```javascript
async getImageBlobUrl(imageUrl) {
try {
console.log(`获取图片blob数据: ${imageUrl}`);
// 处理图片URL路径
let apiUrl = imageUrl;
if (imageUrl.startsWith('/api/')) {
// 移除/api前缀,因为request会自动添加baseURL
apiUrl = imageUrl.replace('/api', '');
} else if (imageUrl.startsWith('/resource/')) {
// 直接使用相对路径
apiUrl = imageUrl;
} else if (imageUrl.startsWith('http')) {
// 完整URL,需要特殊处理
console.log('检测到完整URL,直接返回原URL');
return imageUrl;
}
console.log(`API请求URL: ${apiUrl}`);
// 使用request方法获取图片数据
const response = await request({
url: apiUrl,
method: 'GET',
responseType: 'arraybuffer', // 设置响应类型为arraybuffer
header: {
isToken: true, // 图片请求可能需要token验证
},
});
// 将arraybuffer转换为blob
const blob = new Blob([response], { type: 'image/*' });
const blobUrl = URL.createObjectURL(blob);
console.log(`成功创建blob URL: ${blobUrl}`);
return blobUrl;
} catch (error) {
console.error(`获取图片blob数据失败: ${imageUrl}`, error);
// 如果请求失败,返回原始URL作为备用方案
return imageUrl;
}
}
```
#### `preloadAllImages()` - 预加载图片
```javascript
async preloadAllImages() {
console.log('=== 预加载所有图片 ===');
// 预加载附件列表中的图片
const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
for (const file of imageFiles) {
if (file.url) {
try {
console.log(`预加载图片: ${file.name} - ${file.url}`);
const img = new Image();
img.onload = () => {
console.log(`图片预加载成功: ${file.name}`);
};
img.onerror = () => {
console.error(`图片预加载失败: ${file.name} - ${file.url}`);
};
img.src = file.blobUrl || file.url;
} catch (error) {
console.error(`预加载图片出错: ${file.name}`, error);
}
}
}
}
```
### 3. 增强模板错误处理
在模板中为image组件添加错误处理:
```vue
<image
:src="image.blobUrl || image.url"
class="attachment-image"
mode="aspectFill"
@error="handleImageError"
></image>
```
```javascript
// 图片加载错误处理
handleImageError(event) {
console.warn('图片加载失败:', event);
}
```
## 修复效果
### 修复前
- 已有撤销申请记录时,图片附件无法显示
- 控制台没有任何API调用记录
- 图片区域显示空白或加载失败
### 修复后
- 已有撤销申请记录时,图片附件正常显示
- 控制台显示完整的图片处理流程日志
- 图片正确加载并支持预览功能
## 技术要点
1. **blob数据处理**: 通过获取图片的arraybuffer数据并转换为blob URL,确保图片能正确显示
2. **URL路径处理**: 正确处理不同格式的图片URL路径
3. **异步处理**: 使用async/await确保图片处理的时序正确
4. **错误处理**: 添加完善的错误处理和降级方案
5. **内存管理**: 在页面卸载时清理blob URL,防止内存泄漏
## 测试建议
1. **基础功能测试**:
- 创建新的撤销申请(上传图片附件)
- 保存后重新进入页面,验证图片是否正常显示
2. **边界条件测试**:
- 测试不同格式的图片文件(JPG, PNG, GIF等)
- 测试混合附件(图片+文档)
- 测试网络异常情况下的降级处理
3. **性能测试**:
- 测试多张图片的加载性能
- 检查内存泄漏情况
## 相关文件
- **主要修改文件**: `mobile/src/pages/xskh/wjjl/revokeApplication.vue`
- **参考实现文件**: `mobile/src/pages/xskh/wjjl/RevokeApplicationView.vue`
- **修复日期**: 2025-01-03
- **修复人员**: AI Assistant
## 总结
通过参考查看页面的成功实现,为撤销申请页面添加了完整的图片blob数据处理机制,确保了已有撤销申请记录的图片附件能够正确显示。修复方案具有良好的错误处理和性能优化特性。
@@ -1,179 +0,0 @@
# 培训申请表字段修复执行报告
## 📋 执行概要
- **执行时间**: 2025-01-03
- **数据库**: 47.99.164.66:3306/hzdyjs_xg
- **表名**: rs_training_application
- **操作类型**: 字段添加
- **执行状态**: ✅ 成功完成
## 🎯 修复目标
解决培训申请界面新增字段与数据库结构不匹配的问题,确保申请培训功能正常工作。
## 🛠️ 执行步骤记录
### 1. 数据备份 ✅
```sql
CREATE TABLE rs_training_application_backup_20250103 AS
SELECT * FROM rs_training_application;
```
- **备份结果**: 成功备份 33 条记录
- **备份表名**: rs_training_application_backup_20250103
### 2. 字段添加 ✅
成功添加了以下10个字段:
| 序号 | 字段名 | 类型 | 默认值 | 注释 | 状态 |
|------|--------|------|--------|------|------|
| 1 | current_title | varchar(100) | NULL | 现职称 | ✅ |
| 2 | current_position | varchar(50) | NULL | 现岗位 | ✅ |
| 3 | work_start_time | date | NULL | 参加工作时间 | ✅ |
| 4 | phone | varchar(20) | NULL | 联系电话 | ✅ |
| 5 | organizer_unit | varchar(200) | NULL | 主办单位 | ✅ |
| 6 | training_type | varchar(50) | NULL | 培训类型 | ✅ |
| 7 | training_fee | decimal(10,2) | 0.00 | 培训费用 | ✅ |
| 8 | companions | text | NULL | 同行人员列表(JSON格式) | ✅ |
| 9 | training_purpose | text | NULL | 培训目的 | ✅ |
| 10 | attachment_ids | text | NULL | 附件ID列表(逗号分隔) | ✅ |
**注意**: course_id 字段在执行前已存在,无需添加。
### 3. 验证测试 ✅
- **插入测试数据**: 成功插入包含所有新字段的测试记录
- **数据验证**: 所有字段数据正确保存和查询
- **清理测试数据**: 成功删除测试记录
## 📊 执行结果
### 修复前表结构
- **字段总数**: 25个
- **缺失关键字段**: 10个培训申请相关字段
### 修复后表结构
- **字段总数**: 35个
- **新增字段**: 10个
- **字段完整性**: ✅ 完全匹配前端需求
### 完整字段列表
```
application_id, course_id, course_name, reject_time, user_id, user_name,
department, position, current_title, current_position, work_start_time,
phone, organizer_unit, training_type, training_fee, companions,
training_purpose, attachment_ids, reason, status, apply_time,
current_approver, location, duration, start_time, end_time, tenant_id,
create_dept, create_by, create_time, update_by, update_time, remark, deleted
```
## ✅ 验证结果
### 数据库验证
- ✅ 所有10个新字段成功添加
- ✅ 字段类型和长度正确设置
- ✅ 字段注释完整准确
- ✅ 表结构与本地开发环境一致
### 数据完整性验证
- ✅ 测试数据插入成功
- ✅ 新字段数据正确保存
- ✅ 查询结果符合预期
- ✅ 无数据丢失或错误
## 🔧 后续步骤建议
### 1. 重启后端服务
```bash
cd backend
mvn clean compile
mvn spring-boot:run -Dspring-boot.run.profiles=dev
```
### 2. 前端功能测试
- 访问培训申请页面:http://localhost/rs/training
- 测试申请培训表单提交功能
- 验证所有新字段是否正常工作
### 3. 完整性验证
- 填写完整的申请表单(包括所有新字段)
- 提交申请并检查数据库中的数据
- 验证申请列表和详情页面显示
## 🚨 重要提醒
### 数据安全
- ✅ 已创建完整数据备份
- ✅ 原有数据完整保留
- ✅ 可随时回滚(如需要)
### 回滚方案(紧急情况下使用)
```sql
-- 删除新增字段(紧急回滚)
ALTER TABLE rs_training_application
DROP COLUMN current_title,
DROP COLUMN current_position,
DROP COLUMN work_start_time,
DROP COLUMN phone,
DROP COLUMN organizer_unit,
DROP COLUMN training_type,
DROP COLUMN training_fee,
DROP COLUMN companions,
DROP COLUMN training_purpose,
DROP COLUMN attachment_ids;
-- 完全回滚到备份状态
DROP TABLE rs_training_application;
RENAME TABLE rs_training_application_backup_20250103 TO rs_training_application;
```
## 📈 预期改进
### 功能恢复
- 申请培训表单完全可用
- 所有字段数据正确保存
- 培训申请流程完整无缺
### 数据一致性
- 前后端数据结构完全匹配
- 字段映射关系正确
- 支持完整的业务功能
## 📝 执行日志
### MySQL连接信息
- **主机**: 47.99.164.66:3306
- **数据库**: hzdyjs_xg
- **用户**: root
- **连接状态**: ✅ 成功
### 执行SQL语句
```sql
-- 1. 数据备份
CREATE TABLE rs_training_application_backup_20250103 AS SELECT * FROM rs_training_application;
-- 2. 添加字段(10个ALTER语句,全部成功执行)
ALTER TABLE rs_training_application ADD COLUMN current_title varchar(100) DEFAULT NULL COMMENT '现职称' AFTER position;
ALTER TABLE rs_training_application ADD COLUMN current_position varchar(50) DEFAULT NULL COMMENT '现岗位' AFTER current_title;
ALTER TABLE rs_training_application ADD COLUMN work_start_time date DEFAULT NULL COMMENT '参加工作时间' AFTER current_position;
ALTER TABLE rs_training_application ADD COLUMN phone varchar(20) DEFAULT NULL COMMENT '联系电话' AFTER work_start_time;
ALTER TABLE rs_training_application ADD COLUMN organizer_unit varchar(200) DEFAULT NULL COMMENT '主办单位' AFTER phone;
ALTER TABLE rs_training_application ADD COLUMN training_type varchar(50) DEFAULT NULL COMMENT '培训类型' AFTER organizer_unit;
ALTER TABLE rs_training_application ADD COLUMN training_fee decimal(10,2) DEFAULT 0.00 COMMENT '培训费用' AFTER training_type;
ALTER TABLE rs_training_application ADD COLUMN companions text DEFAULT NULL COMMENT '同行人员列表(JSON格式)' AFTER training_fee;
ALTER TABLE rs_training_application ADD COLUMN training_purpose text DEFAULT NULL COMMENT '培训目的' AFTER companions;
ALTER TABLE rs_training_application ADD COLUMN attachment_ids text DEFAULT NULL COMMENT '附件ID列表(逗号分隔)' AFTER training_purpose;
-- 3. 验证测试(插入、查询、删除测试数据)
-- 全部成功执行
```
## 🎉 执行结论
**✅ 数据库字段修复执行成功!**
培训申请表 `rs_training_application` 已成功添加所有必需的字段,现在具备了完整的字段结构以支持前端申请培训功能。下一步请重启后端服务并测试前端功能。
---
**执行人员**: Cursor AI Assistant
**执行时间**: 2025-01-03
**执行状态**: ✅ 完成
**数据安全**: ✅ 已备份
**功能状态**: ✅ 可用
@@ -1,96 +0,0 @@
# 🚀 行程变更审批问题修复 - 快速导航
## 🎯 当前状态
**最新问题**: ⚠️ **数据库字段缺失** - `Unknown column 'gender' in 'field list'`
---
## ⚡ 立即修复(3步走)
### 1️⃣ 执行SQL修复(5分钟)
**打开文件**:
```
docs/设计文档/database/⚡一键修复-添加缺失字段.sql
```
**操作**:
- 复制整个SQL内容
- 在MySQL客户端中执行
- 查看执行结果(应显示8个新字段)
---
### 2️⃣ 重启后端服务(1分钟)
**操作**:
- 停止后端服务(IDEA红色按钮 或 Ctrl+C)
- 启动后端服务(IDEA绿色按钮)
- 等待启动完成(看到"Started RuoYiApplication")
---
### 3️⃣ 清除缓存并测试(2分钟)
**操作**:
- 清除浏览器缓存(Ctrl+Shift+R 或 Cmd+Shift+R)
- 打开:人事管理 → 出国行程变更审批
- 点击【通过】按钮
- 提交审批
- 验证成功 ✅
---
## 📚 详细文档
### 如果你想了解问题原因和详细修复步骤
| 问题 | 快速修复 | 详细文档 |
|------|---------|---------|
| 🔴 **当前问题:数据库字段缺失** | 执行SQL | [🚨数据库字段缺失修复指南.md](./🚨数据库字段缺失修复指南.md) |
| ✅ 已修复:审批接口参数错误 | - | [行程变更审批接口参数错误修复总结.md](./行程变更审批接口参数错误修复总结.md) |
| ✅ 已修复:变更记录Tab显示错误 | - | [变更记录Tab显示逻辑修复总结.md](./变更记录Tab显示逻辑修复总结.md) |
| 📋 完整问题汇总 | - | [📊行程变更审批功能-问题汇总.md](./📊行程变更审批功能-问题汇总.md) |
| 🧪 功能测试 | - | [行程变更审批功能-测试指南.md](./行程变更审批功能-测试指南.md) |
---
## 🔍 SQL脚本位置
| 脚本 | 路径 | 用途 |
|------|------|------|
| ⚡ **一键修复** | `docs/设计文档/database/⚡一键修复-添加缺失字段.sql` | 直接执行修复 |
| 🔍 诊断脚本 | `docs/设计文档/database/🔍诊断-检查缺失字段.sql` | 检查缺失字段 |
| 📋 完整建表 | `docs/设计文档/database/rs_overseas_itinerary_change_full.sql` | 完整表结构 |
---
## ❓ 常见问题
### Q1: 执行SQL后仍然报错?
**A**: 确保已重启后端服务(清除MyBatis缓存)
### Q2: 不知道如何执行SQL?
**A**:
1. 打开MySQL客户端(Navicat/DBeaver/命令行)
2. 选择正确的数据库
3. 复制SQL内容
4. 点击"执行"按钮
### Q3: 修复后如何验证?
**A**: 参考 [行程变更审批功能-测试指南.md](./行程变更审批功能-测试指南.md)
---
## 🎉 修复完成后
✅ 审批通过功能正常
✅ 审批拒绝功能正常
✅ 变更记录正确显示
✅ 所有问题已解决
---
**如有疑问,请查看详细文档或联系维护人员** 💪
@@ -1,266 +0,0 @@
# ✅ 数据库字段修复完成报告
## 📊 修复执行结果
**执行时间**: 2025-10-13
**修复状态**: ✅ **成功完成**
**修复方式**: 智能增量修复(只添加缺失字段)
---
## 🎯 修复内容
### 已成功添加的字段(7个新字段)
| 序号 | 字段名 | 类型 | 注释 | 状态 |
|------|--------|------|------|------|
| 1 | `gender` | char(1) | 性别:0-男,1-女 | ✅ 已添加 |
| 2 | `birth_date` | date | 出生日期 | ✅ 已添加 |
| 3 | `political_status` | varchar(20) | 政治面貌 | ✅ 已添加 |
| 4 | `professional_title` | varchar(100) | 职称 | ✅ 已添加 |
| 5 | `position` | varchar(100) | 职务 | ✅ 已添加 |
| 6 | `document_name` | varchar(100) | 申领证件名称 | ✅ 已添加 |
| 7 | `document_number` | varchar(100) | 证件号码 | ✅ 已添加 |
| 8 | `department` | varchar(200) | 部门 | ✅ 已存在(无需添加) |
---
## 📋 执行过程
### 1️⃣ 初始状态检查
```sql
-- 执行前字段数量
字段数量: 39个
```
### 2️⃣ 智能修复执行
```bash
执行文件: docs/设计文档/database/⚡智能修复-只添加缺失字段.sql
执行方式: mysql命令行
执行结果: 成功
```
### 3️⃣ 修复后验证
```sql
-- 执行后字段数量
字段数量: 46个
-- 新增字段数量
新增: 7个字段
-- 所有必需字段验证
✅ gender - 已存在
✅ birth_date - 已存在
✅ political_status - 已存在
✅ professional_title - 已存在
✅ position - 已存在
✅ document_name - 已存在
✅ document_number - 已存在
✅ department - 已存在
```
---
## 🔧 技术细节
### 智能修复SQL特点
- ✅ **检查字段是否存在**:避免重复添加
- ✅ **动态生成ALTER语句**:只添加缺失的字段
- ✅ **跳过已存在字段**:不会报错
- ✅ **自动验证结果**:显示字段数量和详情
### 执行的SQL语句
```sql
-- 使用动态SQL检查并添加字段
SET @sql = (SELECT IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'hzdyjs_xg'
AND TABLE_NAME = 'rs_overseas_itinerary_change'
AND COLUMN_NAME = 'gender') = 0,
'ALTER TABLE rs_overseas_itinerary_change ADD COLUMN `gender` ...',
'SELECT ''字段已存在,跳过'' AS result;'
));
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
```
---
## 🎉 修复成功验证
### 数据库层面
✅ **所有8个必需字段都已存在**
✅ **字段类型正确**
✅ **字段注释完整**
✅ **字段顺序合理**
### 应用层面(待验证)
⚠️ **需要重启后端服务**
⚠️ **需要清除浏览器缓存**
⚠️ **需要测试审批功能**
---
## 🚀 下一步操作
### ⚠️ 必须执行(立即)
#### 1️⃣ 重启后端服务(1分钟)
**为什么需要重启**:
- MyBatis会缓存Mapper映射关系
- 数据库表结构变更后,必须清除缓存
- 否则仍然会报"Unknown column 'gender'"错误
**操作步骤**:
**方式A: IDEA中重启**(推荐)
1. 找到IDEA中的RuoYi运行配置
2. 点击红色停止按钮 🔴
3. 等待完全停止(看到"进程已结束")
4. 点击绿色启动按钮 ▶️
5. 等待启动完成(看到"Started RuoYiApplication")
**方式B: 命令行重启**
```bash
# 1. 停止后端服务
kill -9 $(ps aux | grep "RuoYiApplication" | grep -v grep | awk '{print $2}')
# 2. 启动后端服务(在backend目录下)
cd /Users/wuzilong/work/00code/hzdyjs_xg/backend
mvn spring-boot:run -pl ruoyi-admin
```
---
#### 2️⃣ 清除浏览器缓存(30秒)
**操作步骤**:
1. 打开前端页面(http://localhost)
2. 按下快捷键:
- Windows: `Ctrl + Shift + R`
- Mac: `Cmd + Shift + R`
3. 或者手动清除:
- 打开开发者工具(F12)
- 右键点击刷新按钮
- 选择"清空缓存并硬性重新加载"
---
#### 3️⃣ 测试审批功能(2分钟)
**测试步骤**:
1. 登录系统
2. 打开:**人事管理 → 出国行程变更审批**
3. 找到一条"待审核"的变更申请
4. 点击【通过】按钮
5. 填写审批意见(可选)
6. 点击【确定】
**预期结果**:
```
✅ 提示"审批成功"
✅ 状态变为"已通过"
✅ 不再报"Unknown column 'gender'"错误
✅ 不再报任何数据库错误
```
**如果仍然报错**:
- 确认后端服务已完全重启
- 确认浏览器缓存已清除
- 查看后端日志,检查是否还有其他错误
---
## 📚 相关文档
### 修复文档
- [🚨数据库字段缺失修复指南.md](./🚨数据库字段缺失修复指南.md) - 详细修复指南
- [📊行程变更审批功能-问题汇总.md](./📊行程变更审批功能-问题汇总.md) - 所有问题汇总
### 测试文档
- [行程变更审批功能-测试指南.md](./行程变更审批功能-测试指南.md) - 完整测试流程
### SQL脚本
- `docs/设计文档/database/⚡智能修复-只添加缺失字段.sql` - **已执行成功**
- `docs/设计文档/database/⚡一键修复-添加缺失字段.sql` - 简单版本
- `docs/设计文档/database/🔍诊断-检查缺失字段.sql` - 诊断脚本
### API文档
- [出国行程变更管理API.md](../开发文档/API/出国行程变更管理API.md) - API接口文档
---
## 💡 经验总结
### 问题分析
1. **数据库表结构不一致**:开发过程中表结构有变化
2. **后端Mapper查询字段多**:包含了个人信息字段
3. **VO类有字段但表没有**:导致MyBatis映射失败
### 解决方案
1. ✅ **智能增量修复**:检查后只添加缺失字段
2. ✅ **动态SQL执行**:避免重复字段错误
3. ✅ **完整验证**:确保所有字段都已添加
### 预防措施
1. **保持表结构文档最新**:及时更新SQL脚本
2. **使用版本控制管理SQL**:记录每次变更
3. **开发环境与生产环境同步**:定期对比表结构
4. **VO类字段与表字段对应**:避免字段缺失
---
## 🎯 总结
### ✅ 已完成
- [x] 检查数据库当前表结构
- [x] 识别缺失的字段
- [x] 创建智能修复SQL
- [x] 执行修复SQL
- [x] 验证字段已添加
- [x] 编写修复报告
### ⚠️ 待执行(您需要做的)
- [ ] 重启后端服务
- [ ] 清除浏览器缓存
- [ ] 测试审批功能
- [ ] 确认问题已解决
---
## 📞 支持
### 如果遇到问题
**后端服务启动失败**:
- 检查数据库连接
- 查看启动日志
- 确认端口未被占用
**仍然报字段缺失错误**:
- 确认SQL执行成功
- 确认后端已重启
- 检查是否连接了正确的数据库
**审批功能仍然报错**:
- 查看后端详细错误日志
- 检查是否还有其他缺失字段
- 参考故障排查文档
**需要帮助**:
- 查看详细修复指南
- 查看问题汇总文档
- 查看测试指南
---
**🎉 数据库修复已完成!现在只需要重启后端服务,就可以正常使用审批功能了!** 💪
---
**报告生成时间**: 2025-10-13
**修复状态**: ✅ 数据库修复成功,待重启后端验证
**下一步**: 重启后端服务 → 清除缓存 → 测试功能
@@ -1,197 +0,0 @@
# 🧪 前端验证指南 - 行程变更状态修复
## 📋 验证步骤
### 1️⃣ 清除浏览器缓存
**方式一:硬刷新(推荐)**
- Windows/Linux: `Ctrl + Shift + R` 或 `Ctrl + F5`
- Mac: `Cmd + Shift + R`
**方式二:清除浏览器缓存**
1. 打开浏览器开发者工具(F12)
2. 右键点击刷新按钮
3. 选择"清空缓存并硬性重新加载"
### 2️⃣ 访问页面
导航路径:
```
系统管理 → 人事管理 → 出国行程变更申请 → 变更行程管理
```
或直接访问URL:
```
http://localhost/#/rs/overseas/itinerary-change
```
### 3️⃣ 检查变更状态列
在"变更申请"选项卡中,检查"变更状态"列是否正确显示:
✅ **正确显示**:
- 🔵 待审核(蓝色标签)
- 🟢 已通过(绿色标签)
- 🔴 已拒绝(红色标签)
- ⚪ 已撤回(灰色标签)
❌ **错误显示**:
- `a独...a®re...`(乱码)
- 空白
- NULL
### 4️⃣ 检查开发者工具
**查看API响应**:
1. 打开开发者工具(F12)
2. 切换到"Network"标签
3. 刷新页面
4. 找到 `/rs/itinerary-change/list` 请求
5. 查看响应数据中的 `statusText` 字段
**预期响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"rows": [
{
"changeId": "1977740586114916354",
"applicantName": "admin",
"status": "0",
"statusText": "待审核", // ✅ 应该是中文
...
}
]
}
}
```
---
## 🐛 如果仍然显示乱码
### 方案1:检查后端服务
1. **重启后端服务**
```bash
# 停止后端服务
# 重新启动后端服务
```
2. **检查后端日志**
- 查看是否有SQL执行错误
- 查看MyBatis映射是否正常
### 方案2:检查数据库连接字符集
**后端配置文件** (`application.yml` 或 `application.properties`):
```yaml
spring:
datasource:
url: jdbc:mysql://host:port/database?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
```
确保URL中包含:
- `useUnicode=true`
- `characterEncoding=utf8mb4`
### 方案3:检查MyBatis配置
**Mapper XML位置**:
```
backend/ruoyi-modules/ruoyi-rs/src/main/resources/mapper/rs/RsOverseasItineraryChangeMapper.xml
```
**检查关键SQL**(第102-104行):
```xml
<sql id="selectChangeVo">
SELECT
dict_status.dict_label AS status_text
FROM rs_overseas_itinerary_change c
LEFT JOIN sys_dict_data dict_status
ON dict_status.dict_value = c.status
AND dict_status.dict_type = 'rs_itinerary_change_status'
</sql>
```
### 方案4:重新执行修复脚本
如果数据库数据再次出现问题,执行:
```bash
mysql -h<host> -P<port> -u<user> -p<password> <database> < docs/设计文档/database/修复-行程变更状态字典编码.sql
```
---
## 🔍 数据库快速诊断命令
### 检查字典数据编码
```sql
SET NAMES utf8mb4;
SELECT
dict_label,
HEX(dict_label) as hex_encoding,
LENGTH(dict_label) as byte_length,
CHAR_LENGTH(dict_label) as char_length
FROM sys_dict_data
WHERE dict_type = 'rs_itinerary_change_status'
ORDER BY dict_sort;
```
**正确的结果**:
| dict_label | hex_encoding | byte_length | char_length |
|-----------|--------------|-------------|-------------|
| 待审核 | E5BE85E5AEA1E6A0B8 | 9 | 3 |
| 已通过 | E5B7B2E9809AE8BF87 | 9 | 3 |
| 已拒绝 | E5B7B2E68B92E7BB9D | 9 | 3 |
| 已撤回 | E5B7B2E692A4E59B9E | 9 | 3 |
### 测试字典关联查询
```sql
SET NAMES utf8mb4;
SELECT
c.change_id,
c.applicant_name,
c.status,
d.dict_label as status_text
FROM rs_overseas_itinerary_change c
LEFT JOIN sys_dict_data d
ON d.dict_value = c.status
AND d.dict_type = 'rs_itinerary_change_status'
WHERE c.deleted = 0
LIMIT 5;
```
**预期结果**:`status_text` 列应该显示中文(如:待审核、已通过)
---
## ✅ 验证成功标准
1. ✅ 前端列表正确显示中文状态标签
2. ✅ API响应中 `statusText` 字段为中文
3. ✅ 详情弹窗中状态显示正确
4. ✅ 变更记录选项卡中状态显示正确
5. ✅ 数据库HEX编码为正确的UTF-8格式
---
## 📞 联系支持
如果按照以上步骤操作后仍然存在问题:
1. 检查 `docs/问题解决方案/行程变更状态乱码修复总结.md`
2. 查看数据库执行日志
3. 检查后端服务日志
4. 验证字典数据是否正确插入
---
**文档创建时间**:2025-10-13
**适用版本**:出国行程变更管理功能 v1.0
**修复状态**:✅ 已修复
@@ -1,313 +0,0 @@
# 变更记录Tab显示逻辑修复总结
## 📋 问题描述
**问题现象**:
- 出国行程变更申请菜单下,"变更申请"和"变更记录"两个选项卡显示的是相同的数据
- 用户期望:"变更记录"只显示已通过审批的变更申请,而不是所有状态的申请
**发现日期**: 2025-10-13
---
## 🎯 需求分析
### 正确的业务逻辑
#### 变更申请选项卡
- **API接口**: `/rs/itinerary/change/list`
- **显示内容**: 所有的变更申请记录
- **包含状态**:
- 0 = 待审核
- 1 = 已通过
- 2 = 已拒绝
- 3 = 已撤回
- **功能**: 新增、编辑、删除、查看、撤回
#### 变更记录选项卡
- **API接口**: `/rs/itinerary/change/my/list`
- **显示内容**: 🔴 **只显示已通过的变更申请**
- **过滤条件**: `status = '1'`(已通过)
- **业务含义**: 真正生效的变更记录
- **功能**: 查看已通过的变更记录
---
## 🔍 问题原因
### 原有实现
**后端Service实现** (`RsOverseasItineraryChangeServiceImpl.java`):
```java
@Override
public TableDataInfo<RsOverseasItineraryChangeVo> queryMyChangeList(RsOverseasItineraryChangeQuery query) {
Long userId = LoginHelper.getUserId();
query.setApplicantId(userId);
// ❌ 问题:没有过滤状态,返回所有状态的申请
return queryPageList(query);
}
```
**问题点**:
- 只设置了 `applicantId`(当前用户),没有设置 `status`
- 导致返回所有状态的变更申请
---
## 🔧 修复方案
### 1. 后端Service层修复
**修改文件**:
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/impl/RsOverseasItineraryChangeServiceImpl.java`
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/service/IRsOverseasItineraryChangeService.java`
**修改内容**:
```java
/**
* 查询我的变更记录列表(当前登录用户的已通过变更申请)
* 说明:变更记录Tab只显示已通过审批的变更申请,这些是真正生效的变更记录
*/
@Override
public TableDataInfo<RsOverseasItineraryChangeVo> queryMyChangeList(RsOverseasItineraryChangeQuery query) {
Long userId = LoginHelper.getUserId();
query.setApplicantId(userId);
query.setStatus("1"); // ✅ 只查询已通过的变更申请
return queryPageList(query);
}
```
**关键改动**:
- ✅ 添加 `query.setStatus("1")`
- ✅ 更新JavaDoc注释,明确说明业务逻辑
---
### 2. 后端Controller层更新
**修改文件**:
- `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasItineraryChangeController.java`
**修改内容**:
```java
/**
* 查询我的变更记录列表(只显示已通过的变更申请)
* 说明:变更记录Tab只显示已通过审批的变更申请,这些是真正生效的变更记录
*/
@SaCheckPermission("rs:itineraryChange:list")
@GetMapping("/my/list")
public TableDataInfo<RsOverseasItineraryChangeVo> myList(RsOverseasItineraryChangeQuery query) {
query.setTenantId(LoginHelper.getTenantId());
return changeService.queryMyChangeList(query);
}
```
**关键改动**:
- ✅ 更新接口注释,明确说明只返回已通过的数据
---
### 3. 前端组件更新
**修改文件**:
- `web/src/views/rs/overseas/itinerary-change.vue`
**修改内容**:
```vue
<!-- Tab 2: 变更记录(只显示已通过的变更申请) -->
<el-tab-pane label="变更记录" name="my">
<div class="tab-content">
<!-- ... -->
</div>
</el-tab-pane>
```
```typescript
// 查询我的变更记录列表(只显示已通过的变更申请)
const getMyList = async () => {
state.myLoading = true;
try {
const response = await listMyItineraryChange(state.myQueryParams);
state.myChangeList = response.rows;
state.myTotal = response.total;
} catch (error) {
console.error('获取我的变更记录失败:', error);
} finally {
state.myLoading = false;
}
};
```
**关键改动**:
- ✅ 更新注释,明确"变更记录"只显示已通过的数据
---
### 4. 前端API接口更新
**修改文件**:
- `web/src/api/rs/overseas.ts`
**修改内容**:
```typescript
// 查询我的变更记录列表(只返回已通过的变更申请)
export function listMyItineraryChange(query: ItineraryChangeQuery) {
return request({
url: '/rs/itinerary/change/my/list',
method: 'get',
params: query
});
}
```
**关键改动**:
- ✅ 更新API注释
---
### 5. API文档创建
**新增文件**:
- `docs/开发文档/API/出国行程变更管理API.md`
**关键内容**:
```markdown
### 2. 查询变更记录列表
**接口说明**: 查询当前用户的变更记录(只返回已通过的变更申请)
**重要说明**:
- 🔴 **此接口只返回status='1'(已通过)的变更申请**
- 变更记录代表真正生效的变更记录,不包括待审核、已拒绝、已撤回的申请
**后端自动过滤条件**:
- `applicantId` = 当前登录用户ID(自动设置)
- `status` = '1'(只查询已通过的,自动设置)
```
---
## ✅ 修复验证
### 验证步骤
1. **重启后端服务**
```bash
# 重启Spring Boot应用
```
2. **清除前端缓存并刷新**
- Windows/Linux: `Ctrl + Shift + R`
- Mac: `Cmd + Shift + R`
3. **测试场景**
#### 场景1: 提交一个新的变更申请
- 状态应该是"待审核"
- 在"变更申请"Tab中可以看到
- 在"变更记录"Tab中**不应该**看到
#### 场景2: 审批通过一个变更申请
- 状态变为"已通过"
- 在"变更申请"Tab中可以看到
- 在"变更记录"Tab中**应该**看到
#### 场景3: 拒绝或撤回一个变更申请
- 状态变为"已拒绝"或"已撤回"
- 在"变更申请"Tab中可以看到
- 在"变更记录"Tab中**不应该**看到
### 预期结果
**变更申请Tab**:
```
✅ 显示所有状态的变更申请
- 待审核
- 已通过
- 已拒绝
- 已撤回
```
**变更记录Tab**:
```
✅ 只显示已通过的变更申请
- 已通过 ✓
```
---
## 📁 修改文件清单
| 序号 | 文件路径 | 修改类型 | 说明 |
|-----|---------|---------|------|
| 1 | `backend/.../RsOverseasItineraryChangeServiceImpl.java` | 🔧 修改 | 添加status过滤 |
| 2 | `backend/.../IRsOverseasItineraryChangeService.java` | 📝 注释 | 更新JavaDoc |
| 3 | `backend/.../RsOverseasItineraryChangeController.java` | 📝 注释 | 更新接口注释 |
| 4 | `web/src/views/rs/overseas/itinerary-change.vue` | 📝 注释 | 更新组件注释 |
| 5 | `web/src/api/rs/overseas.ts` | 📝 注释 | 更新API注释 |
| 6 | `docs/开发文档/API/出国行程变更管理API.md` | ✨ 新增 | 创建完整API文档 |
| 7 | `docs/问题解决方案/变更记录Tab显示逻辑修复总结.md` | ✨ 新增 | 本文档 |
---
## 🎯 业务规则总结
### 数据分类
| 分类 | API接口 | 过滤条件 | 显示位置 |
|-----|---------|---------|----------|
| 变更申请 | `/rs/itinerary/change/list` | 无(所有状态) | 变更申请Tab |
| 变更记录 | `/rs/itinerary/change/my/list` | `status='1'` | 变更记录Tab |
| 待审批 | `/rs/itinerary/change/pending/list` | `status='0'` | 审批页面 |
### 状态流转
```
待审核(0) ──审批通过──> 已通过(1) ──显示在──> 变更记录Tab
│
├─审批拒绝──> 已拒绝(2) ──不显示在──> 变更记录Tab
│
└─用户撤回──> 已撤回(3) ──不显示在──> 变更记录Tab
```
---
## 💡 经验总结
### 问题根因
- 接口命名容易产生误解:`queryMyChangeList` 容易被理解为"我的变更申请"
- 业务逻辑未在代码中明确体现,导致实现偏差
### 最佳实践
1. ✅ **明确的业务注释**: 在Service、Controller、前端API中都添加清晰的注释
2. ✅ **状态过滤显式化**: 后端主动设置状态过滤,不依赖前端传参
3. ✅ **完整的API文档**: 详细说明每个接口的业务含义和过滤条件
4. ✅ **前后端命名一致**: 使用统一的术语("变更记录"="已通过的变更申请")
### 类似场景参考
- 出国申请管理:申请Tab vs 记录Tab
- 出国登记管理:登记Tab vs 记录Tab
- 其他需要区分"申请"和"记录"的业务模块
---
## 📞 相关文档
- [出国行程变更管理API文档](../开发文档/API/出国行程变更管理API.md)
- [出国申请管理API文档](../开发文档/API/出国申请管理API.md)
- [数据库设计文档](../设计文档/database/rs_overseas_itinerary_change_full.sql)
---
**修复完成时间**: 2025-10-13
**影响范围**: 出国行程变更管理功能
**修复类型**: 业务逻辑修正
**风险评估**: 低(仅影响数据过滤,不影响数据完整性)
**回归测试**: 需要验证变更申请和变更记录的显示逻辑
@@ -1,306 +0,0 @@
# 变更记录显示逻辑 - 测试指南
## 🎯 测试目标
验证"变更申请"和"变更记录"两个Tab的数据显示逻辑是否正确
---
## 📋 测试前准备
### 1. 重启后端服务
确保后端代码修改生效:
```bash
# 停止后端服务
# 重新启动后端服务
```
### 2. 清除前端缓存
**方式一:硬刷新(推荐)**
- Windows/Linux: `Ctrl + Shift + R` 或 `Ctrl + F5`
- Mac: `Cmd + Shift + R`
**方式二:清除浏览器缓存**
1. 打开浏览器开发者工具(F12)
2. 右键点击刷新按钮
3. 选择"清空缓存并硬性重新加载"
### 3. 准备测试数据
确保数据库中有:
- ✅ 至少1个已审批通过的出国申请
- ✅ 当前用户有提交变更申请的权限
---
## 🧪 测试场景
### 场景1:新提交的变更申请(待审核状态)
#### 步骤
1. 导航到:**人事管理 → 出国行程变更申请 → 变更行程管理**
2. 默认打开"变更申请"Tab
3. 点击【新增】按钮
4. 选择一个已审批通过的出国申请
5. 填写变更信息,提交
#### 预期结果
**变更申请Tab** ✅
- 列表中**应该显示**刚提交的变更申请
- 状态显示:🔵 待审核
- 可以看到完整的记录信息
**变更记录Tab** ❌
- 列表中**不应该显示**刚提交的变更申请
- 因为状态是"待审核",不是"已通过"
#### 验证SQL
```sql
-- 查看刚提交的变更申请
SELECT
change_id,
applicant_name,
status,
original_destination,
new_destination,
create_time
FROM rs_overseas_itinerary_change
WHERE deleted = 0
AND status = '0' -- 待审核
ORDER BY create_time DESC
LIMIT 1;
```
---
### 场景2:审批通过的变更申请
#### 步骤
1. 导航到:**人事管理 → 出国行程变更审批**
2. 在"待审批"Tab中找到刚提交的变更申请
3. 点击【审批】按钮
4. 选择"通过",填写审批意见,提交
5. 返回到:**人事管理 → 出国行程变更申请 → 变更行程管理**
#### 预期结果
**变更申请Tab** ✅
- 列表中**应该显示**该变更申请
- 状态显示:🟢 已通过
- 可以看到审批信息(审批人、审批时间、审批意见)
**变更记录Tab** ✅
- 列表中**应该显示**该变更申请
- 这是真正生效的变更记录
- 状态显示:🟢 已通过
#### 验证SQL
```sql
-- 查看已通过的变更申请
SELECT
change_id,
applicant_name,
status,
original_destination,
new_destination,
approver_name,
approve_time
FROM rs_overseas_itinerary_change
WHERE deleted = 0
AND status = '1' -- 已通过
ORDER BY approve_time DESC
LIMIT 5;
```
---
### 场景3:审批拒绝的变更申请
#### 步骤
1. 提交一个新的变更申请
2. 导航到审批页面,选择"拒绝"
3. 返回到变更行程管理页面
#### 预期结果
**变更申请Tab** ✅
- 列表中**应该显示**该变更申请
- 状态显示:🔴 已拒绝
**变更记录Tab** ❌
- 列表中**不应该显示**该变更申请
- 因为状态是"已拒绝",不是"已通过"
---
### 场景4:撤回的变更申请
#### 步骤
1. 提交一个新的变更申请
2. 在"变更申请"Tab中找到该申请
3. 点击【撤回】按钮
#### 预期结果
**变更申请Tab** ✅
- 列表中**应该显示**该变更申请
- 状态显示:⚪ 已撤回
**变更记录Tab** ❌
- 列表中**不应该显示**该变更申请
- 因为状态是"已撤回",不是"已通过"
---
## 🔍 API请求验证
### 使用浏览器开发者工具
1. 打开开发者工具(F12)
2. 切换到"Network"标签
3. 执行以下操作并观察请求
#### 变更申请Tab请求
**请求URL**:
```
GET /rs/itinerary/change/list?pageNum=1&pageSize=10
```
**预期响应**:
```json
{
"code": 200,
"rows": [
{"changeId": "...", "status": "0", "statusText": "待审核"},
{"changeId": "...", "status": "1", "statusText": "已通过"},
{"changeId": "...", "status": "2", "statusText": "已拒绝"},
{"changeId": "...", "status": "3", "statusText": "已撤回"}
]
}
```
**验证点**:
- ✅ 包含所有状态(0, 1, 2, 3)的变更申请
---
#### 变更记录Tab请求
**请求URL**:
```
GET /rs/itinerary/change/my/list?pageNum=1&pageSize=10
```
**预期响应**:
```json
{
"code": 200,
"rows": [
{"changeId": "...", "status": "1", "statusText": "已通过"},
{"changeId": "...", "status": "1", "statusText": "已通过"}
]
}
```
**验证点**:
- ✅ **只包含status='1'(已通过)的变更申请**
- ❌ 不包含status='0'、'2'、'3'的申请
---
## 📊 测试数据对比表
### 各状态的显示情况
| 变更申请状态 | 状态值 | 变更申请Tab | 变更记录Tab |
|------------|-------|-----------|-----------|
| 待审核 | 0 | ✅ 显示 | ❌ 不显示 |
| 已通过 | 1 | ✅ 显示 | ✅ 显示 |
| 已拒绝 | 2 | ✅ 显示 | ❌ 不显示 |
| 已撤回 | 3 | ✅ 显示 | ❌ 不显示 |
---
## 🐛 常见问题排查
### 问题1:变更记录Tab显示所有状态的申请
**可能原因**:
- 后端代码未重启
- 前端缓存未清除
**解决方案**:
1. 重启后端服务
2. 清除前端缓存(硬刷新)
3. 检查后端日志,确认Service层的status过滤生效
**验证SQL**:
```sql
-- 检查后端是否正确过滤
-- 观察后端日志中的SQL语句,应该包含:
-- WHERE ... AND status = '1'
```
---
### 问题2:变更记录Tab为空
**可能原因**:
- 没有已通过的变更申请
- 当前用户没有已通过的变更申请
**解决方案**:
1. 提交一个变更申请
2. 审批通过
3. 刷新变更记录Tab
**验证SQL**:
```sql
-- 检查是否有已通过的变更申请
SELECT COUNT(*)
FROM rs_overseas_itinerary_change
WHERE deleted = 0
AND status = '1'
AND applicant_id = <当前用户ID>;
```
---
### 问题3:状态显示为乱码
**可能原因**:
- 字典数据编码问题(已在之前修复)
**解决方案**:
- 参考 `行程变更状态乱码修复总结.md`
---
## ✅ 测试通过标准
### 核心验证点
1. ✅ **变更申请Tab**: 显示所有状态的变更申请
2. ✅ **变更记录Tab**: 只显示已通过(status='1')的变更申请
3. ✅ **API响应**: `/my/list`接口只返回status='1'的数据
4. ✅ **状态标签**: 正确显示中文状态(待审核、已通过、已拒绝、已撤回)
5. ✅ **数据准确性**: 状态流转正确(提交→审批→显示)
---
## 📞 相关文档
- [变更记录Tab显示逻辑修复总结](./变更记录Tab显示逻辑修复总结.md)
- [出国行程变更管理API文档](../开发文档/API/出国行程变更管理API.md)
- [行程变更状态乱码修复总结](./行程变更状态乱码修复总结.md)
---
**测试指南创建时间**: 2025-10-13
**适用版本**: 出国行程变更管理功能 v1.0
**测试类型**: 业务逻辑验证
@@ -1,270 +0,0 @@
# 出国申请登记表PDF生成功能实现总结
## 📋 需求概述
为出国申请管理系统增加自动生成《登记备案人员因私事出国(境)登记表》功能,支持PDF格式文档生成、在线预览、下载和打印。
## 🎯 实现内容
### 1. 后端实现
#### 1.1 核心文件结构
```
backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/
├── domain/
│ ├── vo/RegistrationFormVO.java # PDF生成响应VO
│ ├── bo/GenerateRegistrationFormBo.java # 生成请求BO
│ └── DocumentInfo.java # 文档信息存储实体
├── service/
│ ├── IRegistrationFormService.java # PDF生成服务接口
│ └── impl/RegistrationFormServiceImpl.java # PDF生成服务实现
└── controller/RsOverseasApplicationController.java # 控制器(新增PDF接口)
```
#### 1.2 API接口设计
**生成登记表PDF**
```http
POST /rs/overseas/application/generateRegistrationForm
Authorization: Bearer {token}
Content-Type: application/json
{
"applicationId": 1,
"documentNumber": "CG20250103123456",
"format": "pdf"
}
```
**下载PDF文档**
```http
GET /rs/overseas/application/downloadPdf/{documentId}
Authorization: Bearer {token}
```
**预览PDF文档**
```http
GET /rs/overseas/application/previewPdf/{documentId}
Authorization: Bearer {token}
```
#### 1.3 PDF模板设计
严格按照实际登记表图片格式实现:
- **标题**: 登记报备人员因私事出国(境)登记表
- **编号**: 右上角自动生成编号
- **表格结构**: 7列×7行复杂表格布局
- **照片区域**: 右侧合并单元格,标注尺寸要求
- **盖章区域**: 专门的盖章位置
- **填表说明**: 底部说明文字
#### 1.4 技术特点
- **PDF生成**: 使用iText 5.5.13.3库
- **中文支持**: STSong-Light字体,UniGB-UCS2-H编码
- **文件存储**: 按日期分类存储 (`/uploads/registration-forms/yyyy/MM/dd/`)
- **内存缓存**: 使用ConcurrentHashMap临时存储文档信息
- **安全控制**: Sa-Token权限验证
- **异常处理**: 完善的错误处理和日志记录
### 2. 前端实现
#### 2.1 界面集成
**按钮位置**: 出国申请列表 → 操作列 → "生成登记表"按钮
- 绿色按钮,Document图标
- 只有已提交、已通过或已完成的申请可以生成
- 需要`rs:overseas:generate`权限
#### 2.2 生成弹窗设计
```vue
<!-- 生成登记表弹窗 -->
<el-dialog title="📋 生成出国登记表" width="800px">
<!-- 申请信息预览 -->
<el-card class="application-preview">
<el-descriptions :column="2" border>
<!-- 显示申请人基本信息 -->
</el-descriptions>
</el-card>
<!-- 生成选项 -->
<el-card class="generation-options">
<!-- 文档格式、编号、时间等选项 -->
</el-card>
<!-- 预览区域(生成后显示) -->
<el-card v-if="generatedDocument" class="document-preview">
<!-- PDF信息和操作按钮 -->
<el-button type="primary" @click="handleDownloadPdf">下载PDF</el-button>
<el-button type="success" @click="handlePrintPdf">直接打印</el-button>
<el-button type="info" @click="handleFullPreviewPdf">全屏预览</el-button>
</el-card>
</el-dialog>
```
#### 2.3 核心功能方法
- `canGenerateRegistrationForm()`: 判断是否可生成登记表
- `handleGenerateRegistrationForm()`: 打开生成弹窗
- `handleGenerateForm()`: 调用后端生成PDF
- `handleDownloadPdf()`: 下载PDF文件
- `handlePrintPdf()`: 直接打印PDF
- `handleFullPreviewPdf()`: 全屏预览PDF
### 3. API接口定义
#### 3.1 TypeScript类型定义
```typescript
// 生成请求参数
interface GenerateRegistrationFormRequest {
applicationId: number;
documentNumber?: string;
format?: string;
}
// 生成响应数据
interface RegistrationFormResponse {
documentId: string;
fileName: string;
downloadUrl: string;
previewUrl: string;
fileSize: number;
generateTime: string;
documentNumber: string;
applicationId: number;
}
```
#### 3.2 API方法
```typescript
// 生成登记表
export function generateRegistrationForm(data: GenerateRegistrationFormRequest)
// 下载PDF
export function downloadRegistrationFormPdf(documentId: string)
// 预览PDF
export function previewRegistrationFormPdf(documentId: string)
```
## 🔧 技术规范
### 1. 文件存储规范
- **存储路径**: `/uploads/registration-forms/yyyy/MM/dd/`
- **文件命名**: `registration_form_{applicationId}_{timestamp}.pdf`
- **文件大小限制**: 最大5MB
- **保留期限**: 90天自动清理(可配置)
### 2. PDF格式要求
- **页面尺寸**: A4 (210×297mm)
- **边距设置**: 上下左右各25mm
- **字体要求**: 支持中文,使用宋体
- **分辨率**: 300 DPI,确保打印清晰
### 3. 安全规范
- **访问控制**: 需要登录和权限验证
- **权限要求**: `rs:overseas:generate`、`rs:overseas:download`、`rs:overseas:preview`
- **日志记录**: 记录生成、下载、打印操作
## 🧪 测试验证
### 1. 测试脚本
已创建完整的Python测试脚本:`backend/ruoyi-modules/ruoyi-rs/test_registration_form_pdf.py`
### 2. 测试内容
- ✅ PDF生成接口测试
- ✅ PDF下载功能测试
- ✅ 文件格式验证
- ✅ 权限控制测试
- ✅ 异常情况处理测试
### 3. 运行测试
```bash
cd backend/ruoyi-modules/ruoyi-rs/
python test_registration_form_pdf.py
```
## 📊 功能特点
### 1. 🎯 严格按照实际表格格式
- 100%复制真实登记表样式
- 精确的表格布局和字段映射
- 标准的PDF格式输出
### 2. 🚀 完整的用户体验
- 一键生成PDF文档
- 在线预览功能
- 直接下载和打印
- 友好的操作界面
### 3. 🔒 安全可靠
- 权限控制验证
- 文件安全存储
- 操作日志记录
- 异常处理机制
### 4. 📱 响应式设计
- 支持各种屏幕尺寸
- 移动端友好界面
- 现代化UI设计
## 🚀 部署说明
### 1. 后端部署
1. 确保已添加iText依赖(已在pom.xml中配置)
2. 配置文件上传路径 `ruoyi.profile`
3. 重启后端服务
### 2. 前端部署
1. 前端代码已集成到现有页面
2. 确保权限配置正确
3. 重新构建前端项目
### 3. 权限配置
在系统管理中配置以下权限:
- `rs:overseas:generate` - 生成登记表
- `rs:overseas:download` - 下载PDF
- `rs:overseas:preview` - 预览PDF
## 📈 性能指标
- **生成速度**: 单次生成响应时间 < 3秒
- **文件大小**: 标准PDF约50-100KB
- **并发支持**: 支持多用户同时生成
- **存储效率**: 按日期分类,便于管理
## 🔄 后续扩展
### 1. 功能增强
- [ ] 支持批量生成
- [ ] PDF水印功能
- [ ] 电子签章集成
- [ ] 模板自定义
### 2. 性能优化
- [ ] 文档缓存机制
- [ ] 异步生成队列
- [ ] 文件压缩优化
- [ ] CDN分发支持
### 3. 移动端适配
- [ ] 移动端生成功能
- [ ] 微信小程序集成
- [ ] App端支持
## 📚 相关文档
- [出国申请管理API文档](../开发文档/API/出国申请管理API.md)
- [前端组件开发规范](../../.cursor/cursorrules-frontend.md)
- [后端开发规范](../../.cursor/cursorrules-backend.md)
- [PDF生成技术方案](../技术文档/PDF生成方案.md)
---
**实现完成时间**: 2025-01-03
**实现人员**: weizzzz
**版本**: v1.0.0
@@ -1,496 +0,0 @@
# 出国申请管理系统需求文档
**项目名称**: 校园管理系统 - 出国申请管理模块
**文档版本**: v1.0
**创建时间**: 2025-07-28
**更新时间**: 2025-07-28
## 📋 需求概述
### 业务背景
为规范学校教职工出国(出境)申请流程,建立标准化的出国申请、审批、记录管理体系,提升出国事务管理的效率和规范性。
### 核心目标
1. **流程规范化**:建立标准的出国申请和审批流程
2. **权限分离**:区分申请管理和审批管理功能,实现权限精细化控制
3. **过程透明**:申请进度可视化,审批过程可追溯
4. **数据完整**:完整记录出国申请全过程数据,支持统计分析
## 🎯 功能模块设计
### 模块架构
参考培训申请和培训审批的权限分离设计,出国申请管理分为两个独立模块:
```
人事管理
├── 出国申请 (rs:overseas:*) ← 申请管理功能
└── 出国审批 (rs:overseasApproval:*) ← 审批管理功能
```
## 📊 出国申请模块 (rs:overseas:*)
### 功能定位
面向教职工的出国申请管理平台,提供申请提交、查看、修改等功能。
### 主要功能
#### 1. 出国申请选项卡
- **申请提交**:新增出国申请
- **申请查看**:查看申请详情和状态
- **申请修改**:编辑待审核的申请
- **申请删除**:删除申请记录
- **申请导出**:导出申请数据
#### 2. 出国记录选项卡
- **记录查看**:查看已完成的出国记录
- **记录编辑**:更新出国记录信息
- **材料上传**:上传出国证明材料
- **记录导出**:导出出国记录
### 核心字段设计
#### 出国申请基础信息
```javascript
{
applicationId: string, // 申请ID(雪花ID)
applicantId: string, // 申请人ID
applicantName: string, // 申请人姓名
department: string, // 所属部门
position: string, // 职位
contactPhone: string, // 联系电话
contactEmail: string, // 联系邮箱
}
```
#### 出国详细信息
```javascript
{
purpose: string, // 出国目的(学术交流、培训、会议、考察等)
purposeDetail: string, // 目的详细说明
destination: string, // 目的地国家/地区
destinationCity: string, // 目的地城市
startDate: date, // 出国开始日期
endDate: date, // 出国结束日期
duration: number, // 出国天数
budget: number, // 预算金额
budgetSource: string, // 经费来源(学校、个人、项目、其他)
invitation: string, // 邀请方信息
itinerary: string, // 行程安排
accommodationPlan: string, // 住宿安排
transportationPlan: string, // 交通安排
}
```
#### 审批流程信息
```javascript
{
status: string, // 申请状态(0-待审核,1-已通过,2-已拒绝,3-已取消,4-已完成)
currentApprover: string, // 当前审批人
approvalLevel: string, // 当前审批级别
applyTime: datetime, // 申请时间
approvalTime: datetime, // 审批时间
adminRemark: string, // 管理员备注
applicantRemark: string, // 申请人备注
}
```
### 页面布局设计
#### 出国申请表单
```vue
<el-form>
<!-- 基础信息 -->
<el-card title="基础信息">
<el-form-item label="所属部门" required>
<el-select v-model="form.department" placeholder="请选择部门">
<!-- 从sys_dept获取部门列表 -->
</el-select>
</el-form-item>
<el-form-item label="联系电话" required>
<el-input v-model="form.contactPhone" />
</el-form-item>
<el-form-item label="联系邮箱" required>
<el-input v-model="form.contactEmail" />
</el-form-item>
</el-card>
<!-- 出国信息 -->
<el-card title="出国信息">
<el-form-item label="出国目的" required>
<el-select v-model="form.purpose">
<el-option label="学术交流" value="academic" />
<el-option label="培训学习" value="training" />
<el-option label="国际会议" value="conference" />
<el-option label="考察访问" value="visit" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="目的详细说明" required>
<el-input type="textarea" v-model="form.purposeDetail" />
</el-form-item>
<el-form-item label="目的地国家" required>
<el-input v-model="form.destination" />
</el-form-item>
<el-form-item label="目的地城市" required>
<el-input v-model="form.destinationCity" />
</el-form-item>
</el-card>
<!-- 时间和预算 -->
<el-card title="时间和预算">
<el-form-item label="出国时间" required>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="预算金额" required>
<el-input-number v-model="form.budget" :min="0" />
</el-form-item>
<el-form-item label="经费来源" required>
<el-select v-model="form.budgetSource">
<el-option label="学校经费" value="school" />
<el-option label="个人承担" value="personal" />
<el-option label="项目经费" value="project" />
<el-option label="其他来源" value="other" />
</el-select>
</el-form-item>
</el-card>
<!-- 详细安排 -->
<el-card title="详细安排">
<el-form-item label="邀请方信息">
<el-input type="textarea" v-model="form.invitation" />
</el-form-item>
<el-form-item label="行程安排" required>
<el-input type="textarea" v-model="form.itinerary" />
</el-form-item>
<el-form-item label="住宿安排">
<el-input type="textarea" v-model="form.accommodationPlan" />
</el-form-item>
<el-form-item label="交通安排">
<el-input type="textarea" v-model="form.transportationPlan" />
</el-form-item>
<el-form-item label="申请备注">
<el-input type="textarea" v-model="form.applicantRemark" />
</el-form-item>
</el-card>
</el-form>
```
## 📝 出国审批模块 (rs:overseasApproval:*)
### 功能定位
面向管理员和审批人员的出国申请审批平台,提供审批处理、流程管理等功能。
### 主要功能
#### 1. 待审批申请选项卡
- **申请审批**:审批出国申请(通过/拒绝)
- **审批查询**:查询和筛选待审批申请
- **批量审批**:批量处理多个申请
- **审批导出**:导出审批数据
#### 2. 审批记录选项卡
- **历史审批**:查看已审批的申请记录
- **审批统计**:审批数据统计分析
- **记录导出**:导出审批记录
### 审批流程设计
#### 审批层级
```javascript
// 基于金额和天数的审批层级
const approvalRules = {
level1: { // 一级审批:部门主管
condition: "budget <= 10000 && duration <= 7",
approver: "部门主管"
},
level2: { // 二级审批:分管领导
condition: "budget <= 50000 && duration <= 30",
approver: "分管领导"
},
level3: { // 三级审批:校领导
condition: "budget > 50000 || duration > 30",
approver: "校领导"
}
}
```
#### 审批状态流转
```
申请提交 → 部门审核 → 分管领导审批 → 校领导审批 → 审批完成
↓ ↓ ↓ ↓
待审核 已通过/拒绝 已通过/拒绝 已通过/拒绝
```
## 🗄️ 数据库设计
### 主表:rs_overseas_application
```sql
CREATE TABLE rs_overseas_application (
application_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '申请ID',
applicant_id BIGINT(20) NOT NULL COMMENT '申请人ID',
applicant_name VARCHAR(50) NOT NULL COMMENT '申请人姓名',
department VARCHAR(100) NOT NULL COMMENT '所属部门',
position VARCHAR(100) COMMENT '职位',
contact_phone VARCHAR(20) COMMENT '联系电话',
contact_email VARCHAR(100) COMMENT '联系邮箱',
purpose VARCHAR(50) NOT NULL COMMENT '出国目的',
purpose_detail TEXT NOT NULL COMMENT '目的详细说明',
destination VARCHAR(100) NOT NULL COMMENT '目的地国家',
destination_city VARCHAR(100) COMMENT '目的地城市',
start_date DATE NOT NULL COMMENT '开始日期',
end_date DATE NOT NULL COMMENT '结束日期',
duration INT NOT NULL COMMENT '出国天数',
budget DECIMAL(15,2) NOT NULL COMMENT '预算金额',
budget_source VARCHAR(50) NOT NULL COMMENT '经费来源',
invitation TEXT COMMENT '邀请方信息',
itinerary TEXT NOT NULL COMMENT '行程安排',
accommodation_plan TEXT COMMENT '住宿安排',
transportation_plan TEXT COMMENT '交通安排',
status CHAR(1) NOT NULL DEFAULT '0' COMMENT '申请状态',
current_approver VARCHAR(100) COMMENT '当前审批人',
approval_level CHAR(1) COMMENT '审批级别',
apply_time DATETIME NOT NULL COMMENT '申请时间',
approval_time DATETIME COMMENT '审批时间',
admin_remark TEXT COMMENT '管理员备注',
applicant_remark TEXT COMMENT '申请人备注',
tenant_id VARCHAR(20) DEFAULT '000000' COMMENT '租户编号',
deleted CHAR(1) DEFAULT '0' COMMENT '删除标志',
create_dept BIGINT(20) COMMENT '创建部门',
create_by BIGINT(20) COMMENT '创建者',
create_time DATETIME COMMENT '创建时间',
update_by BIGINT(20) COMMENT '更新者',
update_time DATETIME COMMENT '更新时间',
PRIMARY KEY (application_id)
) ENGINE=InnoDB COMMENT='出国申请表';
```
### 字典表:sys_dict_data(新增出国相关字典)
```sql
-- 出国目的
INSERT INTO sys_dict_data VALUES (NULL, 1, '学术交流', 'academic', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '学术交流访问');
INSERT INTO sys_dict_data VALUES (NULL, 2, '培训学习', 'training', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '培训学习');
INSERT INTO sys_dict_data VALUES (NULL, 3, '国际会议', 'conference', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '参加国际会议');
INSERT INTO sys_dict_data VALUES (NULL, 4, '考察访问', 'visit', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '考察访问');
INSERT INTO sys_dict_data VALUES (NULL, 5, '其他', 'other', 'overseas_purpose', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '其他目的');
-- 经费来源
INSERT INTO sys_dict_data VALUES (NULL, 1, '学校经费', 'school', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '学校经费');
INSERT INTO sys_dict_data VALUES (NULL, 2, '个人承担', 'personal', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '个人承担');
INSERT INTO sys_dict_data VALUES (NULL, 3, '项目经费', 'project', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '项目经费');
INSERT INTO sys_dict_data VALUES (NULL, 4, '其他来源', 'other', 'budget_source', '', '', 'Y', '0', 'admin', NOW(), '', NULL, '其他来源');
-- 申请状态
INSERT INTO sys_dict_data VALUES (NULL, 1, '待审核', '0', 'overseas_status', '', 'warning', 'Y', '0', 'admin', NOW(), '', NULL, '待审核');
INSERT INTO sys_dict_data VALUES (NULL, 2, '已通过', '1', 'overseas_status', '', 'success', 'Y', '0', 'admin', NOW(), '', NULL, '已通过');
INSERT INTO sys_dict_data VALUES (NULL, 3, '已拒绝', '2', 'overseas_status', '', 'danger', 'Y', '0', 'admin', NOW(), '', NULL, '已拒绝');
INSERT INTO sys_dict_data VALUES (NULL, 4, '已取消', '3', 'overseas_status', '', 'info', 'Y', '0', 'admin', NOW(), '', NULL, '已取消');
INSERT INTO sys_dict_data VALUES (NULL, 5, '已完成', '4', 'overseas_status', '', 'primary', 'Y', '0', 'admin', NOW(), '', NULL, '已完成');
```
## 🔐 权限配置
### 菜单权限配置
```sql
-- 出国申请主菜单
INSERT INTO sys_menu VALUES (2130, '出国申请', 2000, 3, 'overseas', 'rs/overseas/index', '', '', 1, 0, 'M', '0', '0', 'rs:overseas:list', 'international', 'admin', NOW(), '', NULL, '出国申请管理');
-- 出国申请子权限
INSERT INTO sys_menu VALUES (2131, '出国申请查询', 2130, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:query', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu VALUES (2132, '出国申请新增', 2130, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:add', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu VALUES (2133, '出国申请修改', 2130, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:edit', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu VALUES (2134, '出国申请删除', 2130, 4, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:remove', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu VALUES (2135, '出国申请导出', 2130, 5, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseas:application:export', '#', 'admin', NOW(), '', NULL, '');
-- 出国审批主菜单
INSERT INTO sys_menu VALUES (2140, '出国审批', 2000, 4, 'overseasApproval', 'rs/overseasApproval/index', '', '', 1, 0, 'M', '0', '0', 'rs:overseasApproval:list', 'process', 'admin', NOW(), '', NULL, '出国审批管理');
-- 出国审批子权限
INSERT INTO sys_menu VALUES (2141, '出国审批查询', 2140, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseasApproval:query', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu VALUES (2142, '出国审批处理', 2140, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseasApproval:process', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO sys_menu VALUES (2143, '出国审批导出', 2140, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'rs:overseasApproval:export', '#', 'admin', NOW(), '', NULL, '');
```
### 角色权限分配
```sql
-- 教职工角色(出国申请权限)
INSERT INTO sys_role_menu SELECT 2, menu_id FROM sys_menu WHERE menu_id BETWEEN 2130 AND 2135;
-- 审批人员角色(出国审批权限)
INSERT INTO sys_role_menu SELECT 3, menu_id FROM sys_menu WHERE menu_id BETWEEN 2140 AND 2143;
-- 管理员角色(全部权限)
INSERT INTO sys_role_menu SELECT 1, menu_id FROM sys_menu WHERE menu_id BETWEEN 2130 AND 2143;
```
## 📱 技术实现
### 前端路由配置
```javascript
// web/src/router/index.ts
{
path: 'overseas',
component: () => import('@/views/rs/overseas/index.vue'),
name: 'OverseasManagement',
meta: { title: '出国申请', icon: 'international', permissions: ['rs:overseas:list'] }
},
{
path: 'overseasApproval',
component: () => import('@/views/rs/overseasApproval/index.vue'),
name: 'OverseasApproval',
meta: { title: '出国审批', icon: 'process', permissions: ['rs:overseasApproval:list'] }
}
```
### API接口设计
```javascript
// web/src/api/rs/overseas.ts
export interface OverseasApplicationForm {
applicationId?: string | number;
department: string;
contactPhone: string;
contactEmail: string;
purpose: string;
purposeDetail: string;
destination: string;
destinationCity: string;
startDate: string;
endDate: string;
budget: number;
budgetSource: string;
invitation?: string;
itinerary: string;
accommodationPlan?: string;
transportationPlan?: string;
applicantRemark?: string;
}
// 申请管理API
export const listOverseasApplication = (query: OverseasQuery) => {
return request({ url: '/rs/overseas/list', method: 'get', params: query });
};
export const addOverseasApplication = (data: OverseasApplicationForm) => {
return request({ url: '/rs/overseas', method: 'post', data: data });
};
// 审批管理API
export const approveOverseasApplication = (applicationId: string | number, status: string, remark?: string) => {
return request({ url: '/rs/overseasApproval/approve', method: 'post', data: { applicationId, status, remark } });
};
```
### 后端控制器
```java
// backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasController.java
@RestController
@RequestMapping("/rs/overseas")
public class RsOverseasController extends BaseController {
@SaCheckPermission("rs:overseas:application:query")
@GetMapping("/list")
public TableDataInfo<RsOverseasApplicationVo> list(RsOverseasApplicationQuery query) {
return rsOverseasService.queryPageList(query);
}
@SaCheckPermission("rs:overseas:application:add")
@PostMapping
public R<Void> add(@Validated @RequestBody RsOverseasApplicationBo bo) {
return toAjax(rsOverseasService.insertByBo(bo));
}
}
// backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasApprovalController.java
@RestController
@RequestMapping("/rs/overseasApproval")
public class RsOverseasApprovalController extends BaseController {
@SaCheckPermission("rs:overseasApproval:process")
@PostMapping("/approve")
public R<Void> approve(@RequestBody ApprovalRequest request) {
return toAjax(rsOverseasApprovalService.approve(request));
}
}
```
## 📊 业务流程
### 申请流程
```mermaid
graph TD
A[教职工提交申请] --> B[系统验证]
B --> C[部门主管审核]
C --> D{金额/天数判断}
D -->|≤1万且≤7天| E[审批完成]
D -->|≤5万且≤30天| F[分管领导审批]
D -->|>5万或>30天| G[校领导审批]
F --> H[审批完成]
G --> I[审批完成]
C --> J[拒绝]
F --> K[拒绝]
G --> L[拒绝]
```
### 状态流转
```javascript
const statusFlow = {
'0': ['1', '2', '3'], // 待审核 → 通过/拒绝/取消
'1': ['4'], // 已通过 → 已完成
'2': [], // 已拒绝 → 终态
'3': [], // 已取消 → 终态
'4': [] // 已完成 → 终态
};
```
## 🔍 验收标准
### 功能测试
1. **申请提交**:教职工能够完整填写并提交出国申请
2. **审批处理**:管理员能够查看、审批、拒绝申请
3. **权限控制**:不同角色只能访问对应功能模块
4. **状态流转**:申请状态按照业务规则正确流转
5. **数据完整**:所有申请和审批数据完整记录
### 性能要求
1. **响应时间**:页面加载时间 < 3秒
2. **并发处理**:支持100人同时使用
3. **数据准确**:审批流程无数据丢失或错误
### 用户体验
1. **界面友好**:参考培训申请的UI设计风格
2. **操作简单**:申请流程不超过5个步骤
3. **提示清晰**:表单验证和错误提示明确
## 📅 开发计划
### 第一阶段(1周):基础搭建
- 数据库表设计和创建
- 菜单权限配置
- 基础页面框架搭建
### 第二阶段(2周):核心功能开发
- 出国申请功能开发
- 出国审批功能开发
- 审批流程实现
### 第三阶段(1周):测试优化
- 功能测试和优化
- 权限测试验证
- 用户体验优化
---
**✅ 需求文档完成**
**📋 参考标准**: 基于培训申请和培训审批的设计模式
**🎯 核心特色**: 权限分离、流程规范、数据完整、用户友好
@@ -1,290 +0,0 @@
# 出国记录因私出国(境)情况回执PDF生成功能说明
## 📋 功能概述
在出国记录详情弹窗中新增"因私出国(境)情况回执"PDF下载功能,供申请人在申请通过后下载并线下填写实际出国相关情况说明。
---
## 🎯 业务需求
### 需求背景
- 申请人提交出国申请并通过审批后,需要提供一个标准化的情况回执表
- 该回执用于记录实际出国情况,包括实际行程、执纪情况等
- 部分字段(如申请行程)需要根据申请数据自动填充
### 功能位置
- **菜单路径**: 出国申请 → 出国记录选项卡
- **触发方式**: 点击列表中的"查看"按钮 → 详情弹窗底部的"下载因私出国(境)情况回执"按钮
---
## 🏗️ 技术实现
### 1. 后端实现
#### 1.1 PDF生成工具类
**文件位置**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/util/OverseasReceiptPdfUtil.java`
**核心功能**:
- 使用iText 5.5.13.3生成PDF文档
- 支持中文字体(STSong-Light)
- 严格按照附件6表格格式生成
**主要方法**:
```java
public static byte[] generateReceiptPdf(RsOverseasRecordVo record)
```
**PDF内容结构**:
1. **附件标题**: "附件 6"
2. **主标题**: "杭州第一技师学院教职工因私出国(境)情况回执"
3. **表格内容**(2列布局):
- 申请行程(自动填充)
- 温馨提醒(5条固定内容)
- 实际行程(留空,供线下填写)
- 执纪情况(留空,供线下填写)
- 有关情况说明(留空,供线下填写)
- 申请人意见(留空,供线下填写)
#### 1.2 Controller接口
**文件位置**: `backend/ruoyi-modules/ruoyi-rs/src/main/java/org/dromara/rs/controller/RsOverseasRecordController.java`
**接口定义**:
```java
@GetMapping("/downloadReceipt/{recordId}")
@SaCheckPermission("rs:overseas:query")
@Log(title = "下载出国情况回执", businessType = BusinessType.EXPORT)
public void downloadReceipt(@PathVariable Long recordId, HttpServletResponse response)
```
**接口说明**:
- **请求方式**: GET
- **路径**: `/rs/overseas/record/downloadReceipt/{recordId}`
- **权限要求**: `rs:overseas:query`
- **返回类型**: application/pdf
- **文件名格式**: `因私出国情况回执_{姓名}_{日期}.pdf`
---
### 2. 前端实现
#### 2.1 详情弹窗按钮
**文件位置**: `web/src/views/rs/overseas/index.vue`
**UI位置**:
```vue
<template #footer v-if="state.viewData && 'recordId' in state.viewData">
<div class="dialog-footer">
<el-button type="primary" @click="handleDownloadReceipt(state.viewData)" icon="Download">
下载因私出国(境)情况回执
</el-button>
<el-button @click="state.viewDialog.visible = false">关闭</el-button>
</div>
</template>
```
#### 2.2 下载方法实现
```typescript
const handleDownloadReceipt = async (row: OverseasRecordVO) => {
// 1. 获取recordId(支持记录ID或申请ID)
// 2. 调用后端API下载PDF
// 3. 解析响应头获取文件名
// 4. 创建Blob并触发浏览器下载
}
```
**技术要点**:
- 使用原生fetch API调用后端接口
- 自动携带Authorization Token和clientid
- 支持从已通过申请(无recordId)下载
- 提供下载进度提示
---
## 📄 PDF内容模板
### 申请行程(自动填充)
```
{申请人姓名}同志,申请于{出发日期}至{返回日期},赴{前往国家}旅游(探亲),在外停留{天数}天,费用自理。
```
**数据来源**:
- `applicantName`: 申请人姓名
- `actualStartDate`: 出发日期(格式:yyyy年MM月dd日)
- `actualEndDate`: 返回日期(格式:yyyy年MM月dd日)
- `destinationCountry`: 前往国家
- `exitReason`: 出境事由(旅游/探亲)
- `actualDuration`: 停留天数
### 温馨提醒(固定内容)
1. 请自觉遵守我国和当地法律法规,尊重当地民族风俗和宗教信仰,注意人身及财产安全。
2. 出国(境)期间不准为企业和个人办理经营业务;不准接受外商或住国(境)外中资机构(企业)的资助,不准在任何单位报销出国(境)费用。
3. 出国(境)时间、天数、目的地、事由等情况有变化的,须在出国(境)前向学院人事(组织)处报备。
4. 请于回国 10 日内,将出国(境)证件和本回执通过本单位组织人事(管理)部门转交学院人事(组织)处集中保管。
5. 若有违反《中国共产党纪律处分条例》和出国(境)有关文件规定的,将视情节轻重给予相应处理。
### 实际行程(留空填写)
```
本人于______年___月___日至______年___月___日,赴_______________旅游(探亲),在外停留_____天,费用(是□否□)自理。
```
### 执纪情况(留空填写)
```
本人因私出国(境)期间,(是□否□)严格遵守保密纪律、外事纪律,(是□否□)自觉遵守所在国家法律,(是□否□)严格执行国家、省、市、局和学院因私出国(境)管理有关规定,(有□无□)违规违纪行为。出国(境)证件已于___月___日 按规定上缴______统一保管。
(证件编号:_______________)
```
### 有关情况说明
```
(实际行程与申请行程有出入及其他情况的,请简要说明)
```
### 申请人意见
```
本人保证填写信息真实。
签名:
年 月 日
```
---
## 🔧 依赖配置
### Maven依赖(已配置)
```xml
<!-- iText PDF生成库 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<!-- iText亚洲字体支持包 - 支持中文 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
```
---
## ✅ 测试验证
### 测试场景
1. **正常下载**: 有recordId的出国记录
2. **已通过申请**: 无recordId,使用applicationId
3. **文件名正确性**: 包含申请人姓名和日期
4. **字段自动填充**: 申请行程信息正确显示
5. **中文支持**: PDF中文正常显示
### 测试步骤
1. 登录系统
2. 进入"出国申请"菜单
3. 切换到"出国记录"选项卡
4. 点击任一记录的"查看"按钮
5. 在弹窗底部点击"下载因私出国(境)情况回执"
6. 验证PDF下载成功
7. 打开PDF检查内容格式和字段填充
### 预期结果
- ✅ PDF成功下载到本地
- ✅ 文件名格式正确:`因私出国情况回执_姓名_20250108.pdf`
- ✅ PDF内容格式与附件6一致
- ✅ 申请行程字段自动填充正确
- ✅ 中文字体显示正常
- ✅ 表格布局清晰,边框完整
- ✅ 留空字段为空白,供线下填写
---
## 📊 数据流程
```
用户操作
↓
前端: index.vue
↓ handleDownloadReceipt()
后端: RsOverseasRecordController
↓ downloadReceipt()
Service: IRsOverseasRecordService
↓ queryById(recordId)
工具类: OverseasReceiptPdfUtil
↓ generateReceiptPdf(record)
iText PDF生成
↓
返回PDF字节流
↓
浏览器下载文件
```
---
## 🎨 UI设计
### 按钮样式
- **类型**: Primary按钮
- **图标**: Download图标
- **文本**: "下载因私出国(境)情况回执"
- **位置**: 记录详情弹窗底部,"关闭"按钮左侧
### 提示信息
- **开始下载**: "正在生成PDF,请稍候..."(info提示)
- **下载成功**: "PDF下载成功!"(success提示)
- **下载失败**: "下载失败: {错误信息}"(error提示)
---
## 📌 注意事项
### 1. 权限控制
- 使用与查看记录相同的权限:`rs:overseas:query`
- 仅在记录详情弹窗中显示下载按钮
- 申请详情弹窗不显示该按钮
### 2. 数据兼容性
- 支持已创建的出国记录(有recordId)
- 支持已通过审批的申请(无recordId,使用applicationId)
- 字段为空时显示下划线占位符
### 3. 字体支持
- 优先使用STSong-Light字体
- 备选方案:STSongStd-Light
- 如果字体加载失败,抛出ServiceException
### 4. 文件名编码
- 使用UTF-8编码
- URL编码处理中文字符
- 符合RFC 5987标准
---
## 🔄 后续优化建议
1. **批量下载**: 支持选中多条记录批量生成ZIP压缩包
2. **在线预览**: 集成PDF.js实现在线预览功能
3. **模板配置**: 将温馨提醒等固定内容配置化
4. **水印支持**: 添加学院公章或水印
5. **签名支持**: 集成电子签名功能
6. **历史记录**: 记录PDF生成和下载历史
---
## 📝 相关文档
- **需求附件**: 附件6 - 因私出国(境)情况回执(纸质表格图片)
- **API文档**: 暂无(建议补充到API文档总览)
- **测试文档**: 暂无(建议补充测试用例)
- **用户手册**: 暂无(建议补充操作指南)
---
**文档编写**: Cursor AI
**编写日期**: 2025-01-08
**版本号**: v1.0
**状态**: ✅ 已完成并测试通过
@@ -1,133 +0,0 @@
需求分析概览
### 核心业务模块
1. **招聘管理模块** - 招聘公告、职位管理
2. **应聘者管理模块** - 注册登录、简历管理、职位申请
3. **培训管理模块** - 培训申请、审核、记录管理
4. **出国申请模块** - 出国申请、审核流程
5. **用户认证模块** - 多种登录方式、密码重置
## 👥 角色体系设计
基于您的需求,系统需要以下角色:
### 1. **应聘者角色** (APPLICANT)
- 账号注册、登录
- 简历填写和管理
- 职位申请和跟踪
- 个人信息维护
### 2. **HR管理员角色** (HR_ADMIN)
- 发布招聘公告
- 管理招聘职位
- 审核应聘者申请
- 简历筛选和评估
### 3. **教职工角色** (TEACHER)
- 培训申请和查询
- 出国申请提交
- 个人培训记录查看
- 证书上传管理
### 4. **培训管理员角色** (TRAINING_ADMIN)
- 培训课程管理
- 培训申请审核
- 培训记录登记
- 培训证书管理
### 5. **审批领导角色** (APPROVER)
- 培训申请审批
- 出国申请审批
- 在线签字审批
- 审批流程管理
### 6. **系统管理员角色** (SYSTEM_ADMIN)
- 用户权限管理
- 系统配置管理
- 数据统计分析
## 📋 需要提供的材料清单
为了完整开发这个人事管理系统,我需要您提供以下材料:
### 🔴 必需材料
#### 1. **业务流程图**
- 招聘流程:发布公告 → 职位申请 → 简历筛选 → 面试安排 → 录用决定
- 培训申请流程:申请提交 → 部门审核 → 领导审批 → 培训执行 → 记录归档
- 出国申请流程:申请提交 → 部门审核 → 领导审批 → 出国执行
#### 2. **详细字段需求**
- **招聘职位**:职位名称、部门、岗位要求、工作地点等
- **简历信息**:基本信息、教育背景、工作经历、技能证书、附件等
- **培训记录**:培训名称、培训时间、学时、培训类别、证书等
- **出国申请**:出国目的、目的地、时间、预算、行程安排等
#### 3. **审批规则**
- 培训申请的审批层级(部门主管 → 分管领导 → 校领导)
- 出国申请的审批层级和条件
- 不同金额/时长的审批权限
#### 4. **权限矩阵**
- 各角色的功能权限明细
- 数据查看权限范围
- 操作权限限制
### 🟡 建议提供的材料
#### 5. **UI/UX设计要求**
- 简历填写页面的布局要求
- 审批流程的界面展示
- 移动端适配要求
#### 6. **集成需求**
- 是否需要与现有OA系统集成
- 短信/邮件服务商选择
- 文件存储方案
#### 7. **报表需求**
- 招聘统计报表
- 培训统计分析
- 人事数据报表
## 🚀 开发规划建议
### 第一阶段:基础模块开发 (2-3周)
1. 用户认证模块(多种登录方式)
2. 角色权限管理
3. 基础数据字典
### 第二阶段:招聘管理模块 (2-3周)
1. 招聘公告管理
2. 职位管理
3. 应聘者注册和简历管理
### 第三阶段:培训管理模块 (2-3周)
1. 培训申请和审核
2. 培训记录管理
3. 工作流集成
### 第四阶段:出国申请模块 (1-2周)
1. 出国申请流程
2. 审批管理
3. 记录查询
### 第五阶段:优化完善 (1周)
1. 报表统计
2. 移动端适配
3. 性能优化
## 📝 下一步行动
请您优先提供以下关键材料,我就可以开始详细的技术设计:
1. **详细的业务流程图** - 特别是审批流程的具体步骤
2. **各模块的详细字段清单** - 包括必填字段、字段类型、校验规则
3. **角色权限矩阵** - 明确各角色可以执行的操作
4. **审批规则说明** - 不同类型申请的审批层级和条件
有了这些材料,我将按照项目规范为您生成:
- 完整的数据库设计文档
- 标准的API接口文档
- 详细的开发计划
- 规范的代码模板
@@ -1,408 +0,0 @@
# 培训管理模块开发需求
## 一、需求概述
基于人事管理系统现有招聘管理功能,开发培训管理模块,实现培训申请、审核、培训记录管理等功能。培训管理模块主要支持教职工提交培训申请,培训管理员审核申请和管理培训课程,审批领导进行多级审批,并提供培训记录统计功能。
## 二、功能模块设计
### 1. 培训课程管理
#### 功能描述
- 培训课程的CRUD操作
- 课程分类管理
- 课程状态管理(待开课、进行中、已结束)
- 课程名额限制和报名统计
#### 数据表设计
```sql
CREATE TABLE `rs_training_course` (
`course_id` bigint(20) NOT NULL COMMENT '课程ID',
`title` varchar(100) NOT NULL COMMENT '课程名称',
`category` varchar(50) NOT NULL COMMENT '课程类别',
`description` text COMMENT '课程描述',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`duration` int(11) NOT NULL COMMENT '学时数',
`location` varchar(200) COMMENT '培训地点',
`max_participants` int(11) DEFAULT 0 COMMENT '最大参与人数',
`current_participants` int(11) DEFAULT 0 COMMENT '当前报名人数',
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`course_id`)
) ENGINE=InnoDB COMMENT='培训课程表';
```
### 2. 培训申请管理
#### 功能描述
- 教职工提交培训申请
- 培训申请列表展示和筛选
- 申请详情查看
- 申请状态管理
- 多级审批流程(部门主管→分管领导→校领导)
#### 数据表设计
```sql
CREATE TABLE `rs_training_application` (
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`course_id` bigint(20) NOT NULL COMMENT '课程ID',
`user_id` bigint(20) NOT NULL COMMENT '申请人ID',
`user_name` varchar(50) NOT NULL COMMENT '申请人姓名',
`department` varchar(50) NOT NULL COMMENT '所属部门',
`position` varchar(50) COMMENT '职位',
`reason` text COMMENT '申请理由',
`status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝 3进行中 4已完成)',
`apply_time` datetime DEFAULT NULL COMMENT '申请时间',
`current_approver` varchar(50) DEFAULT NULL COMMENT '当前审批人',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`application_id`)
) ENGINE=InnoDB COMMENT='培训申请表';
```
### 3. 培训审批流程
#### 功能描述
- 审批流程定义和配置
- 审批记录跟踪
- 审批意见填写
- 审批通知(短信/邮件)
#### 数据表设计
```sql
CREATE TABLE `rs_training_approval` (
`approval_id` bigint(20) NOT NULL COMMENT '审批ID',
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`approver_id` bigint(20) NOT NULL COMMENT '审批人ID',
`approver_name` varchar(50) NOT NULL COMMENT '审批人姓名',
`approval_level` int(11) NOT NULL COMMENT '审批级别(1部门主管 2分管领导 3校领导)',
`status` char(1) DEFAULT '0' COMMENT '状态(0待审核 1已通过 2已拒绝)',
`opinion` varchar(500) DEFAULT NULL COMMENT '审批意见',
`approval_time` datetime DEFAULT NULL COMMENT '审批时间',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`approval_id`)
) ENGINE=InnoDB COMMENT='培训审批表';
```
### 4. 培训记录管理
#### 功能描述
- 培训记录登记
- 培训证书上传和管理
- 个人培训记录查询
- 培训统计报表
#### 数据表设计
```sql
CREATE TABLE `rs_training_record` (
`record_id` bigint(20) NOT NULL COMMENT '记录ID',
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`course_id` bigint(20) NOT NULL COMMENT '课程ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) NOT NULL COMMENT '用户姓名',
`completion_status` char(1) DEFAULT '0' COMMENT '完成状态(0进行中 1已完成 2未完成)',
`completion_time` datetime DEFAULT NULL COMMENT '完成时间',
`score` decimal(5,2) DEFAULT NULL COMMENT '成绩',
`certificate_path` varchar(255) DEFAULT NULL COMMENT '证书路径',
`certificate_number` varchar(50) DEFAULT NULL COMMENT '证书编号',
`feedback` text COMMENT '培训反馈',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`record_id`)
) ENGINE=InnoDB COMMENT='培训记录表';
```
## 三、API接口设计
### 1. 培训课程管理API
```
1. 查询培训课程列表
GET /training/course/list
2. 获取培训课程详情
GET /training/course/{courseId}
3. 新增培训课程
POST /training/course
4. 修改培训课程
PUT /training/course
5. 删除培训课程
DELETE /training/course/{courseIds}
6. 切换培训课程状态
PUT /training/course/toggle/{courseId}
7. 导出培训课程
GET /training/course/export
```
### 2. 培训申请管理API
```
1. 查询培训申请列表
GET /training/application/list
2. 获取培训申请详情
GET /training/application/{applicationId}
3. 提交培训申请
POST /training/application
4. 修改培训申请
PUT /training/application
5. 取消培训申请
PUT /training/application/cancel/{applicationId}
6. 导出培训申请
GET /training/application/export
7. 获取我的培训申请列表
GET /training/application/my
```
### 3. 培训审批API
```
1. 查询待我审批的申请列表
GET /training/approval/pending
2. 审批培训申请
PUT /training/approval/process/{applicationId}/{status}
3. 获取申请审批历史
GET /training/approval/history/{applicationId}
4. 批量审批
POST /training/approval/batch
```
### 4. 培训记录API
```
1. 查询培训记录列表
GET /training/record/list
2. 获取培训记录详情
GET /training/record/{recordId}
3. 登记培训记录
POST /training/record
4. 修改培训记录
PUT /training/record
5. 上传培训证书
POST /training/record/certificate/{recordId}
6. 查询我的培训记录
GET /training/record/my
7. 导出培训记录
GET /training/record/export
8. 获取培训统计数据
GET /training/record/statistics
```
## 四、前端页面设计
### 1. 培训课程管理页面 ⚠️【已废弃 - 2025-01-03】
**重要说明**:培训课程模块已废弃,课程信息现在直接存储在 `rs_training_application` 表中,通过培训申请功能管理。
~~- **页面路径**:/rs/training/course~~
~~- **功能组件**:~~
~~- 搜索表单(课程名称、类别、状态)~~
~~- 操作按钮(新增、修改、删除、导出)~~
~~- 数据表格(课程信息展示)~~
~~- 表单对话框(新增/修改课程信息)~~
### 2. 培训申请管理页面
- **页面路径**:/rs/training/application
- **功能组件**:
- 搜索表单(课程名称、申请人、状态)
- 操作按钮(导出)
- 数据表格(申请信息展示)
- 申请详情对话框
- 审批操作面板
### 3. 我的培训页面
- **页面路径**:/rs/training/my
- **功能组件**:
- 培训申请选项卡
- 申请表单
- 我的申请列表
- 培训记录选项卡
- 我的培训记录
- 证书查看与下载
### 4. 培训统计报表页面
- **页面路径**:/rs/training/statistics
- **功能组件**:
- 统计图表(按部门、课程类型等维度)
- 数据筛选器
- 导出功能
## 五、权限设计
### 1. 权限项定义
```
# 培训课程管理权限
~~- rs:training:course:list 查看培训课程列表~~ 【已废弃】
~~- rs:training:course:query 查询培训课程详情~~ 【已废弃】
~~- rs:training:course:add 新增培训课程~~ 【已废弃】
~~- rs:training:course:edit 修改培训课程~~ 【已废弃】
~~- rs:training:course:remove 删除培训课程~~ 【已废弃】
~~- rs:training:course:export 导出培训课程~~ 【已废弃】
# 培训申请管理权限
- rs:training:application:list 查看培训申请列表
- rs:training:application:query 查询培训申请详情
- rs:training:application:add 提交培训申请
- rs:training:application:edit 修改培训申请
- rs:training:application:export 导出培训申请
# 培训审批权限
- rs:training:approval:list 查看审批列表
- rs:training:approval:process 处理培训审批
# 培训记录管理权限
- rs:trainingrecord:list 查看培训记录列表
- rs:trainingrecord:query 查询培训记录详情
- rs:trainingrecord:add 新增培训记录
- rs:trainingrecord:edit 修改培训记录
- rs:trainingrecord:export 导出培训记录
```
### 2. 角色权限分配
```
# 教职工角色 (TEACHER)
- rs:training:course:list
- rs:training:course:query
- rs:training:application:add
- rs:training:application:query
- rs:trainingrecord:query (个人)
# 培训管理员角色 (TRAINING_ADMIN)
- rs:training:course:*
- rs:training:application:list
- rs:training:application:query
- rs:training:application:export
- rs:trainingrecord:*
# 审批领导角色 (APPROVER)
- rs:training:course:list
- rs:training:course:query
- rs:training:application:list
- rs:training:application:query
- rs:training:approval:*
```
## 六、开发计划
### 第一阶段:数据库设计与基础架构 (5天)
- 创建数据库表
- 生成基础实体类
- 搭建模块结构
### 第二阶段:培训课程管理功能 (5天)
- 后端API开发
- 前端页面开发
- 单元测试
### 第三阶段:培训申请功能 (7天)
- 后端API开发
- 前端页面开发
- 审批流程设计
- 单元测试
### 第四阶段:培训记录管理功能 (5天)
- 后端API开发
- 前端页面开发
- 证书上传功能
- 单元测试
### 第五阶段:统计报表与优化 (3天)
- 培训统计报表
- 权限控制完善
- 性能优化
- 集成测试
## 七、技术要点与注意事项
1. **工作流集成**
- 使用Warm-Flow工作流引擎实现多级审批流程
- 遵循工作流开发规范,确保VO类包含基础字段
2. **前后端字段一致性**
- 严格遵循命名规范,确保前后端字段名称一致
- 注意类型一致性,特别是日期、枚举等类型
3. **文件上传处理**
- 培训证书上传需支持多种格式(PDF, JPG等)
- 实现文件预览功能
4. **权限控制**
- 实现数据权限,教职工只能查看自己的培训记录
- 审批权限按角色和部门进行控制
5. **文档同步更新**
- 确保API变更同步更新到API文档
- 所有新增功能需要同步更新变更记录
## 八、验收标准
1. **功能完整性**
- 培训管理各模块功能齐全
- 审批流程运行正常
- 各角色权限控制正确
2. **界面体验**
- 操作流程简洁明了
- 页面响应迅速
- 数据展示清晰
3. **性能要求**
- 列表页面加载时间<2秒
- 表单提交响应时间<1秒
- 导出功能支持大数据量
4. **兼容性**
- 支持主流浏览器
- 移动端适配良好
5. **安全性**
- 数据传输加密
- 防SQL注入
- 敏感信息脱敏
@@ -1,93 +0,0 @@
第一步:生成招聘页面原型图
cursor
根据以下需求生成招聘模块的Figma原型图:
1. 页面组成:
- 招聘公告列表页(带搜索/筛选)
- 职位详情页(申请按钮)
- 简历填写表单(分步骤)
- HR管理后台(职位发布表单)
2. 核心功能:
- 应聘者能浏览公开职位
- HR能发布新职位(含工作地点/要求等字段)
- 响应式布局(支持移动端)
3. 数据字段参考:
- 职位:title, department, location, description, requirements
- 简历:personal_info, education[], work_experience[], skills
4. 角色权限:
- 应聘者:仅查看/申请
- HR:增删改职位
第二步:生成招聘模块表结构
cursor
生成符合MySQL规范的SQL建表语句,包含以下核心表:
1. `jobs` 职位表:
- id (PK)
- title VARCHAR(100)
- department ENUM('教学','行政','科研')
- salary_range VARCHAR(50)
- location VARCHAR(100)
- description TEXT
- requirements TEXT
- is_active BOOLEAN (是否在招)
- created_at TIMESTAMP
- created_by (FK to users)
2. `applicants` 应聘者表:
- user_id (PK, FK to users)
- resume_file_path VARCHAR(255)
- current_status ENUM('active','hired','rejected')
3. `applications` 申请记录表:
- id (PK)
- job_id (FK)
- applicant_id (FK)
- apply_time TIMESTAMP
- status ENUM('pending','interviewed','offered')
4. 补充关联表:
- resumes (存储结构化简历数据)
- application_attachments (附件)
第三步:初始化数据库
cursor
基于上述表结构执行以下操作:
1. 创建名为`recruitment_system`的MySQL数据库
2. 生成各表的DDL语句(包含外键约束)
3. 为每个表添加基础索引:
- jobs表的department/is_active字段
- applications表的job_id/applicant_id字段
4. 插入测试数据:
- 5个示例职位(3个教学岗、2个行政岗)
- 10条模拟应聘者记录
- 每个职位关联2-3条申请记录
第四步:前后端开发指令
cursor
使用MERN栈开发招聘模块,分两部分实现:
【前端部分】
1. 创建React页面:
- /jobs 职位列表页(带Ant Design表格)
- /jobs/:id 职位详情页
- /apply 简历表单(分教育/工作经历多步骤)
2. 主要功能组件:
- 职位搜索过滤器(部门/工作地点)
- 简历PDF上传组件
- 申请状态跟踪面板
【后端部分】
1. 创建Express路由:
- GET /api/jobs (支持分页查询)
- POST /api/jobs (HR权限校验)
- POST /api/applications
2. 核心业务逻辑:
- 职位发布时自动生成公告
- 申请时发送邮件通知HR
- 简历文件存储到AWS S3
3. 权限控制中间件:
- 应聘者只能访问GET /jobs
- HR角色需要JWT校验
+4 -3
View File
@@ -81,7 +81,7 @@
<el-table-column label="主办单位" align="center" prop="organizerUnit" :show-overflow-tooltip="true" />
<el-table-column label="培训地点" align="center" prop="location" />
<el-table-column label="培训类型" align="center" prop="trainingType" />
<el-table-column label="申请人" align="center" prop="userName" />
<el-table-column label="申请人" align="center" prop="nickName" />
<el-table-column label="同行人" align="center" prop="companions" width="150">
<template #default="scope">
<span>{{ getCompanionNames(scope.row.companions) || '无' }}</span>
@@ -262,7 +262,7 @@
<el-dialog title="申请详情" v-model="applicationDetailOpen" width="800px" append-to-body>
<el-descriptions :column="2" border>
<!-- 申请人基本信息 -->
<el-descriptions-item label="申请人">{{ applicationDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ applicationDetail.nickName || applicationDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="所在部门(系部)">{{ applicationDetail.department }}</el-descriptions-item>
<el-descriptions-item label="现职称">{{ applicationDetail.currentTitle || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="现岗位">{{ applicationDetail.currentPosition || '暂无' }}</el-descriptions-item>
@@ -392,7 +392,7 @@
<el-dialog title="审批记录详情" v-model="approvalRecordDetailOpen" width="800px" append-to-body>
<el-descriptions :column="2" border>
<!-- 申请人基本信息 -->
<el-descriptions-item label="申请人">{{ approvalRecordDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ approvalRecordDetail.nickName || approvalRecordDetail.userName }}</el-descriptions-item>
<el-descriptions-item label="所在部门(系部)">{{ approvalRecordDetail.department }}</el-descriptions-item>
<el-descriptions-item label="现职称">{{ approvalRecordDetail.currentTitle || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="现岗位">{{ approvalRecordDetail.currentPosition || '暂无' }}</el-descriptions-item>
@@ -985,6 +985,7 @@ const handleViewApprovalRecord = async (row: any) => {
courseName: applicationData.courseName,
userId: applicationData.userId,
userName: applicationData.userName,
nickName: applicationData.nickName, // 用户昵称(真实姓名)
department: applicationData.department, // 从申请数据获取部门
currentTitle: applicationData.currentTitle, // 现职称
currentPosition: applicationData.currentPosition, // 现岗位