添加员工管理自动导入能力,修复了部门,岗位未显示

This commit is contained in:
2026-07-08 18:49:37 +08:00
parent 349f06b22b
commit 185231948c
6 changed files with 262 additions and 33 deletions

View File

@@ -62,11 +62,17 @@ public class HrmMyApplyController extends BaseController {
if (currentUserId == null) { if (currentUserId == null) {
return TableDataInfo.build(); return TableDataInfo.build();
} }
HrmEmployee emp = employeeMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<HrmEmployee>() // 极端场景一个 sys_user 可能对应多条员工记录(历史孤儿/副本没清干净),
.eq(HrmEmployee::getUserId, currentUserId)); // 用 list + limit 1 兜底,别用 selectOne 直接抛异常。
if (emp == null) { List<HrmEmployee> empList = employeeMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<HrmEmployee>()
.eq(HrmEmployee::getUserId, currentUserId)
.orderByAsc(HrmEmployee::getEmpId)
.last("limit 1"));
if (empList.isEmpty()) {
return TableDataInfo.build(); return TableDataInfo.build();
} }
HrmEmployee emp = empList.get(0);
SysUser sysUser = sysUserMapper.selectUserById(emp.getUserId()); SysUser sysUser = sysUserMapper.selectUserById(emp.getUserId());
String nickName = sysUser != null ? sysUser.getNickName() : null; String nickName = sysUser != null ? sysUser.getNickName() : null;

View File

@@ -1,8 +1,33 @@
package com.ruoyi.hrm.mapper; 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.common.core.mapper.BaseMapperPlus;
import com.ruoyi.hrm.domain.HrmEmployee; import com.ruoyi.hrm.domain.HrmEmployee;
import com.ruoyi.hrm.domain.bo.HrmEmployeeBo;
import com.ruoyi.hrm.domain.vo.HrmEmployeeVo; import com.ruoyi.hrm.domain.vo.HrmEmployeeVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface HrmEmployeeMapper extends BaseMapperPlus<HrmEmployeeMapper, HrmEmployee, HrmEmployeeVo> { public interface HrmEmployeeMapper extends BaseMapperPlus<HrmEmployeeMapper, HrmEmployee, HrmEmployeeVo> {
/**
* 按 empId 查询员工详情。JOIN sys_dept / sys_post 补上 dept_name / post_name。
* <p>
* 注意:不能沿用父接口的 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<HrmEmployeeVo> selectEmployeeVoList(@Param("bo") HrmEmployeeBo bo);
/**
* 员工分页(含 dept / post 名称)。
*/
Page<HrmEmployeeVo> selectEmployeeVoPage(IPage<HrmEmployee> page, @Param("bo") HrmEmployeeBo bo);
} }

View File

@@ -1,7 +1,6 @@
package com.ruoyi.hrm.service.impl; package com.ruoyi.hrm.service.impl;
import cn.hutool.core.bean.BeanUtil; 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.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.domain.PageQuery;
@@ -26,25 +25,30 @@ public class HrmEmployeeServiceImpl implements IHrmEmployeeService {
@Override @Override
public HrmEmployeeVo queryById(Long empId) { public HrmEmployeeVo queryById(Long empId) {
return baseMapper.selectVoById(empId); // 走 JOIN 版本,返回带 deptName/postName 的完整 VO
return baseMapper.selectEmployeeVoById(empId);
} }
@Override @Override
public HrmEmployeeVo queryByUserId(Long userId) { public HrmEmployeeVo queryByUserId(Long userId) {
return baseMapper.selectVoOne(Wrappers.<HrmEmployee>lambdaQuery().eq(HrmEmployee::getUserId, userId)); // 同一个 sys_user 极端情况可能对应多条员工,取 empId 最小的一条兜底
HrmEmployeeBo bo = new HrmEmployeeBo();
bo.setUserId(userId);
List<HrmEmployeeVo> list = baseMapper.selectEmployeeVoList(bo);
return list.isEmpty() ? null : list.get(0);
} }
@Override @Override
public TableDataInfo<HrmEmployeeVo> queryPageList(HrmEmployeeBo bo, PageQuery pageQuery) { public TableDataInfo<HrmEmployeeVo> queryPageList(HrmEmployeeBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<HrmEmployee> lqw = buildQueryWrapper(bo); // 显式调用 XML 里带 JOIN 的自定义方法,父接口的 selectVoPage default 只会走单表 selectPage
Page<HrmEmployeeVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw); // 拿不到 dept_name / post_name。
Page<HrmEmployeeVo> result = baseMapper.selectEmployeeVoPage(pageQuery.build(), bo);
return TableDataInfo.build(result); return TableDataInfo.build(result);
} }
@Override @Override
public List<HrmEmployeeVo> queryList(HrmEmployeeBo bo) { public List<HrmEmployeeVo> queryList(HrmEmployeeBo bo) {
LambdaQueryWrapper<HrmEmployee> lqw = buildQueryWrapper(bo); return baseMapper.selectEmployeeVoList(bo);
return baseMapper.selectVoList(lqw);
} }
@Override @Override
@@ -70,17 +74,4 @@ public class HrmEmployeeServiceImpl implements IHrmEmployeeService {
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) { public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
return baseMapper.deleteBatchIds(ids) > 0; return baseMapper.deleteBatchIds(ids) > 0;
} }
private LambdaQueryWrapper<HrmEmployee> buildQueryWrapper(HrmEmployeeBo bo) {
LambdaQueryWrapper<HrmEmployee> 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;
}
} }

