提交代码

This commit is contained in:
zlwu committed 2025-11-28 20:01:32 +08:00
1 parent 4e34c7eb36
commit 375a2c8df7
54 files changed
+4631 -4352

No files matched your search

@@ -1,2 +0,0 @@
# 人事管理模块 - 控制器层
# 此目录用于存放REST API控制器类
@@ -24,6 +24,7 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
@@ -97,6 +98,8 @@ public class RsOverseasApplicationController extends BaseController {
/**
* 查询当前用户的待审批申请列表(五级审批模式)
*
* @deprecated 已废弃,请使用 pendingPageList 方法(支持分页和搜索)
*/
@SaCheckPermission("rs:overseasapproval:list")
@GetMapping("/pending/list")
@@ -105,6 +108,30 @@ public class RsOverseasApplicationController extends BaseController {
return R.ok(list);
}
/**
* 查询当前用户的待审批申请分页列表(根据角色过滤,支持分页和搜索)
*
* 🔴 核心逻辑:
* 1. 根据用户角色确定审批级别列表
* 2. 部门负责人(rs_bmfzr)只能看到本部门的申请
* 3. 其他角色可以看到全校的申请
* 4. 支持分页和搜索(前往国家、出境事由)
* 5. 只返回status='0'(待审核)的申请
* 6. 按创建时间倒序排列
*
* 角色映射:
* - rs_bmfzr → approval_level='1' + 部门过滤
* - hr_dept_role → approval_level='2'
* - discipline_committee_role → approval_level='3'
* - college_leader_role → approval_level='4'
* - party_secretary_role → approval_level='5'
*/
@SaCheckPermission("rs:overseasapproval:list")
@GetMapping("/pending/page")
public TableDataInfo<RsOverseasApplicationVo> pendingPageList(RsOverseasApplicationQuery query) {
return applicationService.queryPendingPageListByRole(query);
}
// ========================================
// 五级审批模式说明
//
@@ -321,7 +348,7 @@ public class RsOverseasApplicationController extends BaseController {
@PostMapping("/approve")
public R<Void> approve(@RequestBody ApprovalRequest request) {
Long approverId = LoginHelper.getUserId();
String approverName = LoginHelper.getUsername();
String approverName = StringUtils.defaultIfBlank(LoginHelper.getNickname(), LoginHelper.getUsername());
// 🔴 标准工作流模式:支持任务ID参数
// 如果传入了taskId,将通过工作流引擎进行审批
@@ -488,12 +515,6 @@ public class RsOverseasApplicationController extends BaseController {
public R<Map<String, Object>> uploadStampedFile(@RequestParam("file") MultipartFile file,
@RequestParam("applicationId") Long applicationId) {
try {
// 验证文件格式
String contentType = file.getContentType();
if (!isValidFileType(contentType)) {
return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件");
}
// 验证文件大小(10MB)
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
@@ -561,12 +582,6 @@ public class RsOverseasApplicationController extends BaseController {
public R<Map<String, Object>> uploadPartyRetentionFile(@RequestParam("file") MultipartFile file,
@RequestParam("applicationId") Long applicationId) {
try {
// 验证文件格式
String contentType = file.getContentType();
if (!isValidFileType(contentType)) {
return R.fail("不支持的文件格式,请上传PDF、JPG或PNG文件");
}
// 验证文件大小(10MB)
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
@@ -583,7 +598,7 @@ public class RsOverseasApplicationController extends BaseController {
ossResult.getOriginalName(), // 原始文件名
new Date(), // 上传时间
"1", // 上传状态:已上传
ossResult.getOssId() // OSS文件ID
String.valueOf(ossResult.getOssId()) // OSS文件ID - 转换为字符串
);
// 返回结果
@@ -616,25 +631,24 @@ public class RsOverseasApplicationController extends BaseController {
return;
}
// 处理多个OSS ID的情况(逗号分隔),取第一个ID进行下载
String ossIdStr = application.getPartyRetentionFileOssId();
Long ossId;
if (ossIdStr.contains(",")) {
// 如果包含逗号,取第一个ID
ossId = Long.parseLong(ossIdStr.split(",")[0].trim());
} else {
// 单个ID直接解析
ossId = Long.parseLong(ossIdStr);
}
// 使用系统OSS服务进行安全下载
ossService.secureDownload(application.getPartyRetentionFileOssId(), response);
ossService.secureDownload(ossId, response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 验证文件类型
*/
private boolean isValidFileType(String contentType) {
if (contentType == null)
return false;
return contentType.equals("application/pdf") ||
contentType.equals("image/jpeg") ||
contentType.equals("image/jpg") ||
contentType.equals("image/png");
}
/**
* 获取文件扩展名
*/
@@ -62,17 +62,13 @@ public class RsOverseasItineraryChangeController extends BaseController {
}
/**
* 🔴 标准化改造:删除自定义待审批查询接口
*
* 原因:出国行程变更工作流应使用标准工作流模式
* 用户应通过工作流API pageByTaskWait() 获取待办任务
* 而不是调用自定义的业务API
* 查询待审批的行程变更申请列表(status=0)
*/
@SaCheckPermission("rs:itineraryChange:approve")
@GetMapping("/pending/list")
public R<String> pendingList(RsOverseasItineraryChangeQuery query) {
// 提示用户使用标准工作流API
return R.fail("请使用标准工作流待办页面查看审批任务。访问:工作流 > 我的任务 > 待办任务");
public TableDataInfo<RsOverseasItineraryChangeVo> pendingList(RsOverseasItineraryChangeQuery query) {
query.setTenantId(LoginHelper.getTenantId());
return changeService.queryPendingList(query);
}
/**
@@ -167,7 +163,7 @@ public class RsOverseasItineraryChangeController extends BaseController {
/**
* 导出行程变更申请列表
*/
@SaCheckPermission("rs:itineraryChange:export")
// @SaCheckPermission("rs:itineraryChange:export")
@Log(title = "行程变更申请", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(RsOverseasItineraryChangeQuery query, HttpServletResponse response) {
@@ -179,7 +175,7 @@ public class RsOverseasItineraryChangeController extends BaseController {
/**
* 查询某个出国记录的所有变更历史
*/
@SaCheckPermission("rs:itineraryChange:query")
// @SaCheckPermission("rs:itineraryChange:query")
@GetMapping("/record/{recordId}/history")
public R<List<RsOverseasItineraryChangeVo>> getChangeHistory(@PathVariable Long recordId) {
return R.ok(changeService.queryByRecordId(recordId));
@@ -32,7 +32,7 @@ import org.dromara.rs.service.IRsOverseasRegistrationService;
/**
* 出国登记Controller
*
*
* @author weizzzz
* @date 2025-09-30
*/
@@ -47,7 +47,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 查询出国登记列表
*/
@SaCheckPermission("rs:overseas:registration:list")
// @SaCheckPermission("rs:overseasregistration:list")
@GetMapping("/list")
public TableDataInfo<RsOverseasRegistrationVo> list(RsOverseasRegistrationQuery query) {
return registrationService.queryPageList(query);
@@ -56,7 +56,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 查询我的出国登记列表
*/
@SaCheckPermission("rs:overseas:registration:list")
// @SaCheckPermission("rs:overseasregistration:list")
@GetMapping("/my/list")
public TableDataInfo<RsOverseasRegistrationVo> myList(RsOverseasRegistrationQuery query) {
return registrationService.queryMyRegistrationList(query);
@@ -65,7 +65,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 导出出国登记列表
*/
@SaCheckPermission("rs:overseas:registration:export")
// @SaCheckPermission("rs:overseasregistration:export")
@Log(title = "出国登记", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(RsOverseasRegistrationQuery query, HttpServletResponse response) {
@@ -76,27 +76,26 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 获取出国登记详细信息
*/
@SaCheckPermission("rs:overseas:registration:query")
// @SaCheckPermission("rs:overseasregistration:query")
@GetMapping("/{registrationId}")
public R<RsOverseasRegistrationVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<RsOverseasRegistrationVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return R.ok(registrationService.queryById(registrationId));
}
/**
* 根据申请ID获取出国登记信息
*/
@SaCheckPermission("rs:overseas:registration:query")
// @SaCheckPermission("rs:overseasregistration:query")
@GetMapping("/application/{applicationId}")
public R<RsOverseasRegistrationVo> getByApplicationId(@NotNull(message = "申请ID不能为空")
@PathVariable Long applicationId) {
public R<RsOverseasRegistrationVo> getByApplicationId(
@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) {
return R.ok(registrationService.queryByApplicationId(applicationId));
}
/**
* 新增出国登记
*/
@SaCheckPermission("rs:overseas:registration:add")
// @SaCheckPermission("rs:overseasregistration:add")
@Log(title = "出国登记", businessType = BusinessType.INSERT)
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody RsOverseasRegistrationBo bo) {
@@ -106,7 +105,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 新增并提交出国登记
*/
@SaCheckPermission("rs:overseas:registration:add")
// @SaCheckPermission("rs:overseasregistration:add")
@Log(title = "出国登记", businessType = BusinessType.INSERT)
@PostMapping("/addAndSubmit")
public R<Void> addAndSubmit(@Validated(AddGroup.class) @RequestBody RsOverseasRegistrationBo bo) {
@@ -116,7 +115,7 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 修改出国登记
*/
@SaCheckPermission("rs:overseas:registration:edit")
// @SaCheckPermission("rs:overseasregistration:edit")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody RsOverseasRegistrationBo bo) {
@@ -126,67 +125,61 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 删除出国登记
*/
@SaCheckPermission("rs:overseas:registration:remove")
// @SaCheckPermission("rs:overseasregistration:remove")
@Log(title = "出国登记", businessType = BusinessType.DELETE)
@DeleteMapping("/{registrationIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] registrationIds) {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] registrationIds) {
return toAjax(registrationService.deleteWithValidByIds(List.of(registrationIds), true));
}
/**
* 提交出国登记
*/
@SaCheckPermission("rs:overseas:registration:submit")
// @SaCheckPermission("rs:overseasregistration:submit")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/submit/{registrationId}")
public R<Void> submit(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<Void> submit(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return toAjax(registrationService.submitRegistration(registrationId));
}
/**
* 确认出国登记
*/
@SaCheckPermission("rs:overseas:registration:confirm")
// @SaCheckPermission("rs:overseasregistration:confirm")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/confirm/{registrationId}")
public R<Void> confirm(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<Void> confirm(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return toAjax(registrationService.confirmRegistration(registrationId));
}
/**
* 撤回出国登记
*/
@SaCheckPermission("rs:overseas:registration:withdraw")
// @SaCheckPermission("rs:overseasregistration:withdraw")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/withdraw/{registrationId}")
public R<Void> withdraw(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId) {
public R<Void> withdraw(@NotNull(message = "主键不能为空") @PathVariable Long registrationId) {
return toAjax(registrationService.withdrawRegistration(registrationId));
}
/**
* 基于出国申请创建登记记录
*/
@SaCheckPermission("rs:overseas:registration:create")
// @SaCheckPermission("rs:overseasregistration:create")
@Log(title = "出国登记", businessType = BusinessType.INSERT)
@PostMapping("/create/{applicationId}")
public R<Void> createFromApplication(@NotNull(message = "申请ID不能为空")
@PathVariable Long applicationId) {
public R<Void> createFromApplication(@NotNull(message = "申请ID不能为空") @PathVariable Long applicationId) {
return toAjax(registrationService.createFromApplication(applicationId));
}
/**
* 批准出国登记
*/
@SaCheckPermission("rs:registration:approve")
// @SaCheckPermission("rs:registration:approve")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/approve/{registrationId}")
public R<Void> approve(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
public R<Void> approve(@NotNull(message = "主键不能为空") @PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
String approveNotes = (bo != null && bo.getRemarks() != null) ? bo.getRemarks() : "同意";
return toAjax(registrationService.approveRegistration(registrationId, approveNotes));
}
@@ -194,13 +187,12 @@ public class RsOverseasRegistrationController extends BaseController {
/**
* 拒绝出国登记
*/
@SaCheckPermission("rs:registration:reject")
// @SaCheckPermission("rs:registration:reject")
@Log(title = "出国登记", businessType = BusinessType.UPDATE)
@PutMapping("/reject/{registrationId}")
public R<Void> reject(@NotNull(message = "主键不能为空")
@PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
public R<Void> reject(@NotNull(message = "主键不能为空") @PathVariable Long registrationId,
@RequestBody(required = false) RsOverseasRegistrationBo bo) {
String rejectReason = (bo != null && bo.getRemarks() != null) ? bo.getRemarks() : "不同意";
return toAjax(registrationService.rejectRegistration(registrationId, rejectReason));
}
}
}
@@ -384,10 +384,10 @@ public class RsOverseasApplication extends BaseEntity {
private String partyRetentionUploadStatus;
/**
* 党籍保留附件OSS ID(用于安全下载)
* 党籍保留附件OSS ID(用于安全下载,支持多个ID用逗号分隔)
*/
@ExcelIgnore
private Long partyRetentionFileOssId;
private String partyRetentionFileOssId;
/**
* 租户编号
@@ -29,7 +29,7 @@ public class RsOverseasRegistration extends BaseEntity {
/**
* 登记ID
*/
@TableId(value = "registration_id", type = IdType.AUTO)
@TableId(value = "registration_id", type = IdType.ASSIGN_ID)
@ExcelProperty(value = "登记ID")
private Long registrationId;
@@ -317,6 +317,32 @@ public class RsOverseasApplicationBo extends BaseEntity {
*/
private String stampedFileUploadStatus;
/**
* 党籍保留附件文件路径
*/
private String partyRetentionFilePath;
/**
* 党籍保留附件文件名
*/
private String partyRetentionFileName;
/**
* 党籍保留附件上传时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date partyRetentionUploadTime;
/**
* 党籍保留附件上传状态:0-未上传,1-已上传
*/
private String partyRetentionUploadStatus;
/**
* 党籍保留附件OSS ID(支持多个ID用逗号分隔)
*/
private String partyRetentionFileOssId;
/**
* 租户ID
*/
@@ -365,9 +365,9 @@ public class RsOverseasApplicationVo implements Serializable {
private String partyRetentionUploadStatus;
/**
* 党籍保留附件OSS ID(用于安全下载)
* 党籍保留附件OSS ID(用于安全下载,支持多个ID用逗号分隔)
*/
private Long partyRetentionFileOssId;
private String partyRetentionFileOssId;
/**
* 租户编号
@@ -196,6 +196,11 @@ public class RsOverseasApprovalMonitorVo implements Serializable {
@ExcelProperty(value = "应急联系人及电话")
private String emergencyContact;
/**
* 申请人手写签名(base64)
*/
private String signature;
// ==================== 审批状态信息 ====================
/**
@@ -144,4 +144,23 @@ public interface RsOverseasApplicationMapper extends BaseMapperPlus<RsOverseasAp
* @return 待审批申请列表
*/
List<RsOverseasApplication> selectPendingApplications(List<String> approvalLevels, Long deptId);
/**
* 🔴 新增:根据角色查询待审批申请列表(支持分页和搜索)
*
* 查询逻辑:
* 1. 根据用户角色确定审批级别列表
* 2. 关联 sys_user 表获取申请人的部门信息
* 3. 按审批级别过滤(approvalLevel IN (...))
* 4. 按状态过滤(status = '0' 待审核)
* 5. 按部门过滤(部门负责人:u.dept_id = 当前用户部门ID)
* 6. 按搜索条件过滤(前往国家、出境事由)
* 7. 按创建时间倒序排列
*
* @param query 查询条件(包含分页、搜索条件)
* @param approvalLevels 审批级别列表,例如:["1", "2"]
* @param deptId 部门ID(部门负责人需要),为null则不过滤部门
* @return 待审批申请分页列表
*/
List<RsOverseasApplicationVo> selectPendingApplicationsByRole(RsOverseasApplicationQuery query, List<String> approvalLevels, Long deptId);
}
@@ -48,6 +48,14 @@ public interface IRsOverseasApplicationService {
*/
List<RsOverseasApplicationVo> queryPendingByCurrentUser();
/**
* 查询当前用户的待审批申请分页列表(根据角色过滤)
*
* @param query 查询条件(包含分页、搜索条件)
* @return 待审批申请分页集合
*/
TableDataInfo<RsOverseasApplicationVo> queryPendingPageListByRole(RsOverseasApplicationQuery query);
/**
* 查询出国申请详细信息
*
@@ -207,7 +215,7 @@ public interface IRsOverseasApplicationService {
* @return 是否成功
*/
Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId);
java.util.Date uploadTime, String uploadStatus, String ossId);
/**
* 校验出国登记前置条件
@@ -51,7 +51,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
/**
* 出国申请Service业务层处理
*
*
* @author weizzzz
* @date 2025-07-28
*/
@@ -232,14 +232,14 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 查询当前用户的待审批申请列表(动态权限方案 + 部门过滤)
*
*
* 🆕 核心改进:
* 1. 不再依赖 current_approver_id 字段(该字段仅作为冗余展示用)
* 2. 动态查询用户的审批权限,转换为审批级别
* 3. 根据审批级别和状态查询待审批申请
* 4. 🔴 部门负责人只能看到本部门的申请
* 5. 支持人员岗位变动:权限调整后立即生效
*
*
* 查询逻辑:
* - 用户有 rs:overseas:approve1 权限(部门负责人)→ 查询本部门员工的、approval_level='1' 且
* status='0' 的申请
@@ -307,6 +307,156 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
return result;
}
/**
* 查询当前用户的待审批申请分页列表(根据角色过滤)
*
* 🔴 核心逻辑:
* 1. 根据用户角色确定审批级别列表
* 2. 部门负责人(rs_bmfzr)只能看到本部门的申请
* 3. 其他角色可以看到全校的申请
* 4. 支持分页和搜索(前往国家、出境事由)
* 5. 按创建时间倒序排列
* 6. 🔴 修复:当用户同时拥有多个角色时,分别查询后合并去重
*
* 角色映射:
* - rs_bmfzr → approval_level='1' + 部门过滤
* - hr_dept_role → approval_level='2'
* - discipline_committee_role → approval_level='3'
* - college_leader_role → approval_level='4'
* - party_secretary_role → approval_level='5'
*/
@Override
public TableDataInfo<RsOverseasApplicationVo> queryPendingPageListByRole(RsOverseasApplicationQuery query) {
Long currentUserId = LoginHelper.getUserId();
String currentUserName = LoginHelper.getUsername();
String tenantId = LoginHelper.getTenantId();
log.info("🔍 查询用户待审批申请分页列表 - 用户ID: {}, 用户名: {}", currentUserId, currentUserName);
// 1. 获取当前用户的角色列表
List<SysRoleVo> userRoles = roleService.selectRolesByUserId(currentUserId);
if (userRoles == null || userRoles.isEmpty()) {
log.warn("❌ 用户 {} 没有任何角色,返回空列表", currentUserName);
return TableDataInfo.build();
}
// 2. 根据角色编码确定审批级别列表
List<String> deptHeadLevels = new ArrayList<>(); // 部门负责人审批级别
List<String> otherLevels = new ArrayList<>(); // 其他审批级别
Long deptIdForFilter = null;
boolean hasDeptHeadRole = false;
boolean hasOtherRole = false;
for (SysRoleVo role : userRoles) {
String roleKey = role.getRoleKey();
if (roleKey == null) {
continue;
}
// 角色编码映射到审批级别
if ("rs_bmfzr".equals(roleKey)) {
deptHeadLevels.add("1");
hasDeptHeadRole = true;
} else if ("hr_dept_role".equals(roleKey)) {
otherLevels.add("2");
hasOtherRole = true;
} else if ("discipline_committee_role".equals(roleKey)) {
otherLevels.add("3");
hasOtherRole = true;
} else if ("college_leader_role".equals(roleKey)) {
otherLevels.add("4");
hasOtherRole = true;
} else if ("party_secretary_role".equals(roleKey)) {
otherLevels.add("5");
hasOtherRole = true;
}
}
if (!hasDeptHeadRole && !hasOtherRole) {
log.warn("❌ 用户 {} 没有任何出国审批角色,返回空列表", currentUserName);
return TableDataInfo.build();
}
log.info("✅ 用户 {} 拥有审批级别 - 部门负责人: {}, 其他角色: {}",
currentUserName, deptHeadLevels, otherLevels);
// 3. 如果是部门负责人,获取部门ID进行过滤
if (hasDeptHeadRole) {
SysUserVo currentUser = userService.selectUserById(currentUserId);
if (currentUser != null && currentUser.getDeptId() != null) {
deptIdForFilter = currentUser.getDeptId();
log.info("🔒 [部门权限过滤] 部门负责人,用户: {}, 部门ID: {}, 只查看本部门待审批",
currentUserName, deptIdForFilter);
} else {
log.warn("⚠️ 用户 {} 是部门负责人但没有部门信息,无法过滤", currentUserName);
}
}
// 4. 设置租户ID
if (StringUtils.isNotBlank(tenantId)) {
query.setTenantId(tenantId);
}
// 5. 🔴 修复:分别查询不同角色的数据,然后合并去重
List<RsOverseasApplicationVo> allList = new ArrayList<>();
Map<Long, RsOverseasApplicationVo> applicationMap = new HashMap<>(); // 用于去重,key=applicationId
// 5.1 查询部门负责人的数据(本部门,审批级别='1')
if (hasDeptHeadRole && !deptHeadLevels.isEmpty() && deptIdForFilter != null) {
List<RsOverseasApplicationVo> deptList = baseMapper.selectPendingApplicationsByRole(
query, deptHeadLevels, deptIdForFilter);
log.info("📋 [部门负责人] 查询到 {} 条本部门待审批申请", deptList.size());
for (RsOverseasApplicationVo vo : deptList) {
applicationMap.put(vo.getApplicationId(), vo);
}
}
// 5.2 查询其他角色的数据(全校,审批级别为其他级别)
if (hasOtherRole && !otherLevels.isEmpty()) {
List<RsOverseasApplicationVo> otherList = baseMapper.selectPendingApplicationsByRole(
query, otherLevels, null); // deptIdForFilter=null,不限制部门
log.info("📋 [其他角色] 查询到 {} 条全校待审批申请", otherList.size());
for (RsOverseasApplicationVo vo : otherList) {
applicationMap.put(vo.getApplicationId(), vo);
}
}
// 5.3 合并去重后的结果,按创建时间倒序排列
allList = new ArrayList<>(applicationMap.values());
allList.sort((a, b) -> {
if (a.getCreateTime() != null && b.getCreateTime() != null) {
return b.getCreateTime().compareTo(a.getCreateTime());
}
return 0;
});
log.info("✅ [合并结果] 去重后共 {} 条待审批申请", allList.size());
// 6. 手动分页处理
int pageNum = query.getPageNum();
int pageSize = query.getPageSize();
int total = allList.size();
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, total);
List<RsOverseasApplicationVo> pageList;
if (start >= total) {
pageList = new ArrayList<>();
} else {
pageList = allList.subList(start, end);
}
// 7. 构建分页对象
Page<RsOverseasApplicationVo> page = new Page<>(pageNum, pageSize);
page.setRecords(pageList);
page.setTotal(total);
log.info("📋 用户 {} 查询到 {} 条待审批申请,分页显示第 {} 页,共 {} 条",
currentUserName, total, pageNum, pageList.size());
return TableDataInfo.build(page);
}
/**
* 转换实体为VO对象
* 🔴 修复:使用Mapper查询完整的VO对象,包含字典翻译等扩展信息
@@ -514,7 +664,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 提交出国申请(动态审批人方案)
*
*
* 🆕 核心改进:
* 1. 不再设置固定的 current_approver_id(该字段改为冗余展示字段)
* 2. 仅设置 approval_level='1',审批人根据权限动态查询
@@ -599,7 +749,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
RsOverseasApproval approval = new RsOverseasApproval();
approval.setApplicationId(applicationId);
approval.setApproverId(approverId);
approval.setApproverName(approverName);
approval.setApproverName(resolveApproverDisplayName(approverId, approverName));
approval.setApprovalLevel(application.getApprovalLevel());
approval.setApprovalStatus(approvalStatus);
approval.setApprovalOpinion(approvalOpinion);
@@ -721,7 +871,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 修复已审批通过但未创建记录的申请
*
*
* @return 修复的记录数量
*/
@Override
@@ -950,6 +1100,16 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
entity.setStampedFileUploadTime(bo.getStampedFileUploadTime());
entity.setStampedFileUploadStatus(bo.getStampedFileUploadStatus());
// ==================== 党籍保留附件相关字段 ====================
entity.setPartyRetentionFilePath(bo.getPartyRetentionFilePath());
entity.setPartyRetentionFileName(bo.getPartyRetentionFileName());
entity.setPartyRetentionUploadTime(bo.getPartyRetentionUploadTime());
entity.setPartyRetentionUploadStatus(bo.getPartyRetentionUploadStatus());
entity.setPartyRetentionFileOssId(bo.getPartyRetentionFileOssId());
// ==================== 附件ID列表 ====================
entity.setAttachmentIds(bo.getAttachmentIds());
// ==================== 租户信息 ====================
// 🔴 关键修复:设置租户ID(如果Bo中有值则使用Bo的值,否则依赖MyBatis-Plus自动填充)
if (bo.getTenantId() != null) {
@@ -975,6 +1135,22 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
}
}
/**
* 解析审批人的展示名称:优先使用用户昵称,缺省时回退账号
*/
private String resolveApproverDisplayName(Long approverId, String fallbackName) {
String displayName = fallbackName;
try {
SysUserVo approver = userService.selectUserById(approverId);
if (approver != null && StringUtils.isNotBlank(approver.getNickName())) {
displayName = approver.getNickName();
}
} catch (Exception e) {
log.warn("⚠️ 获取审批人昵称失败, 使用账号作为展示名称. approverId={}, error={}", approverId, e.getMessage());
}
return displayName;
}
/**
* 批量删除出国申请
*/
@@ -1008,7 +1184,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
@Override
@Transactional
public Boolean updatePartyRetentionFileInfo(Long applicationId, String filePath, String fileName,
java.util.Date uploadTime, String uploadStatus, Long ossId) {
java.util.Date uploadTime, String uploadStatus, String ossId) {
return baseMapper.update(null,
new LambdaUpdateWrapper<RsOverseasApplication>()
.eq(RsOverseasApplication::getApplicationId, applicationId)
@@ -1087,7 +1263,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据申请人ID获取部门审批人ID
*
*
* @param applicantId 申请人ID
* @return 部门审批人ID
*/
@@ -1134,7 +1310,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据申请人ID获取人事处审批人列表
*
*
* @param applicantId 申请人ID
* @return 人事处审批人ID列表
*/
@@ -1176,7 +1352,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据申请人查找对应的部门负责人审批人ID
*
*
* @param applicantId 申请人ID
* @return 部门负责人审批人ID
*/
@@ -1203,7 +1379,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据部门ID查找对应的部门负责人审批人
*
*
* @param deptId 部门ID
* @return 部门负责人审批人ID
*/
@@ -1251,7 +1427,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 查找父部门的负责人审批人
*
*
* @param deptId 当前部门ID
* @param approvers 所有部门负责人审批人列表
* @return 父部门负责人审批人ID
@@ -1288,7 +1464,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据审批级别获取下一级审批人ID
*
*
* @param approvalLevel 审批级别
* @return 下一级审批人ID
*/
@@ -1298,7 +1474,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 根据审批级别获取审批环节描述
*
*
* @param approvalLevel 审批级别
* @return 审批环节描述
*/
@@ -1321,14 +1497,14 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 🆕 根据当前登录用户的审批权限,获取其可审批的级别列表
*
*
* 权限到级别的映射:
* - rs:overseas:approve1 → 审批级别 "1" (部门负责人)
* - rs:overseas:approve2 → 审批级别 "2" (人事处)
* - rs:overseas:approve3 → 审批级别 "3" (纪委)
* - rs:overseas:approve4 → 审批级别 "4" (学院分管领导)
* - rs:overseas:approve5 → 审批级别 "5" (学院党委书记)
*
*
* @param userId 用户ID
* @return 可审批的级别列表,例如:["2", "3"]
*/
@@ -1371,7 +1547,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 🆕 根据审批级别获取对应的权限标识
*
*
* @param approvalLevel 审批级别(2-5)
* @return 权限标识,例如:"rs:overseas:approve2"
*/
@@ -1396,7 +1572,7 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
/**
* 🔴 新增方法:验证审批人是否有指定级别的审批权限
*
*
* @param approverId 审批人ID
* @param approvalLevel 需要的审批级别(1-5)
* @return true-有权限,false-无权限
@@ -1423,4 +1599,4 @@ public class RsOverseasApplicationServiceImpl extends ServiceImpl<RsOverseasAppl
return hasPermission;
}
}
}
@@ -311,6 +311,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
vo.setMobilePhone(application.getMobilePhone());
vo.setContactPhone(application.getContactPhone());
vo.setEmergencyContact(application.getEmergencyContact());
vo.setSignature(application.getSignature());
// 审批状态信息
vo.setStatus(application.getStatus());
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.rs.domain.RsOverseasItineraryChange;
@@ -13,14 +14,23 @@ import org.dromara.rs.domain.bo.RsOverseasItineraryChangeBo;
import org.dromara.rs.domain.query.RsOverseasItineraryChangeQuery;
import org.dromara.rs.domain.vo.RsOverseasItineraryChangeVo;
import org.dromara.rs.mapper.RsOverseasItineraryChangeMapper;
import org.dromara.rs.mapper.RsOverseasApplicationMapper;
import org.dromara.rs.mapper.RsOverseasRecordMapper;
import org.dromara.rs.domain.RsOverseasApplication;
import org.dromara.rs.domain.RsOverseasRecord;
import org.dromara.rs.domain.vo.RsOverseasRecordVo;
import org.dromara.rs.service.IRsOverseasItineraryChangeService;
import org.dromara.rs.service.IRsOverseasApplicationService;
import org.dromara.rs.service.IRsOverseasRecordService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* 出国行程变更申请Service业务层处理
@@ -35,14 +45,38 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
private final RsOverseasItineraryChangeMapper changeMapper;
private final IRsOverseasApplicationService applicationService;
private final RsOverseasApplicationMapper applicationMapper;
private final IRsOverseasRecordService recordService;
private final RsOverseasRecordMapper recordMapper;
/**
* 查询出国行程变更申请分页列表
*/
@Override
public TableDataInfo<RsOverseasItineraryChangeVo> queryPageList(RsOverseasItineraryChangeQuery query) {
// 查询所有符合条件的数据(不分页)
List<RsOverseasItineraryChangeVo> list = changeMapper.selectChangeList(query);
return TableDataInfo.build(list);
// 手动分页处理
int pageNum = query.getPageNum();
int pageSize = query.getPageSize();
int total = list.size();
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, total);
List<RsOverseasItineraryChangeVo> pageList;
if (start >= total) {
pageList = new ArrayList<>();
} else {
pageList = list.subList(start, end);
}
// 构建分页对象
Page<RsOverseasItineraryChangeVo> page = new Page<>(pageNum, pageSize);
page.setRecords(pageList);
page.setTotal(total);
return TableDataInfo.build(page);
}
/**
@@ -89,11 +123,12 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(RsOverseasItineraryChangeBo bo) {
// 查询出国申请信息
org.dromara.rs.domain.vo.RsOverseasApplicationVo application = applicationService.queryById(bo.getApplicationId());
org.dromara.rs.domain.vo.RsOverseasApplicationVo application = applicationService
.queryById(bo.getApplicationId());
if (application == null) {
throw new ServiceException("出国申请不存在");
}
// 检查申请状态是否为已通过
if (!"1".equals(application.getStatus())) {
throw new ServiceException("只能为已通过审批的出国申请提交变更申请");
@@ -102,8 +137,8 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
// 检查是否有未完成的变更申请
LambdaQueryWrapper<RsOverseasItineraryChange> wrapper = Wrappers.lambdaQuery();
wrapper.eq(RsOverseasItineraryChange::getApplicationId, bo.getApplicationId())
.in(RsOverseasItineraryChange::getStatus, Arrays.asList("0")) // 待审核
.eq(RsOverseasItineraryChange::getDeleted, 0);
.in(RsOverseasItineraryChange::getStatus, Arrays.asList("0")) // 待审核
.eq(RsOverseasItineraryChange::getDeleted, 0);
Long count = changeMapper.selectCount(wrapper);
if (count > 0) {
throw new ServiceException("该出国申请已有待审核的变更申请,请先处理");
@@ -117,11 +152,12 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
entity.setApplicantId(userId);
entity.setApplicantName(LoginHelper.getUsername());
entity.setApplicationId(bo.getApplicationId());
// 从出国申请中复制原始信息
entity.setOriginalDestination(application.getDestination());
entity.setOriginalDestinationCity(application.getDestinationCity());
entity.setOriginalExitReason(application.getExitReason() != null ? application.getExitReason() : application.getPurposeDetail());
entity.setOriginalExitReason(
application.getExitReason() != null ? application.getExitReason() : application.getPurposeDetail());
entity.setOriginalStartDate(application.getStartDate());
entity.setOriginalEndDate(application.getEndDate());
entity.setOriginalDuration(application.getDuration());
@@ -148,9 +184,10 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
throw new ServiceException("变更申请不存在");
}
// 只有待审核或已拒绝的申请可以修改
if (!"0".equals(original.getStatus()) && !"2".equals(original.getStatus())) {
throw new ServiceException("只有待审核或已拒绝的变更申请可以修改");
// 只有待审核、已拒绝或已撤回的申请可以修改
if (!"0".equals(original.getStatus()) && !"2".equals(original.getStatus())
&& !"3".equals(original.getStatus())) {
throw new ServiceException("只有待审核、已拒绝或已撤回的变更申请可以修改");
}
// 检查权限:只能修改自己的申请
@@ -161,7 +198,7 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
// 转换BO到Entity
RsOverseasItineraryChange entity = BeanUtil.toBean(bo, RsOverseasItineraryChange.class);
// 重新提交时,状态改为待审核
entity.setStatus("0");
entity.setApprovalOpinion(null);
@@ -179,24 +216,24 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
@Transactional(rollbackFor = Exception.class)
public Boolean deleteByIds(List<Long> changeIds) {
Long userId = LoginHelper.getUserId();
for (Long changeId : changeIds) {
RsOverseasItineraryChange change = changeMapper.selectById(changeId);
if (change == null) {
continue;
}
// 检查权限:只能删除自己的申请
if (!userId.equals(change.getApplicantId())) {
throw new ServiceException("只能删除自己的变更申请");
}
// 只有待审核或已拒绝的申请可以删除
if (!"0".equals(change.getStatus()) && !"2".equals(change.getStatus())) {
throw new ServiceException("只有待审核或已拒绝的变更申请可以删除");
}
}
return changeMapper.deleteBatchIds(changeIds) > 0;
}
@@ -229,6 +266,12 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
/**
* 审批变更申请
*
* 🔴 审批通过后,需要更新原申请单和记录的数据:
* 1. 前往国家 → 更新到 rs_overseas_application 和 rs_overseas_record
* 2. 实际出行开始时间、结束时间 → 更新到 rs_overseas_application 和 rs_overseas_record
* 3. 行程安排 → 更新到 rs_overseas_application 和 rs_overseas_record
* 4. 出境事由 → 追加到 rs_overseas_application 的备注字段
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -249,9 +292,139 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
change.setApprovalOpinion(opinion);
change.setApprovalTime(new Date());
change.setApproverId(userId);
change.setApproverName(LoginHelper.getUsername());
String displayName = StringUtils.defaultIfBlank(LoginHelper.getNickname(), LoginHelper.getUsername());
change.setApproverName(displayName);
return changeMapper.updateById(change) > 0;
// 更新变更申请状态
boolean updateChangeResult = changeMapper.updateById(change) > 0;
if (!updateChangeResult) {
throw new ServiceException("更新变更申请状态失败");
}
// 🔴 如果审批通过,需要更新原申请单和记录的数据
if (approved && change.getApplicationId() != null) {
log.info("🔧 [审批通过] 开始更新原申请单和记录数据,变更ID: {}, 申请ID: {}", changeId, change.getApplicationId());
try {
// 1. 更新 rs_overseas_application 表
RsOverseasApplication application = applicationMapper.selectById(change.getApplicationId());
if (application != null) {
LambdaUpdateWrapper<RsOverseasApplication> appUpdateWrapper = new LambdaUpdateWrapper<>();
appUpdateWrapper.eq(RsOverseasApplication::getApplicationId, change.getApplicationId());
// 更新前往国家(destination 和 destinationCountry 同步更新)
if (change.getNewDestination() != null && !change.getNewDestination().trim().isEmpty()) {
appUpdateWrapper.set(RsOverseasApplication::getDestination, change.getNewDestination());
appUpdateWrapper.set(RsOverseasApplication::getDestinationCountry, change.getNewDestination());
log.info("✅ [更新申请单] 前往国家: {} (同步更新 destination 和 destinationCountry)",
change.getNewDestination());
}
// 更新出发日期和返回日期
if (change.getNewStartDate() != null) {
appUpdateWrapper.set(RsOverseasApplication::getStartDate, change.getNewStartDate());
log.info("✅ [更新申请单] 出发日期: {}", change.getNewStartDate());
}
if (change.getNewEndDate() != null) {
appUpdateWrapper.set(RsOverseasApplication::getEndDate, change.getNewEndDate());
log.info("✅ [更新申请单] 返回日期: {}", change.getNewEndDate());
// 重新计算出国天数
if (change.getNewStartDate() != null && change.getNewEndDate() != null) {
long diffInMillies = change.getNewEndDate().getTime() - change.getNewStartDate().getTime();
int days = (int) (diffInMillies / (1000 * 60 * 60 * 24)) + 1;
appUpdateWrapper.set(RsOverseasApplication::getDuration, days);
log.info("✅ [更新申请单] 出国天数: {} 天", days);
}
}
// 更新行程安排
if (change.getNewItinerary() != null && !change.getNewItinerary().trim().isEmpty()) {
appUpdateWrapper.set(RsOverseasApplication::getItinerary, change.getNewItinerary());
log.info("✅ [更新申请单] 行程安排已更新");
}
// 追加出境事由到备注字段
if (change.getNewExitReason() != null && !change.getNewExitReason().trim().isEmpty()) {
String currentRemarks = application.getRemarks() != null ? application.getRemarks() : "";
String newRemarks = currentRemarks.isEmpty()
? "行程变更-出境事由: " + change.getNewExitReason()
: currentRemarks + "\n行程变更-出境事由: " + change.getNewExitReason();
appUpdateWrapper.set(RsOverseasApplication::getRemarks, newRemarks);
log.info("✅ [更新申请单] 备注已追加出境事由: {}", change.getNewExitReason());
}
boolean appUpdateResult = applicationMapper.update(null, appUpdateWrapper) > 0;
if (!appUpdateResult) {
log.warn("⚠️ [更新申请单] 更新失败,申请ID: {}", change.getApplicationId());
} else {
log.info("✅ [更新申请单] 更新成功,申请ID: {}", change.getApplicationId());
}
} else {
log.warn("⚠️ [更新申请单] 申请单不存在,申请ID: {}", change.getApplicationId());
}
// 2. 更新 rs_overseas_record 表
RsOverseasRecordVo recordVo = recordMapper
.selectOverseasRecordByApplicationId(change.getApplicationId());
if (recordVo != null) {
RsOverseasRecord record = recordMapper.selectById(recordVo.getRecordId());
if (record != null) {
LambdaUpdateWrapper<RsOverseasRecord> recordUpdateWrapper = new LambdaUpdateWrapper<>();
recordUpdateWrapper.eq(RsOverseasRecord::getRecordId, record.getRecordId());
// 更新前往国家
if (change.getNewDestination() != null && !change.getNewDestination().trim().isEmpty()) {
recordUpdateWrapper.set(RsOverseasRecord::getDestination, change.getNewDestination());
log.info("✅ [更新记录] 前往国家: {}", change.getNewDestination());
}
// 更新实际开始日期和结束日期
if (change.getNewStartDate() != null) {
recordUpdateWrapper.set(RsOverseasRecord::getActualStartDate, change.getNewStartDate());
log.info("✅ [更新记录] 实际开始日期: {}", change.getNewStartDate());
}
if (change.getNewEndDate() != null) {
recordUpdateWrapper.set(RsOverseasRecord::getActualEndDate, change.getNewEndDate());
log.info("✅ [更新记录] 实际结束日期: {}", change.getNewEndDate());
// 重新计算实际出国天数
if (change.getNewStartDate() != null && change.getNewEndDate() != null) {
long diffInMillies = change.getNewEndDate().getTime()
- change.getNewStartDate().getTime();
int days = (int) (diffInMillies / (1000 * 60 * 60 * 24)) + 1;
recordUpdateWrapper.set(RsOverseasRecord::getActualDuration, days);
log.info("✅ [更新记录] 实际出国天数: {} 天", days);
}
}
// 更新行程安排
if (change.getNewItinerary() != null && !change.getNewItinerary().trim().isEmpty()) {
recordUpdateWrapper.set(RsOverseasRecord::getItinerary, change.getNewItinerary());
log.info("✅ [更新记录] 行程安排已更新");
}
boolean recordUpdateResult = recordMapper.update(null, recordUpdateWrapper) > 0;
if (!recordUpdateResult) {
log.warn("⚠️ [更新记录] 更新失败,记录ID: {}", record.getRecordId());
} else {
log.info("✅ [更新记录] 更新成功,记录ID: {}", record.getRecordId());
}
} else {
log.warn("⚠️ [更新记录] 记录不存在,记录ID: {}", recordVo.getRecordId());
}
} else {
log.warn("⚠️ [更新记录] 未找到关联记录,申请ID: {}", change.getApplicationId());
}
log.info("✅ [审批通过] 数据更新完成,变更ID: {}", changeId);
} catch (Exception e) {
log.error("❌ [审批通过] 更新原申请单和记录数据失败,变更ID: {}, 错误: {}", changeId, e.getMessage(), e);
throw new ServiceException("审批通过后更新原申请单和记录数据失败: " + e.getMessage());
}
}
return true;
}
/**
@@ -262,4 +435,3 @@ public class RsOverseasItineraryChangeServiceImpl implements IRsOverseasItinerar
return changeMapper.selectChangeListByRecordId(recordId);
}
}
@@ -617,5 +617,42 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</if>
ORDER BY a.apply_time DESC, a.create_time DESC
</select>
<!-- 🔴 新增:根据角色查询待审批申请列表(支持分页和搜索) -->
<select id="selectPendingApplicationsByRole" resultMap="RsOverseasApplicationVoResult">
<include refid="selectOverseasApplicationVo"/>
<where>
oa.deleted = '0'
AND oa.status = '0'
<!-- 审批级别过滤 -->
<if test="approvalLevels != null and approvalLevels.size() > 0">
AND oa.approval_level IN
<foreach collection="approvalLevels" item="level" open="(" separator="," close=")">
#{level}
</foreach>
</if>
<!-- 部门过滤(仅对部门负责人生效) -->
<if test="deptId != null">
AND EXISTS (
SELECT 1 FROM sys_user u
WHERE u.user_id = oa.applicant_id
AND u.dept_id = #{deptId}
)
</if>
<!-- 搜索条件:前往国家 -->
<if test="query != null and query.destination != null and query.destination != ''">
AND oa.destination LIKE CONCAT('%', #{query.destination}, '%')
</if>
<!-- 搜索条件:出境事由 -->
<if test="query != null and query.exitReason != null and query.exitReason != ''">
AND oa.exit_reason = #{query.exitReason}
</if>
<!-- 租户过滤 -->
<if test="query != null and query.tenantId != null and query.tenantId != ''">
AND oa.tenant_id = #{query.tenantId}
</if>
</where>
ORDER BY oa.create_time DESC
</select>
</mapper>
@@ -74,8 +74,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectOverseasRecordDetailVo">
select
ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department,
ors.position,
COALESCE(sp.post_name, ors.position) as position_text,
oa.position,
oa.position as position_text,
ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city,
ors.actual_start_date, ors.actual_end_date, ors.actual_duration, ors.actual_budget,
ors.invitation, ors.itinerary, ors.accommodation_plan, ors.transportation_plan,
@@ -134,7 +134,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'record' as data_source
from rs_overseas_record ors
left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0'
left join sys_post sp on (ors.position = CAST(sp.post_id AS CHAR) or ors.position = sp.post_code) and sp.status = '0'
</sql>
<!-- 联合查询:出国记录 + 已完成审批的申请 -->
@@ -142,8 +141,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-- 查询已创建的出国记录(关联申请表获取扩展字段)
select
ors.record_id, ors.application_id, ors.applicant_id, ors.applicant_name, ors.department,
ors.position,
COALESCE(sp1.post_name, ors.position) as position_text,
oa.position,
oa.position as position_text,
ors.contact_phone, ors.contact_email, ors.purpose, ors.purpose_detail, ors.destination, ors.destination_city,
ors.actual_start_date as actual_start_date, ors.actual_end_date as actual_end_date, ors.actual_duration as actual_duration,
ors.actual_budget as actual_budget,
@@ -212,7 +211,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'record' as data_source
from rs_overseas_record ors
left join rs_overseas_application oa on ors.application_id = oa.application_id and oa.deleted = '0'
left join sys_post sp1 on (ors.position = CAST(sp1.post_id AS CHAR) or ors.position = sp1.post_code) and sp1.status = '0'
where ors.deleted = '0'
UNION ALL
@@ -222,7 +220,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
NULL as record_id,
oa.application_id, oa.applicant_id, oa.applicant_name, oa.department,
oa.position,
COALESCE(sp2.post_name, oa.position) as position_text,
oa.position as position_text,
oa.contact_phone, oa.contact_email, oa.purpose, oa.purpose_detail, oa.destination, oa.destination_city,
oa.start_date as actual_start_date, oa.end_date as actual_end_date, oa.duration as actual_duration,
NULL as actual_budget,
@@ -284,7 +282,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
'进行中' as completion_status_text,
'approved_application' as data_source
from rs_overseas_application oa
left join sys_post sp2 on (oa.position = CAST(sp2.post_id AS CHAR) or oa.position = sp2.post_code) and sp2.status = '0'
where oa.deleted = '0'
and oa.status = '1'
and not exists (
@@ -1,142 +0,0 @@
-- 招聘模块表结构
-- 创建于 2025-07-02
-- 职位表
DROP TABLE IF EXISTS `rs_job`;
CREATE TABLE `rs_job` (
`job_id` bigint(20) NOT NULL COMMENT '职位ID',
`title` varchar(100) NOT NULL COMMENT '职位名称',
`department` varchar(50) NOT NULL COMMENT '部门',
`salary_range` varchar(50) DEFAULT NULL COMMENT '薪资范围',
`location` varchar(100) DEFAULT NULL COMMENT '工作地点',
`description` text COMMENT '职位描述',
`requirements` text COMMENT '任职要求',
`is_active` char(1) DEFAULT '0' COMMENT '是否在招(0是 1否)',
`publish_time` datetime DEFAULT NULL COMMENT '发布时间',
`view_count` int(11) DEFAULT '0' COMMENT '浏览次数',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL 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 '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`job_id`),
KEY `idx_department` (`department`),
KEY `idx_is_active` (`is_active`)
) ENGINE=InnoDB COMMENT='招聘职位表';
-- 应聘者表
DROP TABLE IF EXISTS `rs_applicant`;
CREATE TABLE `rs_applicant` (
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '关联用户ID',
`name` varchar(50) NOT NULL COMMENT '姓名',
`phone` varchar(20) NOT NULL COMMENT '手机号码',
`email` varchar(100) NOT NULL COMMENT '电子邮箱',
`resume_file_path` varchar(255) DEFAULT NULL COMMENT '简历文件路径',
`self_intro` text COMMENT '自我介绍',
`current_status` varchar(20) DEFAULT 'active' COMMENT '当前状态(active-可申请 hired-已录用 rejected-已拒绝)',
`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 '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`applicant_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB COMMENT='应聘者信息表';
-- 教育经历表
DROP TABLE IF EXISTS `rs_education`;
CREATE TABLE `rs_education` (
`edu_id` bigint(20) NOT NULL COMMENT '教育经历ID',
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`school_name` varchar(100) NOT NULL COMMENT '学校名称',
`major` varchar(100) DEFAULT NULL COMMENT '专业',
`degree` varchar(50) DEFAULT NULL COMMENT '学位',
`start_date` date DEFAULT NULL COMMENT '开始日期',
`end_date` date DEFAULT NULL COMMENT '结束日期',
`gpa` varchar(20) DEFAULT NULL COMMENT '绩点',
`description` 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 '更新时间',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`edu_id`),
KEY `idx_applicant_id` (`applicant_id`)
) ENGINE=InnoDB COMMENT='教育经历表';
-- 工作经历表
DROP TABLE IF EXISTS `rs_work_experience`;
CREATE TABLE `rs_work_experience` (
`exp_id` bigint(20) NOT NULL COMMENT '工作经历ID',
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`company_name` varchar(100) NOT NULL COMMENT '公司名称',
`position` varchar(100) NOT NULL COMMENT '职位',
`start_date` date DEFAULT NULL COMMENT '开始日期',
`end_date` date DEFAULT NULL COMMENT '结束日期',
`is_current` char(1) DEFAULT '0' COMMENT '是否当前工作(0是 1否)',
`description` 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 '更新时间',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`exp_id`),
KEY `idx_applicant_id` (`applicant_id`)
) ENGINE=InnoDB COMMENT='工作经历表';
-- 申请记录表
DROP TABLE IF EXISTS `rs_application`;
CREATE TABLE `rs_application` (
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`job_id` bigint(20) NOT NULL COMMENT '职位ID',
`applicant_id` bigint(20) NOT NULL COMMENT '应聘者ID',
`apply_time` datetime NOT NULL COMMENT '申请时间',
`status` varchar(20) DEFAULT 'pending' COMMENT '状态(pending-待处理 interviewed-已面试 offered-已录用 rejected-已拒绝)',
`hr_notes` text COMMENT 'HR备注',
`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 '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`application_id`),
KEY `idx_job_id` (`job_id`),
KEY `idx_applicant_id` (`applicant_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB COMMENT='申请记录表';
-- 附件表
DROP TABLE IF EXISTS `rs_attachment`;
CREATE TABLE `rs_attachment` (
`attachment_id` bigint(20) NOT NULL COMMENT '附件ID',
`application_id` bigint(20) NOT NULL COMMENT '申请ID',
`file_name` varchar(100) NOT NULL COMMENT '文件名',
`file_path` varchar(255) NOT NULL COMMENT '文件路径',
`file_type` varchar(50) DEFAULT NULL COMMENT '文件类型',
`file_size` bigint(20) 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 '更新时间',
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
PRIMARY KEY (`attachment_id`),
KEY `idx_application_id` (`application_id`)
) ENGINE=InnoDB COMMENT='申请附件表';
-- 插入初始测试数据
INSERT INTO `rs_job` (`job_id`, `title`, `department`, `salary_range`, `location`, `description`, `requirements`, `is_active`, `create_time`, `create_by`, `update_time`, `update_by`, `remark`, `deleted`, `tenant_id`)
VALUES
(1, '高级Java开发工程师', '教学', '15k-25k', '杭州', '负责校园教学管理系统的开发和维护,参与核心模块设计与实现,优化系统性能,解决技术难题。\n作为团队核心成员,参与技术方案评审,指导初级开发人员,确保项目按时高质量交付。', '计算机相关专业本科及以上学历,3年以上Java开发经验\n精通Java核心技术,熟悉Spring Boot、MyBatis等主流框架\n熟练掌握MySQL数据库设计与优化\n有教育行业相关系统开发经验者优先\n良好的沟通能力和团队协作精神', '0', NOW(), 1, NOW(), 1, '示例职位', '0', '000000'),
(2, '前端工程师', '教学', '12k-18k', '杭州', '负责校园教学管理系统前端开发,使用Vue3+TypeScript技术栈,参与UI设计与实现', '计算机相关专业本科及以上学历,2年以上前端开发经验\n精通HTML5/CSS3/JavaScript,熟悉Vue3/React等主流框架\n熟练使用TypeScript进行开发\n有教育行业相关系统开发经验者优先', '0', NOW(), 1, NOW(), 1, '示例职位', '0', '000000'),
(3, '行政助理', '行政', '5k-8k', '杭州', '协助部门主管处理日常行政事务,包括文件整理、会议安排、接待来访等工作', '大专及以上学历,行政管理相关专业优先\n熟悉办公软件,有较强的文字处理能力\n工作认真负责,有较强的组织协调能力\n有相关工作经验者优先', '0', NOW(), 1, NOW(), 1, '示例职位', '0', '000000');
@@ -1,11 +0,0 @@
-- -----------------------------
-- 删除培训申请表中的提交次数字段
-- -----------------------------
-- 删除 rs_training_application 表中的 submit_count 字段
-- 注意:MySQL 5.7及以下版本不支持 DROP COLUMN IF EXISTS 语法
-- 需要先检查字段是否存在,然后删除
ALTER TABLE rs_training_application DROP COLUMN submit_count;
-- 验证字段已删除
DESCRIBE rs_training_application;
@@ -1,62 +0,0 @@
-- -----------------------------
-- 培训审批数据一致性检查和修复
-- -----------------------------
-- 检查审批记录表是否为空
SELECT COUNT(*) AS approval_count FROM rs_training_approval WHERE deleted = '0';
-- 检查审批记录与申请记录是否一致
SELECT
COUNT(*) AS inconsistent_count
FROM
rs_training_approval a
LEFT JOIN
rs_training_application app ON a.application_id = app.application_id
WHERE
a.deleted = '0'
AND (app.application_id IS NULL OR app.deleted = '1');
-- 插入测试数据(如果没有审批记录)
INSERT INTO rs_training_approval (
approval_id, application_id, approver_id, approver_name,
approval_level, status, opinion, approval_time, tenant_id,
create_by, create_time, deleted
)
SELECT
FLOOR(RAND() * 1000000) + 100000, -- 随机审批ID
app.application_id, -- 使用真实申请ID
app.create_by, -- 使用申请的创建者ID作为审批人ID
(SELECT user_name FROM sys_user WHERE user_id = app.create_by LIMIT 1), -- 获取用户名
1, -- 审批级别
app.status, -- 使用申请状态
'系统自动生成的测试审批记录', -- 审批意见
app.update_time, -- 使用申请更新时间作为审批时间
app.tenant_id, -- 租户ID
app.create_by, -- 创建者
NOW(), -- 创建时间
'0' -- 未删除
FROM
rs_training_application app
WHERE
app.deleted = '0'
AND app.status IN ('1', '2') -- 已通过或已拒绝
AND NOT EXISTS (
SELECT 1
FROM rs_training_approval a
WHERE a.application_id = app.application_id AND a.deleted = '0'
)
LIMIT 5; -- 限制生成5条测试数据
-- 查询生成的测试数据
SELECT
a.approval_id, a.application_id, a.approver_name, a.status, a.opinion, a.approval_time,
app.user_name AS applicant_name, app.reason
FROM
rs_training_approval a
JOIN
rs_training_application app ON a.application_id = app.application_id
WHERE
a.deleted = '0'
AND a.create_time > DATE_SUB(NOW(), INTERVAL 1 HOUR) -- 最近一小时创建的记录
ORDER BY
a.create_time DESC;
@@ -1,38 +0,0 @@
-- ----------------------------
-- 培训管理模块测试数据
-- ----------------------------
-- ----------------------------
-- 培训课程表测试数据
-- ----------------------------
INSERT INTO `rs_training_course` (`course_id`, `title`, `description`, `instructor`, `duration`, `location`, `capacity`, `status`, `start_time`, `end_time`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `deleted`)
VALUES
(10001, '新入职教师培训', '针对新入职教师的岗前培训,包括教学理念、师德师风、教学方法等内容', '张教授', 16, '学术会议厅', 50, '1', '2025-08-01 09:00:00', '2025-08-03 17:00:00', '000000', 1, NOW(), 1, NOW(), '重要培训', '0'),
(10002, '教育技术与信息化', '介绍现代教育技术在教学中的应用,包括多媒体教学、在线教育平台等', '李博士', 12, '多媒体教室', 30, '1', '2025-08-10 09:00:00', '2025-08-12 17:00:00', '000000', 1, NOW(), 1, NOW(), '技术培训', '0'),
(10003, '教学方法与技能培训', '提升教师教学技能,包括课堂管理、学生互动、教学评估等', '王老师', 8, '实训楼', 40, '1', '2025-08-05 14:00:00', '2025-08-05 18:00:00', '000000', 1, NOW(), 1, NOW(), '实用培训', '0'),
(10004, '学生管理与心理健康', '帮助教师了解学生心理,掌握学生管理技巧和心理疏导方法', '陈心理师', 6, '学术报告厅', 60, '1', '2025-08-15 14:00:00', '2025-08-15 18:00:00', '000000', 1, NOW(), 1, NOW(), '心理培训', '0'),
(10005, '科研方法与学术写作', '指导教师进行科学研究,提升学术写作和论文发表能力', '赵教授', 20, '图书馆报告厅', 25, '1', '2025-08-25 09:00:00', '2025-08-30 17:00:00', '000000', 1, NOW(), 1, NOW(), '科研培训', '0');
-- ----------------------------
-- 培训申请表测试数据
-- ----------------------------
INSERT INTO `rs_training_application` (`application_id`, `course_id`, `user_id`, `user_name`, `department`, `position`, `reason`, `status`, `apply_time`, `current_approver`, `process_instance_id`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `deleted`)
VALUES
(20001, 10001, 2, '张老师', '教务处', '教师', '新入职需要培训', '1', '2025-07-25 12:03:49', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), NULL, '0'),
(20002, 10002, 3, '王老师', '信息技术中心', '工程师', '学习新技术', '1', '2025-08-01 12:37:17', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '未完成', '0'),
(20003, 10003, 4, '刘老师', '计算机学院', '讲师', '提升教学技能', '1', '2025-07-25 12:04:06', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '培训尚未结束', '0'),
(20004, 10004, 5, '李老师', '学生处', '辅导员', '提升学生管理能力', '1', '2025-08-06 14:24:37', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '未完成', '0'),
(20005, 10005, 6, '陈老师', '科研处', '副教授', '提升科研能力', '1', '2025-08-22 15:25:51', 'admin', NULL, '000000', 1, NOW(), 1, NOW(), '未完成', '0');
-- ----------------------------
-- 培训审批表测试数据
-- ----------------------------
INSERT INTO `rs_training_approval` (`approval_id`, `application_id`, `approver_id`, `approver_name`, `approval_level`, `status`, `opinion`, `approval_time`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `deleted`)
VALUES
(30001, 20001, 101, '王主任', 1, '1', '同意参加,安排好工作交接', '2025-07-21 09:30:00', '000000', 101, NOW(), 101, NOW(), NULL, '0'),
(30002, 20003, 102, '孙院长', 1, '1', '批准参加,对提升教学质量有帮助', '2025-07-23 10:15:00', '000000', 102, NOW(), 102, NOW(), NULL, '0'),
(30003, 20004, 103, '周主任', 1, '2', '近期有重要工作交接,建议下次再参加', '2025-07-24 14:20:00', '000000', 103, NOW(), 103, NOW(), NULL, '0');
-- ----------------------------
-- 培训记录表测试数据已删除,请通过申请界面新增
-- ----------------------------
@@ -1,43 +0,0 @@
-- -----------------------------
-- 培训审批数据修复脚本
-- -----------------------------
-- 检查comment列是否存在,如果存在则迁移数据到opinion列
SET @columnExists = 0;
SELECT COUNT(*) INTO @columnExists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'rs_training_approval'
AND COLUMN_NAME = 'comment';
-- 如果comment列存在,将数据从comment迁移到opinion
SET @sqlStmt = IF(@columnExists > 0,
'UPDATE rs_training_approval SET opinion = comment WHERE opinion IS NULL AND comment IS NOT NULL',
'SELECT "comment列不存在,无需迁移" AS message');
PREPARE stmt FROM @sqlStmt;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 添加测试数据(如果表为空)
INSERT INTO rs_training_approval (
approval_id, application_id, approver_id, approver_name,
approval_level, status, opinion, approval_time, tenant_id,
create_by, create_time, deleted
)
SELECT
FLOOR(RAND() * 1000000) + 100000, -- 随机ID
app.application_id, -- 关联到真实的申请ID
1, -- 管理员ID
'系统管理员', -- 管理员名称
1, -- 第一级审批
'1', -- 已通过状态
'系统自动生成的测试审批记录', -- 审批意见
NOW(), -- 当前时间
app.tenant_id, -- 使用申请的租户ID
app.create_by, -- 使用申请的创建者
NOW(), -- 当前时间
'0' -- 未删除
FROM rs_training_application app
WHERE NOT EXISTS (SELECT 1 FROM rs_training_approval)
LIMIT 1;
@@ -1,58 +0,0 @@
-- -----------------------------
-- 培训审批历史记录完整性更新脚本
-- -----------------------------
-- 1. 在培训申请表中添加提交次数字段
ALTER TABLE rs_training_application ADD COLUMN IF NOT EXISTS submit_count int DEFAULT 1 COMMENT '提交次数';
-- 2. 检查并移除审批表可能存在的唯一约束(允许一个申请有多条审批记录)
ALTER TABLE rs_training_approval DROP INDEX IF EXISTS uk_application_approver;
ALTER TABLE rs_training_approval DROP INDEX IF EXISTS uk_application_level;
-- 3. 确保审批表中有足够的索引来高效查询
CREATE INDEX IF NOT EXISTS idx_training_approval_application ON rs_training_approval(application_id);
CREATE INDEX IF NOT EXISTS idx_training_approval_status ON rs_training_approval(status);
CREATE INDEX IF NOT EXISTS idx_training_approval_time ON rs_training_approval(approval_time);
-- 4. 添加审批记录表注释说明
ALTER TABLE rs_training_approval COMMENT = '培训审批表(记录所有审批动作,包括通过和拒绝记录)';
-- 5. 更新已有数据的提交次数为1(如果有)
UPDATE rs_training_application SET submit_count = 1 WHERE submit_count IS NULL;
-- 6. 检查并修复可能的数据不一致问题
-- 查找有申请状态为已拒绝但没有对应拒绝记录的数据
SELECT
app.application_id,
app.course_id,
app.user_name,
app.status,
COUNT(appr.approval_id) as approval_count
FROM
rs_training_application app
LEFT JOIN
rs_training_approval appr ON app.application_id = appr.application_id
WHERE
app.status = '2' AND app.deleted = '0'
GROUP BY
app.application_id
HAVING
approval_count = 0;
-- 查找有申请状态为已通过但没有对应通过记录的数据
SELECT
app.application_id,
app.course_id,
app.user_name,
app.status,
COUNT(appr.approval_id) as approval_count
FROM
rs_training_application app
LEFT JOIN
rs_training_approval appr ON app.application_id = appr.application_id
WHERE
app.status = '1' AND app.deleted = '0'
GROUP BY
app.application_id
HAVING
approval_count = 0;
@@ -1,19 +0,0 @@
-- -----------------------------
-- 培训审批历史记录更新
-- -----------------------------
-- 确保审批表中没有唯一约束,允许一个申请有多条审批记录
ALTER TABLE rs_training_approval DROP INDEX IF EXISTS uk_application_approver;
-- 添加审批意见列(如果不存在)
ALTER TABLE rs_training_approval ADD COLUMN IF NOT EXISTS opinion varchar(500) COMMENT '审批意见' AFTER status;
-- 添加comment列的别名以保持向后兼容(如果存在)
ALTER TABLE rs_training_approval CHANGE COLUMN comment opinion varchar(500) COMMENT '审批意见';
-- 更新审批记录表注释
ALTER TABLE rs_training_approval COMMENT = '培训审批表(记录所有审批动作,包括拒绝记录)';
-- 添加相关索引以提高查询效率
CREATE INDEX IF NOT EXISTS idx_training_approval_application ON rs_training_approval(application_id);
CREATE INDEX IF NOT EXISTS idx_training_approval_status ON rs_training_approval(status);
@@ -1,17 +0,0 @@
-- 更新培训课程表,添加create_dept字段
ALTER TABLE rs_training_course ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
-- 更新培训申请表,添加create_dept字段
ALTER TABLE rs_training_application ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
-- 更新培训申请表,添加location、duration、startTime和endTime字段
ALTER TABLE rs_training_application ADD COLUMN location varchar(200) COMMENT '培训地点' AFTER current_approver;
ALTER TABLE rs_training_application ADD COLUMN duration int(11) COMMENT '学时' AFTER location;
ALTER TABLE rs_training_application ADD COLUMN start_time datetime COMMENT '开始时间' AFTER duration;
ALTER TABLE rs_training_application ADD COLUMN end_time datetime COMMENT '结束时间' AFTER start_time;
-- 更新培训审批表,添加create_dept字段
ALTER TABLE rs_training_approval ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
-- 更新培训记录表,添加create_dept字段
ALTER TABLE rs_training_record ADD COLUMN create_dept bigint(20) COMMENT '创建部门' AFTER tenant_id;
@@ -1,122 +0,0 @@
-- ----------------------------
-- 培训管理模块数据库表
-- ----------------------------
-- ----------------------------
-- 培训课程表
-- ----------------------------
drop table if exists rs_training_course;
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) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (course_id)
) engine=innodb comment = '培训课程表';
-- ----------------------------
-- 培训申请表
-- ----------------------------
drop table if exists rs_training_application;
create table rs_training_application (
application_id bigint(20) not null comment '申请ID',
course_name varchar(100) not null comment '培训名称',
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 comment '申请时间',
current_approver varchar(50) comment '当前审批人',
location varchar(200) comment '培训地点',
duration int(11) comment '学时',
start_time datetime comment '开始时间',
end_time datetime comment '结束时间',
tenant_id varchar(20) default '000000' comment '租户编号',
create_dept bigint(20) comment '创建部门',
reject_time datetime comment '拒绝时间',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (application_id)
) engine=innodb comment = '培训申请表';
-- ----------------------------
-- 培训审批表
-- ----------------------------
drop table if exists rs_training_approval;
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) comment '审批意见',
approval_time datetime comment '审批时间',
tenant_id varchar(20) default '000000' comment '租户编号',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (approval_id)
) engine=innodb comment = '培训审批表';
-- ----------------------------
-- 培训记录表
-- ----------------------------
drop table if exists rs_training_record;
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 comment '完成时间',
score decimal(5,2) comment '成绩',
certificate_path varchar(255) comment '证书路径',
certificate_number varchar(50) comment '证书编号',
attachment_ids varchar(255) comment '证书附件ID(多个用逗号分隔)',
feedback text comment '培训反馈',
location varchar(255) comment '培训地点',
duration int(11) comment '学时',
apply_time datetime comment '申请时间',
status varchar(10) comment '申请状态',
course_name varchar(255) comment '课程名称',
department varchar(255) comment '所属部门',
position varchar(255) comment '职位',
reason varchar(255) comment '申请理由',
current_approver varchar(255) comment '当前审批人',
start_time datetime comment '开始时间',
end_time datetime comment '结束时间',
tenant_id varchar(20) default '000000' comment '租户编号',
create_dept bigint(20) comment '创建部门',
create_by bigint(20) comment '创建者',
create_time datetime comment '创建时间',
update_by bigint(20) comment '更新者',
update_time datetime comment '更新时间',
remark varchar(500) comment '备注',
deleted char(1) default '0' comment '删除标志',
primary key (record_id)
) engine=innodb comment = '培训记录表';
@@ -1,5 +0,0 @@
-- 添加自定义课程名称字段
-- 创建于 2025-07-24
-- 在rs_training_application表中添加course_name列,用于存储自定义培训名称
ALTER TABLE `rs_training_application` ADD COLUMN `course_name` varchar(100) DEFAULT NULL COMMENT '自定义课程名称' AFTER `course_id`;
@@ -1,7 +0,0 @@
-- 为rs_application表添加附件相关字段
ALTER TABLE `rs_application`
ADD COLUMN `attachment_path` varchar(255) DEFAULT NULL COMMENT '附件路径' AFTER `self_description`,
ADD COLUMN `attachment_name` varchar(255) DEFAULT NULL COMMENT '附件名称' AFTER `attachment_path`;
-- 更新表注释
ALTER TABLE `rs_application` COMMENT '应聘申请记录表';
@@ -0,0 +1,15 @@
-- =============================================
-- 修改 rs_overseas_application 表结构
-- 将 party_retention_file_oss_id 字段从 bigint 改为 varchar(500)
-- 以支持存储多个附件OSS ID(逗号分隔)
-- 创建时间: 2025-11-27
-- =============================================
-- 修改字段类型(MySQL/MariaDB)
ALTER TABLE rs_overseas_application
MODIFY COLUMN party_retention_file_oss_id VARCHAR(500) COMMENT '党籍保留附件OSS ID(多个ID用逗号分隔)';
-- 注意:
-- 1. 此脚本会保留现有数据,bigint值会自动转换为字符串
-- 2. 建议在执行前备份数据库
-- 3. 如果字段当前有NOT NULL约束,会被保留