添加员工管理自动导入能力,修复了部门,岗位未显示
This commit is contained in:
@@ -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<HrmEmployee>()
|
||||
.eq(HrmEmployee::getUserId, currentUserId));
|
||||
if (emp == null) {
|
||||
// 极端场景一个 sys_user 可能对应多条员工记录(历史孤儿/副本没清干净),
|
||||
// 用 list + limit 1 兜底,别用 selectOne 直接抛异常。
|
||||
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();
|
||||
}
|
||||
HrmEmployee emp = empList.get(0);
|
||||
SysUser sysUser = sysUserMapper.selectUserById(emp.getUserId());
|
||||
String nickName = sysUser != null ? sysUser.getNickName() : null;
|
||||
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
@@ -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.<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
|
||||
public TableDataInfo<HrmEmployeeVo> queryPageList(HrmEmployeeBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<HrmEmployee> lqw = buildQueryWrapper(bo);
|
||||
Page<HrmEmployeeVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
// 显式调用 XML 里带 JOIN 的自定义方法,父接口的 selectVoPage default 只会走单表 selectPage,
|
||||
// 拿不到 dept_name / post_name。
|
||||
Page<HrmEmployeeVo> result = baseMapper.selectEmployeeVoPage(pageQuery.build(), bo);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HrmEmployeeVo> queryList(HrmEmployeeBo bo) {
|
||||
LambdaQueryWrapper<HrmEmployee> 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<Long> ids, Boolean isValid) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 基础 SELECT + JOIN。dept_id 兼容员工表未维护部门时兜底到 sys_user.dept_id -->
|
||||
<sql id="selectVo">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<!-- 使用 JOIN 查询的 select 语句,确保使用 VoResultMap -->
|
||||
<select id="selectVoPage" resultMap="VoResultMap">
|
||||
<include refid="selectVo"/>
|
||||
</select>
|
||||
<sql id="whereBo">
|
||||
<if test="bo != null">
|
||||
<if test="bo.empId != null"> AND e.emp_id = #{bo.empId} </if>
|
||||
<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">
|
||||
<include refid="selectVo"/>
|
||||
</select>
|
||||
|
||||
<select id="selectVoById" resultMap="VoResultMap">
|
||||
<select id="selectEmployeeVoById" resultMap="VoResultMap">
|
||||
<include refid="selectVo"/>
|
||||
AND e.emp_id = #{empId}
|
||||
</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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
46
sql/xxl_job_employee_user_link.md
Normal file
46
sql/xxl_job_employee_user_link.md
Normal 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。
|
||||
Reference in New Issue
Block a user