diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java b/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java index b1c55e8..f6fac90 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java @@ -62,11 +62,17 @@ public class HrmMyApplyController extends BaseController { if (currentUserId == null) { return TableDataInfo.build(); } - HrmEmployee emp = employeeMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(HrmEmployee::getUserId, currentUserId)); - if (emp == null) { + // 极端场景一个 sys_user 可能对应多条员工记录(历史孤儿/副本没清干净), + // 用 list + limit 1 兜底,别用 selectOne 直接抛异常。 + List empList = employeeMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(HrmEmployee::getUserId, currentUserId) + .orderByAsc(HrmEmployee::getEmpId) + .last("limit 1")); + if (empList.isEmpty()) { return TableDataInfo.build(); } + HrmEmployee emp = empList.get(0); SysUser sysUser = sysUserMapper.selectUserById(emp.getUserId()); String nickName = sysUser != null ? sysUser.getNickName() : null; diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/mapper/HrmEmployeeMapper.java b/fad-hrm/src/main/java/com/ruoyi/hrm/mapper/HrmEmployeeMapper.java index 2bf8d4f..e9ac2a8 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/mapper/HrmEmployeeMapper.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/mapper/HrmEmployeeMapper.java @@ -1,8 +1,33 @@ package com.ruoyi.hrm.mapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.core.mapper.BaseMapperPlus; import com.ruoyi.hrm.domain.HrmEmployee; +import com.ruoyi.hrm.domain.bo.HrmEmployeeBo; import com.ruoyi.hrm.domain.vo.HrmEmployeeVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; public interface HrmEmployeeMapper extends BaseMapperPlus { + + /** + * 按 empId 查询员工详情。JOIN sys_dept / sys_post 补上 dept_name / post_name。 + *

+ * 注意:不能沿用父接口的 selectVoById,那是 default 方法先跑 selectById 再 BeanCopy, + * 拿不到 JOIN 出来的 dept_name / post_name。 + */ + HrmEmployeeVo selectEmployeeVoById(@Param("empId") Long empId); + + /** + * 员工列表(含 dept / post 名称)。传 BO,自己在 XML 里拼过滤条件, + * 别用 LambdaQueryWrapper——JOIN 里字段名会跟 sys_user / sys_dept 冲突。 + */ + List selectEmployeeVoList(@Param("bo") HrmEmployeeBo bo); + + /** + * 员工分页(含 dept / post 名称)。 + */ + Page selectEmployeeVoPage(IPage page, @Param("bo") HrmEmployeeBo bo); } diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmEmployeeServiceImpl.java b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmEmployeeServiceImpl.java index 256c504..c2c3474 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmEmployeeServiceImpl.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmEmployeeServiceImpl.java @@ -1,7 +1,6 @@ package com.ruoyi.hrm.service.impl; import cn.hutool.core.bean.BeanUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.common.core.domain.PageQuery; @@ -26,25 +25,30 @@ public class HrmEmployeeServiceImpl implements IHrmEmployeeService { @Override public HrmEmployeeVo queryById(Long empId) { - return baseMapper.selectVoById(empId); + // 走 JOIN 版本,返回带 deptName/postName 的完整 VO + return baseMapper.selectEmployeeVoById(empId); } @Override public HrmEmployeeVo queryByUserId(Long userId) { - return baseMapper.selectVoOne(Wrappers.lambdaQuery().eq(HrmEmployee::getUserId, userId)); + // 同一个 sys_user 极端情况可能对应多条员工,取 empId 最小的一条兜底 + HrmEmployeeBo bo = new HrmEmployeeBo(); + bo.setUserId(userId); + List list = baseMapper.selectEmployeeVoList(bo); + return list.isEmpty() ? null : list.get(0); } @Override public TableDataInfo queryPageList(HrmEmployeeBo bo, PageQuery pageQuery) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + // 显式调用 XML 里带 JOIN 的自定义方法,父接口的 selectVoPage default 只会走单表 selectPage, + // 拿不到 dept_name / post_name。 + Page result = baseMapper.selectEmployeeVoPage(pageQuery.build(), bo); return TableDataInfo.build(result); } @Override public List queryList(HrmEmployeeBo bo) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoList(lqw); + return baseMapper.selectEmployeeVoList(bo); } @Override @@ -70,17 +74,4 @@ public class HrmEmployeeServiceImpl implements IHrmEmployeeService { public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { return baseMapper.deleteBatchIds(ids) > 0; } - - private LambdaQueryWrapper buildQueryWrapper(HrmEmployeeBo bo) { - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(bo.getEmpId() != null, HrmEmployee::getEmpId, bo.getEmpId()); - lqw.eq(bo.getUserId() != null, HrmEmployee::getUserId, bo.getUserId()); - lqw.like(bo.getEmpNo() != null, HrmEmployee::getEmpNo, bo.getEmpNo()); - lqw.like(bo.getEmpName() != null, HrmEmployee::getEmpName, bo.getEmpName()); - lqw.eq(bo.getStatus() != null, HrmEmployee::getStatus, bo.getStatus()); - lqw.eq(bo.getDeptId() != null, HrmEmployee::getDeptId, bo.getDeptId()); - lqw.eq(bo.getPostId() != null, HrmEmployee::getPostId, bo.getPostId()); - lqw.orderByAsc(HrmEmployee::getEmpNo); - return lqw; - } } diff --git a/fad-hrm/src/main/resources/mapper/HrmEmployeeMapper.xml b/fad-hrm/src/main/resources/mapper/HrmEmployeeMapper.xml index e5338fd..1448161 100644 --- a/fad-hrm/src/main/resources/mapper/HrmEmployeeMapper.xml +++ b/fad-hrm/src/main/resources/mapper/HrmEmployeeMapper.xml @@ -48,6 +48,7 @@ + SELECT e.emp_id, e.user_id, e.emp_no, e.emp_name, e.gender, e.mobile, e.email, e.id_no, @@ -59,22 +60,46 @@ p.post_name FROM hrm_employee e LEFT JOIN sys_user su ON su.user_id = e.user_id - LEFT JOIN sys_dept d ON d.dept_id = COALESCE(e.dept_id, su.dept_id) AND d.del_flag = 0 + LEFT JOIN sys_dept d ON d.dept_id = COALESCE(e.dept_id, su.dept_id) AND d.del_flag = '0' LEFT JOIN sys_post p ON e.post_id = p.post_id WHERE e.del_flag = 0 - - + + + AND e.emp_id = #{bo.empId} + AND e.user_id = #{bo.userId} + + AND e.emp_no LIKE CONCAT('%', #{bo.empNo}, '%') + + + AND (e.emp_name LIKE CONCAT('%', #{bo.empName}, '%') + OR e.emp_no LIKE CONCAT('%', #{bo.empName}, '%')) + + + AND e.status = #{bo.status} + + + AND COALESCE(e.dept_id, su.dept_id) = #{bo.deptId} + + AND e.post_id = #{bo.postId} + + - - - AND e.emp_id = #{empId} + + + + diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/task/EmployeeUserLinkJob.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/task/EmployeeUserLinkJob.java new file mode 100644 index 0000000..0f5f481 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/task/EmployeeUserLinkJob.java @@ -0,0 +1,136 @@ +package com.ruoyi.oa.task; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.hrm.domain.HrmEmployee; +import com.ruoyi.hrm.mapper.HrmEmployeeMapper; +import com.ruoyi.system.mapper.SysUserMapper; +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 每日自动把员工挂到对应的 sys_user。 + * + * 匹配规则(保守,宁可漏挂不错挂): + * 1. hrm_employee.user_id 已指向存在的 sys_user 且 nick_name/user_name 与 emp_name 一致 → 跳过 + * 2. 否则按 emp_name 找候选:先看 nick_name,再看 user_name + * 3. 候选唯一 + 手机号也对上 → 自动写 user_id(含孤儿修复) + * 4. 其余情况打日志报告,交人工处理 + * + * xxl-job 调度中心 JobHandler 名称:{@code employeeUserLinkJob} + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class EmployeeUserLinkJob { + + private final HrmEmployeeMapper employeeMapper; + private final SysUserMapper userMapper; + + @XxlJob("employeeUserLinkJob") + public void run() { + List emps = employeeMapper.selectList(Wrappers.lambdaQuery() + .eq(HrmEmployee::getDelFlag, 0)); + List users = userMapper.selectList(Wrappers.lambdaQuery() + .eq(SysUser::getDelFlag, "0")); + + Map byUid = users.stream() + .collect(Collectors.toMap(SysUser::getUserId, u -> u, (a, b) -> a)); + + // 姓名 → 候选。同名可能多人,用 List 装 + Map> byNick = new HashMap<>(); + Map> byUname = new HashMap<>(); + for (SysUser u : users) { + if (u.getNickName() != null) { + byNick.computeIfAbsent(u.getNickName().trim(), k -> new java.util.ArrayList<>()).add(u); + } + if (u.getUserName() != null) { + byUname.computeIfAbsent(u.getUserName().trim(), k -> new java.util.ArrayList<>()).add(u); + } + } + + int skip = 0, autoFix = 0, needManual = 0; + for (HrmEmployee e : emps) { + SysUser current = e.getUserId() == null ? null : byUid.get(e.getUserId()); + if (current != null && nameMatches(current, e.getEmpName())) { + skip++; + continue; + } + + List candidates = byNick.getOrDefault(e.getEmpName().trim(), java.util.Collections.emptyList()); + if (candidates.isEmpty()) { + candidates = byUname.getOrDefault(e.getEmpName().trim(), java.util.Collections.emptyList()); + } + + SysUser picked = pickByPhone(candidates, e.getMobile()); + if (picked == null) { + XxlJobHelper.log("[需人工] emp_id={} emp_name={} current_user_id={} 候选={} —— " + + "候选唯一且手机匹配才能自动挂", + e.getEmpId(), e.getEmpName(), e.getUserId(), + candidates.stream().map(SysUser::getUserId).collect(Collectors.toList())); + needManual++; + continue; + } + if (Objects.equals(picked.getUserId(), e.getUserId())) { + // 姓名判定失败但 user_id 恰好指向匹配用户;一致跳过 + skip++; + continue; + } + + HrmEmployee update = new HrmEmployee(); + update.setEmpId(e.getEmpId()); + update.setUserId(picked.getUserId()); + employeeMapper.updateById(update); + XxlJobHelper.log("[已挂] emp_id={} emp_name={} user_id: {} → {} ({} / {})", + e.getEmpId(), e.getEmpName(), e.getUserId(), picked.getUserId(), + picked.getNickName(), picked.getUserName()); + autoFix++; + } + + String msg = String.format("员工-用户关联体检完成:总数=%d,跳过=%d,自动修=%d,需人工=%d", + emps.size(), skip, autoFix, needManual); + XxlJobHelper.log(msg); + log.info(msg); + } + + /** + * 姓名判定:sys_user.nick_name 或 user_name 与员工 emp_name 完全一致 + */ + private static boolean nameMatches(SysUser u, String empName) { + if (empName == null) return false; + String t = empName.trim(); + return t.equals(safeTrim(u.getNickName())) || t.equals(safeTrim(u.getUserName())); + } + + /** + * 候选里挑一个:唯一 + 手机号匹配。其余场景返回 null,交人工。 + */ + private static SysUser pickByPhone(List candidates, String mobile) { + if (candidates == null || candidates.isEmpty()) return null; + if (mobile == null || mobile.trim().isEmpty()) { + // 无手机号无法二次确认,只允许候选唯一时挂 + return candidates.size() == 1 ? candidates.get(0) : null; + } + String m = mobile.trim(); + List phoneHits = candidates.stream() + .filter(u -> m.equals(safeTrim(u.getPhonenumber()))) + .collect(Collectors.toList()); + if (phoneHits.size() == 1) return phoneHits.get(0); + // 手机号没能唯一定位:若候选只有一个且姓名对上,也允许挂(无手机号情况) + if (phoneHits.isEmpty() && candidates.size() == 1) return candidates.get(0); + return null; + } + + private static String safeTrim(String s) { + return s == null ? "" : s.trim(); + } +} diff --git a/sql/xxl_job_employee_user_link.md b/sql/xxl_job_employee_user_link.md new file mode 100644 index 0000000..3dd2e8a --- /dev/null +++ b/sql/xxl_job_employee_user_link.md @@ -0,0 +1,46 @@ +# xxl-job 任务:员工↔用户自动关联 + +## 部署后一次性操作 + +登录 xxl-job 调度中心(`http://49.232.154.205:xxxx/xxl-job-admin`,用户名 `admin`/`123456` 是默认,见 `ruoyi-extend/ruoyi-xxl-job-admin/`),执行者组选到本项目对应的执行器(一般叫 `xxl-job-executor-ruoyi`),然后: + +**新增任务:** + +| 字段 | 值 | +|---|---| +| 执行器 | xxl-job-executor-ruoyi(或本项目对应组) | +| 任务描述 | 员工↔用户自动关联体检 | +| 调度类型 | CRON | +| Cron | `0 0 3 * * ?` (每天 03:00 执行一次) | +| 运行模式 | BEAN | +| JobHandler | `employeeUserLinkJob` | +| 路由策略 | FIRST | +| 阻塞处理策略 | 单机串行 | +| 任务超时时间 | 300 秒 | +| 失败重试次数 | 0 | + +保存后点「启动」。 + +## 运行逻辑 + +- 扫全部 `hrm_employee.del_flag=0` +- 已挂到有效 sys_user 且姓名对得上 → 跳过 +- 否则按 `emp_name` 匹配 `sys_user.nick_name` (兜底 `user_name`): + - 候选唯一 + 手机号一致 → 自动挂 + - 候选唯一 + 手机号为空 → 也挂 + - 多个候选靠手机号唯一定位 → 挂 + - 其余情况 → 打「需人工」日志,不动数据 + +## 观察 + +xxl-job 调度中心「调度日志」里能看到每次执行的 stdout: + +``` +[已挂] emp_id=xxx emp_name=张三 user_id: null → 123456 (张三 / zhangsan) +[需人工] emp_id=xxx emp_name=胡雪姣 current_user_id=1859249502579310593 候选=[] —— 候选唯一且手机匹配才能自动挂 +员工-用户关联体检完成:总数=54,跳过=53,自动修=0,需人工=1 +``` + +## 手动触发 + +在 xxl-job 后台可以点「执行一次」立即跑,不用等 03:00。