企微同步,需要有租户ID

This commit is contained in:
2026-04-24 20:54:04 +08:00
parent d1ff7bde35
commit 1f7b1e6d55
13 changed files with 814 additions and 0 deletions

View File

@@ -3,13 +3,31 @@ 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.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;
@@ -17,10 +35,17 @@ import java.util.UUID;
public class QweiDepartmentServiceImpl extends ServiceImpl<QweiDepartmentMapper, QweiDepartment>
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<String, Object> add(QweiDepartment entity) {
Map<String, Object> result = new HashMap<>();
try {
String tenantId = resolveTenantId(entity.getTenantId());
if (entity.getDeptId() == null) {
result.put("success", false);
result.put("message", "deptId不能为空");
@@ -40,6 +65,7 @@ public class QweiDepartmentServiceImpl extends ServiceImpl<QweiDepartmentMapper,
}
entity.setId(UUID.randomUUID().toString());
entity.setTenantId(tenantId);
if (entity.getIsDeleted() == null) {
entity.setIsDeleted(0);
}
@@ -138,5 +164,224 @@ public class QweiDepartmentServiceImpl extends ServiceImpl<QweiDepartmentMapper,
return result;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncFromQiWei() {
Map<String, Object> result = new HashMap<>();
try {
QiWeiConfig cfg = loadQiWeiConfig();
String tenantId = resolveTenantId(null);
// 确保后续对 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<QweiDepartment> 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<QweiDepartment> q = new LambdaQueryWrapper<QweiDepartment>()
.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<DictItem> q = new LambdaQueryWrapper<DictItem>()
.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 = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?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<QweiDepartment> fetchDepartments(String token) throws Exception {
String url = "https://qyapi.weixin.qq.com/cgi-bin/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<QweiDepartment> 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");
}
}