概述

本文档记录了 JEECG 系统与 Maxkey 系统间组织与用户数据的同步方案,以及集成 LDAP 域账号认证的登录功能改造。系统通过定时任务实现从 Maxkey 数据库同步组织架构和用户信息至 JEECG 本地数据库,并支持用户通过域账号进行身份验证。

代码目录结构说明

jeecg-system-biz/src/main/java/org/jeecg/modules/system/
├── entity/
│   ├── MxkOrganizations.java
│   └── MxkUserinfo.java
├── job/
│   ├── MaxkeyDepartmentJob.java
│   └── MaxkeyUserJob.java
├── mapper/
│   ├── MxkOrganizationsMapper.java
│   └── MxkUserinfoMapper.java
└── service/
    ├── impl/
    │   ├── MxkOrganizationsServiceImpl.java
    │   └── MxkUserinfoServiceImpl.java
    ├── IMxkOrganizationsService.java
    └── IMxkUserinfoService.java

效果图

一、同步用户

功能说明
该任务定时从 Maxkey 系统拉取用户数据,同步至 JEECG 系统的 sys_user 及相关附属表(部门关联、租户绑定、职务字典等),实现用户信息的统一维护。

核心逻辑

  1. 查询 Maxkey 中所有非 admin 用户。

  2. 遍历用户列表,逐条处理:

    • 基础信息映射(用户名、姓名、邮箱、手机号等)。

    • 关联用户与部门。

    • 处理用户职务信息,并同步至职务字典。

    • 绑定用户至指定租户(ID: 1002)。

  3. 同步完成后,更新系统职务字典项。

package org.jeecg.modules.system.job;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.modules.system.entity.*;
import org.jeecg.modules.system.mapper.SysDictItemMapper;
import org.jeecg.modules.system.mapper.SysPositionMapper;
import org.jeecg.modules.system.mapper.SysUserDepartMapper;
import org.jeecg.modules.system.mapper.SysUserTenantMapper;
import org.jeecg.modules.system.service.IMxkUserinfoService;
import org.jeecg.modules.system.service.ISysDictItemService;
import org.jeecg.modules.system.service.ISysDictService;
import org.jeecg.modules.system.service.ISysUserService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @Description: Maxkey用户同步任务
 *
 * @author: liaogui
 * @date: 2026年01月14日
 */
@Slf4j
public class MaxkeyUserJob implements Job {
    @Autowired
    private IMxkUserinfoService mxkUserinfoService;

    @Autowired
    private ISysUserService sysUserService;

    @Autowired
    private SysUserDepartMapper sysUserDepartMapper;

    @Autowired
    private SysPositionMapper sysPositionMapper;

    @Autowired
    private ISysDictItemService sysDictItemService;

    @Autowired
    private SysDictItemMapper sysDictItemMapper;

    @Autowired
    private SysUserTenantMapper sysUserTenantMapper;

    private final static Integer TENANT_ID = 1002;
    // 默认:123456
    private final static String PASSWORD = "cb362cfeefbf3d8d";
    
    private final static String SALT = "RCGTeGiH";

