package com.rj.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.rj.common.DictItemConstants; import com.rj.common.PasswordUtil; import com.rj.common.QiWeiApiConstants; import com.rj.dto.QiWeiConfig; import com.rj.entity.DictItem; import com.rj.entity.QweiDepartment; import com.rj.mapper.DictItemMapper; import com.rj.mapper.QweiDepartmentMapper; import com.rj.service.IQweiDepartmentService; import com.rj.tenant.TenantContextHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @Service public class QweiDepartmentServiceImpl extends ServiceImpl implements IQweiDepartmentService { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); @Autowired private DictItemMapper dictItemMapper; @Override public Map add(QweiDepartment entity) { Map result = new HashMap<>(); try { String tenantId = resolveTenantId(entity.getTenantId()); if (entity.getDeptId() == null) { result.put("success", false); result.put("message", "deptId不能为空"); return result; } if (entity.getDeptName() == null || entity.getDeptName().trim().isEmpty()) { result.put("success", false); result.put("message", "deptName不能为空"); return result; } LambdaQueryWrapper dup = new LambdaQueryWrapper() .eq(QweiDepartment::getDeptId, entity.getDeptId()); if (this.count(dup) > 0) { result.put("success", false); result.put("message", "deptId已存在"); return result; } entity.setId(UUID.randomUUID().toString()); entity.setTenantId(tenantId); if (entity.getIsDeleted() == null) { entity.setIsDeleted(0); } LocalDateTime now = LocalDateTime.now(); entity.setCreatedAt(now); entity.setUpdatedAt(now); boolean ok = this.save(entity); result.put("success", ok); result.put("message", ok ? "添加成功" : "添加失败"); if (ok) { result.put("data", entity); } return result; } catch (Exception e) { result.put("success", false); result.put("message", "添加异常:" + e.getMessage()); return result; } } @Override public Map update(QweiDepartment entity) { Map result = new HashMap<>(); try { if (entity.getId() == null || entity.getId().trim().isEmpty()) { result.put("success", false); result.put("message", "id不能为空"); return result; } entity.setUpdatedAt(LocalDateTime.now()); boolean ok = this.updateById(entity); result.put("success", ok); result.put("message", ok ? "更新成功" : "更新失败"); if (ok) { result.put("data", this.getById(entity.getId())); } return result; } catch (Exception e) { result.put("success", false); result.put("message", "更新异常:" + e.getMessage()); return result; } } @Override public Map deleteById(String id) { Map result = new HashMap<>(); try { boolean ok = this.removeById(id); result.put("success", ok); result.put("message", ok ? "删除成功" : "删除失败"); return result; } catch (Exception e) { result.put("success", false); result.put("message", "删除异常:" + e.getMessage()); return result; } } @Override public Map pageQuery(Integer current, Integer size, Long deptId, String deptName, Long parentDeptId) { Map result = new HashMap<>(); try { if (current == null || current < 1) { current = 1; } if (size == null || size < 1) { size = 10; } Page page = new Page<>(current, size); LambdaQueryWrapper q = new LambdaQueryWrapper<>(); if (deptId != null) { q.eq(QweiDepartment::getDeptId, deptId); } if (deptName != null && !deptName.trim().isEmpty()) { q.like(QweiDepartment::getDeptName, deptName.trim()); } if (parentDeptId != null) { q.eq(QweiDepartment::getParentDeptId, parentDeptId); } q.orderByAsc(QweiDepartment::getOrderNum).orderByDesc(QweiDepartment::getUpdatedAt); Page data = this.page(page, q); result.put("success", true); result.put("message", "查询成功"); result.put("data", data.getRecords()); result.put("total", data.getTotal()); result.put("current", data.getCurrent()); result.put("size", data.getSize()); result.put("pages", data.getPages()); return result; } catch (Exception e) { result.put("success", false); result.put("message", "查询异常:" + e.getMessage()); return result; } } @Override @Transactional(rollbackFor = Exception.class) public Map syncFromQiWei(String tenantIdParam) { Map result = new HashMap<>(); try { QiWeiConfig cfg = loadQiWeiConfig(); String tenantId = resolveTenantId(tenantIdParam); // 确保后续对 qwei_department 的查询/写入都带 tenant_id 条件 TenantContextHolder.setTenantId(tenantId); String corpId = requireNonBlank(cfg.getCorpid(), "qiwei_config.corpid不能为空"); String contactSecret = requireNonBlank(cfg.getContractSecret(), "qiwei_config.contractSecret不能为空"); String appSecret = trimToNull(cfg.getAppSecret()); List remoteDepartments; try { String token = getAccessToken(corpId, contactSecret); remoteDepartments = fetchDepartments(token); } catch (Exception e) { String msg = e.getMessage(); boolean contactForbidden = msg != null && (msg.contains("e=48009") || msg.contains("api forbidden for contact assistant")); if (!contactForbidden || appSecret == null) { throw e; } String appToken = getAccessToken(corpId, appSecret); remoteDepartments = fetchDepartments(appToken); } int inserted = 0; int updated = 0; for (QweiDepartment dept : remoteDepartments) { dept.setTenantId(tenantId); LambdaQueryWrapper q = new LambdaQueryWrapper() .eq(QweiDepartment::getDeptId, dept.getDeptId()); QweiDepartment exists = this.getOne(q, false); LocalDateTime now = LocalDateTime.now(); if (exists == null) { dept.setId(UUID.randomUUID().toString()); dept.setIsDeleted(0); dept.setCreatedAt(now); dept.setUpdatedAt(now); try { this.save(dept); inserted++; } catch (Exception insertEx) { // 若数据库唯一索引仍是 dept_id 单列,且历史数据 tenant_id 为空,会导致不同租户插入时撞唯一键。 // 这里做一次“修复脏数据”:找到 dept_id 对应的旧记录,如果 tenant_id 为空则补齐为当前租户并更新。 if (!isDuplicateDeptId(insertEx)) { throw insertEx; } QweiDepartment legacy = findByDeptIdIgnoreTenant(dept.getDeptId()); if (legacy == null) { throw insertEx; } String legacyTenant = trimToNull(legacy.getTenantId()); if (legacyTenant == null) { this.baseMapper.updateLegacyTenantAndFieldsByIdIgnoreTenant( legacy.getId(), tenantId, dept.getDeptName(), dept.getParentDeptId(), dept.getOrderNum(), 0, now ); updated++; } else if (!tenantId.equals(legacyTenant)) { throw new IllegalStateException("部门dept_id=" + dept.getDeptId() + " 已存在且属于其他租户(" + legacyTenant + ")。请把唯一索引改为(tenant_id, dept_id)。"); } else { // 同租户但 exists 查不到(理论上不该发生),走更新 exists = legacy; exists.setDeptName(dept.getDeptName()); exists.setParentDeptId(dept.getParentDeptId()); exists.setOrderNum(dept.getOrderNum()); exists.setIsDeleted(0); exists.setUpdatedAt(now); this.updateById(exists); updated++; } } } else { exists.setDeptName(dept.getDeptName()); exists.setParentDeptId(dept.getParentDeptId()); exists.setOrderNum(dept.getOrderNum()); exists.setIsDeleted(0); exists.setUpdatedAt(now); this.updateById(exists); updated++; } } result.put("success", true); result.put("message", "同步成功"); result.put("inserted", inserted); result.put("updated", updated); result.put("total", remoteDepartments.size()); return result; } catch (Exception e) { result.put("success", false); result.put("message", "同步异常:" + e.getMessage()); return result; } finally { TenantContextHolder.clear(); } } private QiWeiConfig loadQiWeiConfig() throws Exception { // 优先使用 JVM 参数,便于联调/脚本执行: // -Dqiwei.corpid=xxx -Dqiwei.contactSecret=xxx String corpid = trimToNull(System.getProperty("qiwei.corpid")); String contactSecret = trimToNull(System.getProperty("qiwei.contactSecret")); if (corpid != null && contactSecret != null) { QiWeiConfig cfg = new QiWeiConfig(); cfg.setCorpid(corpid); cfg.setContractSecret(contactSecret); cfg.setAppSecret(trimToNull(System.getProperty("qiwei.appSecret"))); return cfg; } String tenantId = System.getProperty("tenantId", "TENANT_ID_CST_2026"); TenantContextHolder.setTenantId(tenantId); try { LambdaQueryWrapper q = new LambdaQueryWrapper() .eq(DictItem::getName, DictItemConstants.QIWEI_CONFIG) .orderByDesc(DictItem::getUpdatedTime) .orderByDesc(DictItem::getCreatedTime) .last("limit 1"); DictItem item = dictItemMapper.selectOne(q); if (item == null || item.getValue() == null || item.getValue().trim().isEmpty()) { throw new IllegalStateException("未找到 qiwei_config"); } String json = PasswordUtil.decryptReversibleWithDefaultSalt(item.getValue()); return OBJECT_MAPPER.readValue(json, QiWeiConfig.class); } finally { TenantContextHolder.clear(); } } private String getAccessToken(String corpId, String secret) throws Exception { String url = QiWeiApiConstants.GET_TOKEN + "?corpid=" + URLEncoder.encode(corpId, StandardCharsets.UTF_8) + "&corpsecret=" + URLEncoder.encode(secret, StandardCharsets.UTF_8); HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build(); String body = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()).body(); JsonNode json = OBJECT_MAPPER.readTree(body); int errCode = json.path("errcode").asInt(-1); if (errCode != 0) { throw new IllegalStateException("获取access_token失败: " + body); } return json.path("access_token").asText(); } private List fetchDepartments(String token) throws Exception { String url = QiWeiApiConstants.DEPARTMENT_LIST + "?access_token=" + URLEncoder.encode(token, StandardCharsets.UTF_8); HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build(); String body = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()).body(); JsonNode json = OBJECT_MAPPER.readTree(body); int errCode = json.path("errcode").asInt(-1); if (errCode != 0) { throw new IllegalStateException("获取部门列表失败: " + body); } List list = new ArrayList<>(); JsonNode departmentNode = json.path("department"); if (departmentNode.isArray()) { for (JsonNode n : departmentNode) { QweiDepartment dept = new QweiDepartment(); dept.setDeptId(n.path("id").asLong()); dept.setDeptName(n.path("name").asText(null)); dept.setParentDeptId(n.path("parentid").asLong()); dept.setOrderNum(n.path("order").asInt(0)); list.add(dept); } } return list; } private String requireNonBlank(String value, String msg) { if (value == null || value.trim().isEmpty()) { throw new IllegalStateException(msg); } return value.trim(); } private boolean isDuplicateDeptId(Exception e) { String msg = e.getMessage(); return msg != null && (msg.contains("Duplicate entry") && msg.contains("uk_qwei_department_dept_id")); } private QweiDepartment findByDeptIdIgnoreTenant(Long deptId) { return this.baseMapper.selectByDeptIdIgnoreTenant(deptId); } private String trimToNull(String value) { if (value == null) { return null; } String t = value.trim(); return t.isEmpty() ? null : t; } private String resolveTenantId(String tenantIdFromEntity) { String current = trimToNull(TenantContextHolder.getTenantId()); if (current != null) { return current; } String t = trimToNull(tenantIdFromEntity); if (t != null) { return t; } String fromProp = trimToNull(System.getProperty("tenantId")); if (fromProp != null) { return fromProp; } throw new IllegalStateException("tenantId不能为空(请设置租户上下文或传入 tenantId 或 -DtenantId)"); } }