当天数据导入

This commit is contained in:
2026-04-25 14:13:27 +08:00
parent 4d9e6f1ef8
commit 217422f422
16 changed files with 907 additions and 5 deletions

View File

@@ -0,0 +1,17 @@
package com.rj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.entity.DailyUserTrade;
import com.rj.mapper.DailyUserTradeMapper;
import com.rj.service.IDailyUserTradeService;
import org.springframework.stereotype.Service;
/**
* <p>
* 当天用户交易表 服务实现类
* </p>
*/
@Service
public class DailyUserTradeServiceImpl extends ServiceImpl<DailyUserTradeMapper, DailyUserTrade> implements IDailyUserTradeService {
}

View File

@@ -7,6 +7,7 @@ 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;
@@ -305,7 +306,7 @@ public class QweiDepartmentServiceImpl extends ServiceImpl<QweiDepartmentMapper,
}
private String getAccessToken(String corpId, String secret) throws Exception {
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid="
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();
@@ -319,7 +320,7 @@ public class QweiDepartmentServiceImpl extends ServiceImpl<QweiDepartmentMapper,
}
private List<QweiDepartment> fetchDepartments(String token) throws Exception {
String url = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token="
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();

View File

@@ -3,20 +3,29 @@ 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;
@@ -28,6 +37,7 @@ 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 {
@@ -37,6 +47,8 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
@Autowired
private DictItemMapper dictItemMapper;
@Autowired
private ICustomerManagementService customerManagementService;
@Override
public Map<String, Object> add(QweiUser entity) {
@@ -243,6 +255,63 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
}
}
@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());
@@ -321,7 +390,7 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
}
private QweiUser fetchAndMapUser(String appToken, String userid, String tenantId) throws Exception {
String url = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token="
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();
@@ -355,8 +424,167 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
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 = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token="
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,
@@ -375,6 +603,34 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
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"));
@@ -407,7 +663,7 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
}
private String getAccessToken(String corpId, String secret) throws Exception {
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid="
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();
@@ -427,6 +683,31 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
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");
@@ -455,5 +736,59 @@ public class QweiUserServiceImpl extends ServiceImpl<QweiUserMapper, QweiUser> i
}
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);
}
}
}