用户同步

This commit is contained in:
2026-05-19 08:35:55 +08:00
parent 7c9ee9101e
commit b73f5505bf
10 changed files with 334 additions and 2 deletions

View File

@@ -3,7 +3,10 @@ package com.rj.service;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.rj.config.HxrAdminProperties;
import com.rj.dto.hxr.HxrLbUserSelectResponse;
import com.rj.dto.hxr.HxrSimpleApiResponse;
import com.rj.dto.hxr.HxrUserRow;
import com.rj.dto.hxr.HxrUserSelectResponse;
@@ -33,10 +36,19 @@ import java.util.Optional;
@RequiredArgsConstructor
public class HxrAdminUserService {
/** 与后台 user/select 默认 limit 一致,用于判断是否还有下一页 */
public static final int USER_SELECT_PAGE_SIZE = 90;
private static final ObjectMapper JSON = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private static final ObjectMapper USER_JSON = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
private static final DateTimeFormatter VIP_TIME_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -89,6 +101,61 @@ public class HxrAdminUserService {
return LocalDateTime.of(applyDate.plusDays(calendarDays), now.toLocalTime());
}
/**
* 分页拉取用户列表,{@code data} 解析为 {@link LbUser}(与 lb_user 字段对应)。
*
* @param page 页码,从 1 开始
*/
public Optional<HxrLbUserSelectResponse> fetchUserSelectPage(int page) throws Exception {
String cookieHeader = properties.resolveCookieHeader();
if (cookieHeader == null || cookieHeader.isBlank()) {
log.warn("hxr.admin 未配置 cookie 或 phpsid跳过 user/select");
return Optional.empty();
}
String uri = UriComponentsBuilder.fromUriString(properties.getUserSelectUrl())
.replaceQueryParam("page", Math.max(1, page))
.replaceQueryParam("limit", USER_SELECT_PAGE_SIZE)
.replaceQueryParam("mobile", "")
.replaceQueryParam("id", "")
.replaceQueryParam("pid", "")
.build()
.encode()
.toUriString();
HttpClient client = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(35))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.timeout(java.time.Duration.ofSeconds(120))
.header("Accept", "application/json, text/javascript, */*; q=0.01")
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
.header("Cookie", cookieHeader)
.header("Priority", "u=1, i")
.header("Referer", "https://hxrdhoutai.hxrdsm.cn/app/admin/user/index")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status < 200 || status >= 300) {
log.warn("hxr admin user/select HTTP {} page={} bodyPrefix={}",
status, page, abbreviate(response.body(), 400));
return Optional.empty();
}
HxrLbUserSelectResponse body = USER_JSON.readValue(response.body(), HxrLbUserSelectResponse.class);
if (body.code() != 0) {
log.warn("hxr admin user/select api code={} msg={} page={}", body.code(), body.msg(), page);
return Optional.empty();
}
return Optional.of(body);
}
/**
* 调用第三方接口
* 按手机号查询用户列表,返回第一条。

View File

@@ -16,6 +16,7 @@ public interface ILbUserService extends IService<LbUser> {
Map<String, Object> pageQuery(
Integer current,
Integer size,
String tenantId,
Long pid,
String username,
String nickname,
@@ -30,4 +31,9 @@ public interface ILbUserService extends IService<LbUser> {
String joinTimeEnd,
String updatedAtStart,
String updatedAtEnd);
/**
* 从 hxrd 后台 user/select 分页拉取全部用户并写入 lb_user每页拉取后立即 upsert
*/
Map<String, Object> syncFromHxrAdmin(String tenantId);
}

View File

