795 lines
34 KiB
Java
795 lines
34 KiB
Java
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<QweiUserMapper, QweiUser> 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<String, Object> add(QweiUser entity) {
|
||
Map<String, Object> 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<QweiUser> dup = new LambdaQueryWrapper<QweiUser>()
|
||
.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<String, Object> update(QweiUser entity) {
|
||
Map<String, Object> 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<String, Object> deleteById(String id) {
|
||
Map<String, Object> 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<String, Object> pageQuery(Integer current,
|
||
Integer size,
|
||
String userid,
|
||
String userName,
|
||
Long mainDepartmentId,
|
||
Integer status) {
|
||
Map<String, Object> result = new HashMap<>();
|
||
try {
|
||
if (current == null || current < 1) {
|
||
current = 1;
|
||
}
|
||
if (size == null || size < 1) {
|
||
size = 10;
|
||
}
|
||
|
||
Page<QweiUser> page = new Page<>(current, size);
|
||
LambdaQueryWrapper<QweiUser> 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<QweiUser> 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<String, Object> syncFromQiWei(String tenantIdParam) {
|
||
Map<String, Object> 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<String, Object> syncCustomerFromQiWei(String tenantIdParam) {
|
||
Map<String, Object> 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<QweiUser> q = new LambdaQueryWrapper<QweiUser>()
|
||
.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<QweiUser> salesQuery = new LambdaQueryWrapper<QweiUser>()
|
||
.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<CustomerManagement> queryWrapper = new LambdaQueryWrapper<CustomerManagement>()
|
||
.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<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 = 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<String, String> 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);
|
||
}
|
||
}
|
||
}
|
||
|