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 cn.felord.DefaultAgent; import cn.felord.WeComTokenCacheable; import cn.felord.api.ExternalContactUserApi; import cn.felord.api.WorkWeChatApi; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; 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.CustomerManagement; import com.rj.entity.QweiUser; import com.rj.mapper.DictItemMapper; import com.rj.mapper.QweiUserMapper; import com.rj.service.ICustomerManagementService; import com.rj.service.IQweiUserService; import com.rj.tenant.TenantContextHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import okhttp3.logging.HttpLoggingInterceptor; 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.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @Service public class QweiUserServiceImpl extends ServiceImpl implements IQweiUserService { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); @Autowired private DictItemMapper dictItemMapper; @Autowired private ICustomerManagementService customerManagementService; @Override public Map add(QweiUser entity) { Map result = new HashMap<>(); try { String tenantId = resolveTenantId(entity.getTenantId()); if (entity.getUserid() == null || entity.getUserid().trim().isEmpty()) { result.put("success", false); result.put("message", "userid不能为空"); return result; } LambdaQueryWrapper dup = new LambdaQueryWrapper() .eq(QweiUser::getUserid, entity.getUserid().trim()); if (this.count(dup) > 0) { result.put("success", false); result.put("message", "userid已存在"); return result; } entity.setId(UUID.randomUUID().toString()); entity.setTenantId(tenantId); entity.setUserid(entity.getUserid().trim()); 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(QweiUser entity) { Map result = new HashMap<>(); try { if (entity.getId() == null || entity.getId().trim().isEmpty()) { result.put("success", false); result.put("message", "id不能为空"); return result; } // 更新时不允许把 tenantId 改掉;若传了就忽略 entity.setTenantId(null); if (entity.getUserid() != null) { entity.setUserid(entity.getUserid().trim()); } 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, String userid, String userName, Long mainDepartmentId, Integer status) { 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 (userid != null && !userid.trim().isEmpty()) { q.eq(QweiUser::getUserid, userid.trim()); } if (userName != null && !userName.trim().isEmpty()) { q.like(QweiUser::getUserName, userName.trim()); } if (mainDepartmentId != null) { q.eq(QweiUser::getMainDepartmentId, mainDepartmentId); } if (status != null) { q.eq(QweiUser::getStatus, status); } q.orderByDesc(QweiUser::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<>(); String tenantId = null; try { tenantId = resolveTenantId(tenantIdParam); TenantContextHolder.setTenantId(tenantId); QiWeiConfig cfg = loadQiWeiConfig(); String corpId = requireNonBlank(cfg.getCorpid(), "qiwei_config.corpid不能为空"); String contactSecret = requireNonBlank(cfg.getContractSecret(), "qiwei_config.contractSecret不能为空"); String appSecret = trimToNull(cfg.getAppSecret()); if (appSecret == null) { throw new IllegalStateException("qiwei_config.appSecret不能为空(user/get 需要 appSecret)"); } String contactToken = getAccessToken(corpId, contactSecret); String appToken = getAccessToken(corpId, appSecret); int inserted = 0; int updated = 0; String cursor = ""; int limit = 100; while (true) { JsonNode listId = fetchUserListId(contactToken, cursor, limit); JsonNode deptUser = listId.path("dept_user"); if (deptUser.isArray()) { for (JsonNode du : deptUser) { String userid = du.path("userid").asText(null); if (userid == null || userid.isBlank()) { continue; } QweiUser mapped = fetchAndMapUser(appToken, userid.trim(), tenantId); if (upsertUser(mapped, tenantId)) { inserted++; } else { updated++; } } } JsonNode nextCursor = listId.get("next_cursor"); if (nextCursor == null || nextCursor.isNull()) { break; } String nc = nextCursor.asText(""); if (nc.isBlank()) { break; } cursor = nc; } result.put("success", true); result.put("message", "同步成功"); result.put("inserted", inserted); result.put("updated", updated); result.put("tenantId", tenantId); return result; } catch (Exception e) { result.put("success", false); result.put("message", "同步异常:" + e.getMessage()); if (tenantId != null) { result.put("tenantId", tenantId); } return result; } finally { TenantContextHolder.clear(); } } @Override @Transactional(rollbackFor = Exception.class) public Map syncCustomerFromQiWei(String tenantIdParam) { Map result = new HashMap<>(); String tenantId = null; try { tenantId = resolveTenantId(tenantIdParam); TenantContextHolder.setTenantId(tenantId); QiWeiConfig cfg = loadQiWeiConfig(); String corpId = requireNonBlank(cfg.getCorpid(), "qiwei_config.corpid不能为空"); String externalContactSecret = resolveExternalContactSecret(cfg); String contactToken = getAccessToken(corpId, externalContactSecret); ExternalContactUserApi externalContactUserApi = createExternalContactUserApi(corpId, externalContactSecret); int inserted = 0; int updated = 0; JsonNode followUsers = fetchCustomerContactFollowUsers(contactToken); printFollowUsersToConsole(followUsers); if (followUsers.isArray()) { for (JsonNode followUser : followUsers) { try { String followUserId = trimToNull(followUser.asText(null)); if (followUserId == null) { continue; } SyncCounter counter = syncCustomerByFollowUser(externalContactUserApi, contactToken, followUserId, tenantId); inserted += counter.inserted; updated += counter.updated; }catch (Exception e) { log.error("syncCustomerFromQiWei: {}", e); e.printStackTrace(); } } } result.put("success", true); result.put("message", "同步企微客户成功"); result.put("inserted", inserted); result.put("updated", updated); result.put("total", inserted + updated); result.put("tenantId", tenantId); return result; } catch (Exception e) { result.put("success", false); result.put("message", "同步企微客户异常:" + e.getMessage()); if (tenantId != null) { result.put("tenantId", tenantId); } return result; } finally { TenantContextHolder.clear(); } } private boolean upsertUser(QweiUser user, String tenantId) { LambdaQueryWrapper q = new LambdaQueryWrapper() .eq(QweiUser::getUserid, user.getUserid()); QweiUser exists = this.getOne(q, false); LocalDateTime now = LocalDateTime.now(); if (exists == null) { user.setId(UUID.randomUUID().toString()); user.setTenantId(tenantId); user.setIsDeleted(0); user.setCreatedAt(now); user.setUpdatedAt(now); try { this.save(user); return true; } catch (Exception insertEx) { // 若唯一索引仍是 userid 单列,且历史数据 tenant_id 为空,会导致撞唯一键 if (!isDuplicateUserid(insertEx)) { throw insertEx; } QweiUser legacy = this.baseMapper.selectByUseridIgnoreTenant(user.getUserid()); if (legacy == null) { throw insertEx; } String legacyTenant = trimToNull(legacy.getTenantId()); if (legacyTenant == null) { this.baseMapper.updateLegacyTenantAndFieldsByIdIgnoreTenant( legacy.getId(), tenantId, user.getUserName(), user.getMobile(), user.getEmail(), user.getBizEmail(), user.getTelephone(), user.getPosition(), user.getGender(), user.getStatus(), user.getMainDepartmentId(), user.getDepartmentIdsJson(), user.getAliasName(), user.getAvatar(), user.getThumbAvatar(), user.getAddress(), 0, now ); return false; } if (!tenantId.equals(legacyTenant)) { throw new IllegalStateException("用户userid=" + user.getUserid() + " 已存在且属于其他租户(" + legacyTenant + ")。请把唯一索引改为(tenant_id, userid)。"); } // 同租户兜底更新 exists = legacy; } } exists.setTenantId(null); // 不允许改租户 exists.setUserName(user.getUserName()); exists.setMobile(user.getMobile()); exists.setEmail(user.getEmail()); exists.setBizEmail(user.getBizEmail()); exists.setTelephone(user.getTelephone()); exists.setPosition(user.getPosition()); exists.setGender(user.getGender()); exists.setStatus(user.getStatus()); exists.setMainDepartmentId(user.getMainDepartmentId()); exists.setDepartmentIdsJson(user.getDepartmentIdsJson()); exists.setAliasName(user.getAliasName()); exists.setAvatar(user.getAvatar()); exists.setThumbAvatar(user.getThumbAvatar()); exists.setAddress(user.getAddress()); exists.setIsDeleted(0); exists.setUpdatedAt(now); this.updateById(exists); return false; } private QweiUser fetchAndMapUser(String appToken, String userid, String tenantId) throws Exception { String url = QiWeiApiConstants.USER_GET + "?access_token=" + URLEncoder.encode(appToken, StandardCharsets.UTF_8) + "&userid=" + URLEncoder.encode(userid, 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); } QweiUser user = new QweiUser(); user.setTenantId(tenantId); user.setUserid(userid); user.setUserName(json.path("name").asText(null)); user.setMobile(json.path("mobile").asText(null)); user.setEmail(json.path("email").asText(null)); user.setBizEmail(json.path("biz_email").asText(null)); user.setTelephone(json.path("telephone").asText(null)); user.setPosition(json.path("position").asText(null)); user.setGender(json.hasNonNull("gender") ? json.path("gender").asInt() : null); user.setStatus(json.hasNonNull("status") ? json.path("status").asInt() : null); user.setAvatar(json.path("avatar").asText(null)); user.setThumbAvatar(json.path("thumb_avatar").asText(null)); user.setAliasName(json.path("alias").asText(null)); user.setAddress(json.path("address").asText(null)); user.setMainDepartmentId(json.hasNonNull("main_department") ? json.path("main_department").asLong() : null); JsonNode dep = json.path("department"); if (dep.isArray()) { user.setDepartmentIdsJson(OBJECT_MAPPER.writeValueAsString(dep)); } return user; } private SyncCounter syncCustomerByFollowUser(ExternalContactUserApi externalContactUserApi, String contactToken, String followUserId, String tenantId) throws Exception { SyncCounter counter = new SyncCounter(); JsonNode listJson = fetchExternalContactList(externalContactUserApi, followUserId); JsonNode externalUserIds = listJson.path("external_userid"); if (!externalUserIds.isArray()) { return counter; } for (JsonNode externalUserNode : externalUserIds) { String externalUserId = trimToNull(externalUserNode.asText(null)); if (externalUserId == null) { continue; } JsonNode externalDetail = null; try { externalDetail = fetchExternalContactDetail(contactToken, externalUserId); } catch (IllegalStateException ex) { if (isNoExternalContactRelationError(ex.getMessage())) { // 官方建议:关系不存在(84061)时应跳过该客户,继续处理后续数据 System.out.println("[QiWeiCustomerSync] skip external user due to no relation. externalUserId=" + externalUserId + ", followUserId=" + followUserId + ", reason=" + ex.getMessage()); continue; } } printExternalCustomerToConsole(externalDetail, followUserId); CustomerManagement customer = mapToCustomerManagement(externalDetail, followUserId, tenantId); if (customer == null) { continue; } if (upsertCustomer(customer, tenantId)) { counter.inserted++; } else { counter.updated++; } } return counter; } private JsonNode fetchExternalContactList(ExternalContactUserApi externalContactUserApi, String followUserId) throws Exception { Object sdkResp = externalContactUserApi.listByUserId(followUserId); JsonNode json = OBJECT_MAPPER.valueToTree(sdkResp); int errCode = json.path("errcode").asInt(0); if (errCode != 0) { throw new IllegalStateException("获取外部联系人列表失败: " + json); } if (!json.has("external_userid") && json.has("data")) { ObjectNode adapted = OBJECT_MAPPER.createObjectNode(); adapted.set("external_userid", json.path("data")); return adapted; } return json; } private void printExternalCustomerToConsole(JsonNode externalDetail, String followUserId) { JsonNode externalContact = externalDetail.path("external_contact"); String externalUserId = trimToNull(externalContact.path("external_userid").asText(null)); String customerName = trimToNull(externalContact.path("name").asText(null)); String phone = null; JsonNode followInfo = externalDetail.path("follow_user"); if (followInfo.isArray() && followInfo.size() > 0) { JsonNode firstFollow = followInfo.get(0); JsonNode remarkMobiles = firstFollow.path("remark_mobiles"); if (remarkMobiles.isArray() && remarkMobiles.size() > 0) { phone = trimToNull(remarkMobiles.get(0).asText(null)); } } System.out.println("[QiWeiCustomerSync] externalUserId=" + (externalUserId == null ? "" : externalUserId) + ", customerName=" + (customerName == null ? "" : customerName) + ", phone=" + (phone == null ? "" : phone) + ", followUserId=" + (followUserId == null ? "" : followUserId)); } private JsonNode fetchExternalContactDetail(String contactToken, String externalUserId) throws Exception { String url = QiWeiApiConstants.EXTERNAL_CONTACT_GET + "?access_token=" + URLEncoder.encode(contactToken, StandardCharsets.UTF_8) + "&external_userid=" + URLEncoder.encode(externalUserId, 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); } return json; } private CustomerManagement mapToCustomerManagement(JsonNode externalDetail, String followUserId, String tenantId) { JsonNode externalContact = externalDetail.path("external_contact"); if (externalContact.isMissingNode() || externalContact.isNull()) { return null; } String name = trimToNull(externalContact.path("name").asText(null)); if (name == null) { return null; } String phone = null; JsonNode followInfo = externalDetail.path("follow_user"); if (followInfo.isArray() && followInfo.size() > 0) { JsonNode firstFollow = followInfo.get(0); JsonNode remarkMobiles = firstFollow.path("remark_mobiles"); if (remarkMobiles.isArray() && remarkMobiles.size() > 0) { phone = trimToNull(remarkMobiles.get(0).asText(null)); } } CustomerManagement customer = new CustomerManagement(); customer.setTenantId(tenantId); customer.setCustomerName(name); customer.setContact(phone); customer.setCustomerSource("QIWEI"); customer.setSalesId(followUserId); customer.setSalesPhone(followUserId); LambdaQueryWrapper salesQuery = new LambdaQueryWrapper() .eq(QweiUser::getUserid, followUserId); QweiUser sales = this.getOne(salesQuery, false); if (sales != null) { customer.setSalesName(sales.getUserName()); customer.setSalesPhone(trimToNull(sales.getMobile()) == null ? followUserId : sales.getMobile()); customer.setDealershipId(sales.getMainDepartmentId() == null ? null : String.valueOf(sales.getMainDepartmentId())); } return customer; } private boolean upsertCustomer(CustomerManagement customer, String tenantId) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() .eq(CustomerManagement::getCustomerName, customer.getCustomerName()); if (trimToNull(customer.getContact()) != null) { queryWrapper.eq(CustomerManagement::getContact, customer.getContact()); } CustomerManagement existing = customerManagementService.getOne(queryWrapper, false); LocalDateTime now = LocalDateTime.now(); if (existing == null) { customer.setId(UUID.randomUUID().toString()); customer.setTenantId(tenantId); customer.setRecordingCount(0); customer.setContactCount(0); customer.setCreateTime(now); customer.setUpdateTime(now); customerManagementService.save(customer); return true; } existing.setTenantId(null); existing.setCustomerName(customer.getCustomerName()); if (trimToNull(customer.getContact()) != null) { existing.setContact(customer.getContact()); } existing.setSalesId(customer.getSalesId()); existing.setSalesName(customer.getSalesName()); existing.setSalesPhone(customer.getSalesPhone()); existing.setDealershipId(customer.getDealershipId()); existing.setCustomerSource("QIWEI"); existing.setUpdateTime(now); customerManagementService.updateById(existing); return false; } private JsonNode fetchUserListId(String contactToken, String cursor, int limit) throws Exception { String url = QiWeiApiConstants.USER_LIST_ID + "?access_token=" + URLEncoder.encode(contactToken, StandardCharsets.UTF_8); String payload = OBJECT_MAPPER.writeValueAsString(Map.of( "cursor", cursor == null ? "" : cursor, "limit", limit )); HttpRequest request = HttpRequest.newBuilder(URI.create(url)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .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("获取成员ID列表失败: " + body); } return json; } private JsonNode fetchCustomerContactFollowUsers(String accessToken) throws Exception { String url = QiWeiApiConstants.EXTERNAL_CONTACT_FOLLOW_USER_LIST + "?access_token=" + URLEncoder.encode(accessToken, 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); } return json.path("follow_user"); } private void printFollowUsersToConsole(JsonNode followUsers) { if (followUsers == null || !followUsers.isArray()) { System.out.println("[QiWeiCustomerSync] follow_user list is empty or invalid."); return; } System.out.println("[QiWeiCustomerSync] follow_user count=" + followUsers.size()); for (JsonNode followUser : followUsers) { String followUserId = trimToNull(followUser.asText(null)); if (followUserId == null) { continue; } System.out.println("[QiWeiCustomerSync] followUserId=" + followUserId); } } private QiWeiConfig loadQiWeiConfig() throws Exception { String corpid = trimToNull(System.getProperty("qiwei.corpid")); String contactSecret = trimToNull(System.getProperty("qiwei.contactSecret")); String appSecret = trimToNull(System.getProperty("qiwei.appSecret")); if (corpid != null && contactSecret != null && appSecret != null) { QiWeiConfig cfg = new QiWeiConfig(); cfg.setCorpid(corpid); cfg.setContractSecret(contactSecret); cfg.setAppSecret(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 String requireNonBlank(String value, String msg) { if (value == null || value.trim().isEmpty()) { throw new IllegalStateException(msg); } return value.trim(); } private String resolveExternalContactSecret(QiWeiConfig cfg) { String fromProp = trimToNull(System.getProperty("qiwei.externalContactSecret")); if (fromProp != null) { return fromProp; } String appSecret = trimToNull(cfg.getAppSecret()); if (appSecret != null) { return appSecret; } String contactSecret = trimToNull(cfg.getContractSecret()); if (contactSecret != null) { return contactSecret; } throw new IllegalStateException("外部联系人secret不能为空(请配置 -Dqiwei.externalContactSecret 或 qiwei_config.appSecret/contractSecret)"); } private boolean isNoExternalContactRelationError(String message) { if (message == null) { return false; } return message.contains("\"errcode\":84061") || message.contains("errcode\":84061") || message.contains("不存在外部联系人的关系"); } private boolean isDuplicateUserid(Exception e) { String msg = e.getMessage(); return msg != null && msg.contains("Duplicate entry"); } 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)"); } private ExternalContactUserApi createExternalContactUserApi(String corpId, String contactSecret) { WeComTokenCacheable cacheable = new InMemoryWeComTokenCacheable(); WorkWeChatApi workWeChatApi = new WorkWeChatApi(cacheable, HttpLoggingInterceptor.Level.NONE); return workWeChatApi.externalContactManager(DefaultAgent.of(corpId, contactSecret, "0")) .externalContactUserApi(); } private static class SyncCounter { private int inserted; private int updated; } private static final class InMemoryWeComTokenCacheable implements WeComTokenCacheable { private final ConcurrentHashMap map = new ConcurrentHashMap<>(); @Override public String putCorpTicket(String corpId, String agentId, String corpTicket) { map.put("ticket:corp:" + corpId + ":" + agentId, corpTicket); return corpTicket; } @Override public String getCorpTicket(String corpId, String agentId) { return map.get("ticket:corp:" + corpId + ":" + agentId); } @Override public String putAgentTicket(String corpId, String agentId, String agentTicket) { map.put("ticket:agent:" + corpId + ":" + agentId, agentTicket); return agentTicket; } @Override public String getAgentTicket(String corpId, String agentId) { return map.get("ticket:agent:" + corpId + ":" + agentId); } @Override public String putAccessToken(String corpId, String agentId, String accessToken) { map.put("token:" + corpId + ":" + agentId, accessToken); return accessToken; } @Override public String getAccessToken(String corpId, String agentId) { return map.get("token:" + corpId + ":" + agentId); } @Override public void clearAccessToken(String corpId, String agentId) { map.remove("token:" + corpId + ":" + agentId); } } }