View File

@@ -48,6 +48,7 @@
<result column="update_time" property="updateTime"/> <result column="update_time" property="updateTime"/>
</resultMap> </resultMap>
<!-- 基础 SELECT + JOIN。dept_id 兼容员工表未维护部门时兜底到 sys_user.dept_id -->
<sql id="selectVo"> <sql id="selectVo">
SELECT SELECT
e.emp_id, e.user_id, e.emp_no, e.emp_name, e.gender, e.mobile, e.email, e.id_no, 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 p.post_name
FROM hrm_employee e FROM hrm_employee e
LEFT JOIN sys_user su ON su.user_id = e.user_id 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 LEFT JOIN sys_post p ON e.post_id = p.post_id
WHERE e.del_flag = 0 WHERE e.del_flag = 0
</sql> </sql>
<!-- 使用 JOIN 查询的 select 语句,确保使用 VoResultMap --> <sql id="whereBo">
<select id="selectVoPage" resultMap="VoResultMap"> <if test="bo != null">
<include refid="selectVo"/> <if test="bo.empId != null"> AND e.emp_id = #{bo.empId} </if>
</select> <if test="bo.userId != null"> AND e.user_id = #{bo.userId} </if>
<if test="bo.empNo != null and bo.empNo != ''">
AND e.emp_no LIKE CONCAT('%', #{bo.empNo}, '%')
</if>
<if test="bo.empName != null and bo.empName != ''">
AND (e.emp_name LIKE CONCAT('%', #{bo.empName}, '%')
OR e.emp_no LIKE CONCAT('%', #{bo.empName}, '%'))
</if>
<if test="bo.status != null and bo.status != ''">
AND e.status = #{bo.status}
</if>
<if test="bo.deptId != null">
AND COALESCE(e.dept_id, su.dept_id) = #{bo.deptId}
</if>
<if test="bo.postId != null"> AND e.post_id = #{bo.postId} </if>
</if>
</sql>
<select id="selectVoList" resultMap="VoResultMap"> <select id="selectEmployeeVoById" resultMap="VoResultMap">
<include refid="selectVo"/>
</select>
<select id="selectVoById" resultMap="VoResultMap">
<include refid="selectVo"/> <include refid="selectVo"/>
AND e.emp_id = #{empId} AND e.emp_id = #{empId}
</select> </select>
<select id="selectEmployeeVoList" resultMap="VoResultMap">
<include refid="selectVo"/>
<include refid="whereBo"/>
ORDER BY e.emp_no
</select>
<select id="selectEmployeeVoPage" resultMap="VoResultMap">
<include refid="selectVo"/>
<include refid="whereBo"/>
ORDER BY e.emp_no
</select>
</mapper> </mapper>

View File

@@ -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<HrmEmployee> emps = employeeMapper.selectList(Wrappers.<HrmEmployee>lambdaQuery()
.eq(HrmEmployee::getDelFlag, 0));
List<SysUser> users = userMapper.selectList(Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getDelFlag, "0"));
Map<Long, SysUser> byUid = users.stream()
.collect(Collectors.toMap(SysUser::getUserId, u -> u, (a, b) -> a));
// 姓名 → 候选。同名可能多人,用 List 装
Map<String, List<SysUser>> byNick = new HashMap<>();
Map<String, List<SysUser>> 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<SysUser> 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<SysUser> 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<SysUser> 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();
}
}

View File

@@ -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。