@@ -3,23 +3,37 @@ 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.rj.dto.hxr.HxrLbUserSelectResponse;
import com.rj.entity.LbUser;
import com.rj.mapper.LbUserMapper;
import com.rj.service.HxrAdminUserService;
import com.rj.service.ILbUserService;
import com.rj.tenant.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Slf4j
@Service
public class LbUserServiceImpl extends ServiceImpl<LbUserMapper, LbUser> implements ILbUserService {
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private HxrAdminUserService hxrAdminUserService;
@Override
public Map<String, Object> add(LbUser entity) {
Map<String, Object> result = new HashMap<>();
@@ -29,6 +43,12 @@ public class LbUserServiceImpl extends ServiceImpl<LbUserMapper, LbUser> impleme
result.put("message", "id不能为空");
return result;
}
if (entity.getTenantId() == null || entity.getTenantId().trim().isEmpty()) {
result.put("success", false);
result.put("message", "tenantId不能为空");
return result;
}
entity.setTenantId(entity.getTenantId().trim());
if (this.getById(entity.getId()) != null) {
result.put("success", false);
result.put("message", "该用户 id 已存在");
@@ -107,6 +127,7 @@ public class LbUserServiceImpl extends ServiceImpl<LbUserMapper, LbUser> impleme
public Map<String, Object> pageQuery(
Integer current,
Integer size,
String tenantId,
Long pid,
String username,
String nickname,
@@ -131,6 +152,9 @@ public class LbUserServiceImpl extends ServiceImpl<LbUserMapper, LbUser> impleme
}
LambdaQueryWrapper<LbUser> w = new LambdaQueryWrapper<>();
if (tenantId != null && !tenantId.trim().isEmpty()) {
w.eq(LbUser::getTenantId, tenantId.trim());
}
if (pid != null) {
w.eq(LbUser::getPid, pid);
}
@@ -276,4 +300,127 @@ public class LbUserServiceImpl extends ServiceImpl<LbUserMapper, LbUser> impleme
return null;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncFromHxrAdmin(String tenantId) {
Map<String, Object> result = new HashMap<>();
try {
if (tenantId == null || tenantId.trim().isEmpty()) {
result.put("success", false);
result.put("message", "tenantId不能为空");
return result;
}
String tid = tenantId.trim();
String previousTenantId = TenantContextHolder.getTenantId();
TenantContextHolder.setTenantId(tid);
try {
HxrLbUserSelectResponse firstBody = null;
int page = 1;
int totalSynced = 0;
while (true) {
Optional<HxrLbUserSelectResponse> opt = hxrAdminUserService.fetchUserSelectPage(page);
if (opt.isEmpty()) {
if (page == 1) {
result.put("success", false);
result.put("message", "未拉取到用户(请检查 hxr.admin cookie/phpsid、网络或后台返回");
result.put("synced", 0);
return result;
}
result.put("success", false);
result.put("message", "" + page + " 页拉取失败,已成功同步前 "
+ (page - 1) + " 页共 " + totalSynced + "");
result.put("synced", totalSynced);
if (firstBody != null) {
result.put("remoteCount", firstBody.count());
}
result.put("pages", page - 1);
return result;
}
HxrLbUserSelectResponse body = opt.get();
if (firstBody == null) {
firstBody = body;
}
List<LbUser> rows = body.data();
if (rows == null || rows.isEmpty()) {
break;
}
List<LbUser> entities = new ArrayList<>(rows.size());
for (LbUser row : rows) {
if (row.getId() == null) {
continue;
}
row.setTenantId(tid);
applyDefaults(row);
entities.add(row);
}
int upserted = upsertBatch(entities);
if (upserted < 0) {
result.put("success", false);
result.put("message", "" + page + " 页保存失败,已成功同步前 "
+ (page - 1) + " 页共 " + totalSynced + "");
result.put("synced", totalSynced);
result.put("remoteCount", firstBody.count());
result.put("pages", page - 1);
return result;
}
totalSynced += upserted;
if (rows.size() < HxrAdminUserService.USER_SELECT_PAGE_SIZE) {
break;
}
page++;
}
result.put("success", true);
result.put("message", totalSynced == 0 ? "接口成功,无用户数据" : "同步完成");
result.put("synced", totalSynced);
result.put("remoteCount", firstBody != null ? firstBody.count() : 0);
result.put("pages", page);
return result;
} finally {
if (previousTenantId != null) {
TenantContextHolder.setTenantId(previousTenantId);
} else {
TenantContextHolder.clear();
}
}
} catch (Exception e) {
result.put("success", false);
result.put("message", "同步异常:" + e.getMessage());
result.put("synced", 0);
log.error("syncFromHxrAdmin failed", e);
return result;
}
}
/**
* @return 实际 upsert 条数;无有效 id 时返回 0失败返回 -1
*/
private int upsertBatch(List<LbUser> entities) {
if (entities == null || entities.isEmpty()) {
return 0;
}
Map<Long, LbUser> deduped = new LinkedHashMap<>();
for (LbUser entity : entities) {
if (entity.getId() != null) {
deduped.put(entity.getId(), entity);
}
}
if (deduped.isEmpty()) {
return 0;
}
List<LbUser> rows = new ArrayList<>(deduped.values());
try {
baseMapper.upsertBatch(rows);
return rows.size();
} catch (Exception e) {
log.error("lb_user upsertBatch failed, size={}", rows.size(), e);
return -1;
}
}
}