    private final static String ADMIN = "admin";

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("--------同步Maxkey用户信息 start--------");
        List<MxkUserinfo> mxkUserinfos = mxkUserinfoService.getBaseMapper()
                .selectList(new LambdaQueryWrapper<MxkUserinfo>()
                .notIn(MxkUserinfo::getUsername, List.of(ADMIN))
                );
        AtomicInteger successCount = new AtomicInteger();
        AtomicInteger failCount = new AtomicInteger();
        mxkUserinfos.forEach(mxkUserinfo -> {
            log.info("==============当前用户信息: mxkUserinfo{} ==============", mxkUserinfo);
            if (StringUtils.isNotBlank(mxkUserinfo.getNickname())) {
                try {
                    SysUser sysUser = new SysUser();
                    sysUser.setUsername(mxkUserinfo.getUsername());
                    sysUser.setRealname(mxkUserinfo.getNickname());
                    sysUser.setPassword(PASSWORD);
                    sysUser.setSalt(SALT);
                    sysUser.setEmail(mxkUserinfo.getEmail());
                    sysUser.setPhone(mxkUserinfo.getMobile());
                    sysUser.setLoginTenantId(TENANT_ID);
                    sysUser.setOrgCode(mxkUserinfo.getDepartmentid());
                    sysUser.setBelongDepIds(mxkUserinfo.getDepartmentid());
                    sysUser.setStatus(mxkUserinfo.getUserstate().equals("RESIDENT") ? 1 : 3 );
                    sysUser.setDelFlag(0);
                    sysUser.setActivitiSync(1);
                    sysUser.setWorkNo(mxkUserinfo.getEmployeenumber());
                    sysUser.setUserIdentity(1);
                    sysUser.setSort(1000);
                    sysUser.setCreateBy(ADMIN);
                    sysUser.setSex(mxkUserinfo.getGender() == 2 ? 1 : 2);
                    SysUser one = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, mxkUserinfo.getUsername()));
                    if (one != null ){
                        sysUser.setId(one.getId());
                        sysUser.setUpdateBy(ADMIN);
                    }
                    try {
                        if (StringUtils.isNotBlank(mxkUserinfo.getDepartmentid())) {
                            // 先删除原来的关联关系
                            sysUserDepartMapper.delete(new LambdaQueryWrapper<SysUserDepart>().eq(SysUserDepart::getUserId, sysUser.getId()));
                            sysUserDepartMapper.insert(new SysUserDepart(sysUser.getId(), mxkUserinfo.getDepartmentid()));
                        }
                    } catch (Exception e) {
                        log.error("当前已存在用户与部门的关联关系");
                    }
                    // 更新职务信息表
                    if (StringUtils.isNotBlank(mxkUserinfo.getJobtitle())) {
                        SysPosition sysPosition = sysPositionMapper.selectOne(
                                new LambdaQueryWrapper<SysPosition>()
                                        .eq(SysPosition::getName, mxkUserinfo.getJobtitle()));
                        if (sysPosition == null) {
                            sysPosition = new SysPosition();
                            sysPosition.setName(mxkUserinfo.getJobtitle());
                            Long count = sysPositionMapper.selectCount(new LambdaQueryWrapper<>());
                            sysPosition.setPostLevel(count.intValue() + 1);
                            sysPositionMapper.insertOrUpdate(sysPosition);
                        }
                        sysUser.setPositionType(String.valueOf(sysPosition.getPostLevel()));
                    }
                    // 更新用户对应租户ID
                    Long tenantCount = sysUserTenantMapper.selectCount(new LambdaQueryWrapper<SysUserTenant>()
                            .eq(SysUserTenant::getUserId, sysUser.getId()));
                    if (tenantCount == 0L) {
                        // 绑定租户
                        SysUserTenant sysUserTenant = new SysUserTenant();
                        sysUserTenant.setUserId(sysUser.getId());
                        sysUserTenant.setTenantId(TENANT_ID);
                        sysUserTenant.setStatus("1");
                        sysUserTenant.setCreateBy(ADMIN);
                        sysUserTenantMapper.insert(sysUserTenant);
                    }
                    sysUserService.saveOrUpdate(sysUser);

                    successCount.getAndIncrement();
                }catch (Exception e) {
                    log.error("同步用户信息失败:{}", e.getMessage());
                    failCount.getAndIncrement();
                }
            } else {
                log.error("用户信息缺失:{}", mxkUserinfo);
            }

        });
        // 更新用户职务字典项

        // 先删除用户职务字典项
        List<SysDictItem> sysDictItems = sysDictItemService.selectItemsByMainId("1964944899916697602");
        List<String> ItemIds = sysDictItems.stream()
                .map(sysDictItem -> sysDictItem.getId()).collect(Collectors.toList());
        sysDictItemMapper.deleteByIds(ItemIds);

        List<SysPosition> sysPositions = sysPositionMapper.selectList(new LambdaQueryWrapper<>());
        List<SysDictItem> items = sysPositions.stream()
                .map(sysPosition -> {
                    SysDictItem sysDictItem = new SysDictItem();
                    sysDictItem.setDictId("1964944899916697602");
                    sysDictItem.setItemText(sysPosition.getName());
                    sysDictItem.setItemValue(String.valueOf(sysPosition.getPostLevel()));
                    sysDictItem.setDescription(sysPosition.getName());
                    sysDictItem.setSortOrder(sysPosition.getPostLevel());
                    return sysDictItem;
                })
                .collect(Collectors.toList());
        sysDictItemMapper.insert(items);

        log.info("同步Maxkey用户信息成功:{}条,失败:{}条", successCount, failCount);
    }
}

/

二、同步组织

功能说明
定时同步 Maxkey 的组织架构至 JEECG 的部门表(sys_depart),保持两边的组织树结构一致。

核心逻辑

  1. 查询 Maxkey 所有组织数据。

  2. 转换为 JEECG 部门对象,并设置父子关系(根部门 ID 为 "1" 的不设父级)。

  3. 批量保存或更新至 sys_depart 表。


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.system.entity.MxkOrganizations;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.service.IMxkOrganizationsService;
import org.jeecg.modules.system.service.ISysDepartService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description: Maxkey部门同步任务
 *
 * @author: liaogui
 * @date: 2026年01月14日
 */
@Slf4j
public class MaxkeyDepartmentJob implements Job {
    @Autowired
    private IMxkOrganizationsService mxkOrganizationsService;

    @Autowired
    private ISysDepartService sysDepartService;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        List<MxkOrganizations> list = mxkOrganizationsService.list(new LambdaQueryWrapper<MxkOrganizations>());
        List<SysDepart> sysDeparts = new ArrayList<>();
        list.forEach(mxkOrganizations -> {
            SysDepart depart = new SysDepart();
            depart.setId(mxkOrganizations.getId());
            if (!mxkOrganizations.getId().equals("1")) {
                depart.setParentId(mxkOrganizations.getParentid());
            }
            depart.setDepartName(mxkOrganizations.getFullname());
            depart.setOrgCode(mxkOrganizations.getOrgcode());
            depart.setDelFlag("0");
            sysDeparts.add(depart);
        });
        sysDepartService.saveOrUpdateBatch(sysDeparts);
        log.info("Maxkey部门同步任务执行成功");
    }
}

三、接入LDAP域账号登录

功能说明
在原有登录逻辑基础上,增加对 LDAP/AD 域账号的认证支持,实现域账号与本地账号的统一登录入口。

实现要点

  1. 工具类 LDAPUtil

    • 提供 checkLdap() 方法,用于验证域账号密码。

    • 封装 LDAP 连接、认证及异常处理。

  2. 登录接口改造

    • 在用户登录时,先尝试本地数据库验证。

    • 若本地验证失败(或根据策略),调用 LDAPUtil.checkLdap() 进行域认证。

    • 根据返回结果判断是否登录成功。

配置项

  • LDAP_URL:域控制器地址。

  • USER_EMAIL:域名后缀,用于构建用户 Principal。

package org.jeecg.common.system.util;

import lombok.extern.slf4j.Slf4j;

import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.Hashtable;

/**
 * @Description: 域账号验证工具
 */
@Slf4j
public class LDAPUtil {

    private static final String LDAP_URL = "ldap://test.com:389";//域名
    private static final String USER_EMAIL = "@test.com";

    /**
     * 创建LdapContext进行身份验证
     *
     * @param username 用户名
     * @param password 密码
     * @return 验证成功的LdapContext对象
     * @throws NamingException 如果身份验证过程中发生命名相关异常
     */
    public static LdapContext getLdapContext(String username, String password) throws NamingException {
        //创建连接
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, LDAP_URL);
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, username + USER_EMAIL);
        env.put(Context.SECURITY_CREDENTIALS, password);

        return new InitialLdapContext(env, null);
    }

    /**
     * 连接并验证LDAP服务
     *
     * @param username 用户名
     * @param password 密码
     * @return 错误消息,如果为空则表示验证成功
     */
    public static String checkLdap(String username, String password) {
        LdapContext lct = null;
        try {
            lct = getLdapContext(username, password);
        } catch (AuthenticationException e) {
            log.error("账号或密码错误!", e);
            return "账号或密码错误!";
        } catch (CommunicationException e) {
            log.error("AD域连接失败!", e);
            return "AD域连接失败!";
        } catch (NamingException e) {
            log.error("身份验证未知异常!", e);
            return "身份验证未知异常!";
        } catch (Exception e) {
            log.error("未知异常!", e);
            return "未知异常!";
        } finally {
            if (null != lct) {
                try {
                    lct.close();
                } catch (Exception e) {
                    log.error("关闭LdapContext异常!", e);
                }
            }
        }
        return "";
    }
}

如何使用?

String ck = LDAPUtil.checkLdap(username, password);
boolean ldap = StringUtils.isNotBlank(ck);

完整代码

写在结尾

“如果你遇到过类似问题或有不同解法,欢迎在评论区交流。关注 里奥圭,持续获取Java实践笔记。”