312 lines
13 KiB
Java
312 lines
13 KiB
Java
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;
|
||
import com.rj.util.IsoWorkdayUtils;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.web.util.UriComponentsBuilder;
|
||
|
||
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.LocalDate;
|
||
import java.time.LocalDateTime;
|
||
import java.time.format.DateTimeFormatter;
|
||
import java.util.List;
|
||
import java.util.Optional;
|
||
|
||
/**
|
||
* 调用 hxrd 后台用户查询、更新接口(与 {@link HxrAdminOrderSelectService} 共用 {@link HxrAdminProperties} 会话)。
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
@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");
|
||
|
||
private final HxrAdminProperties properties;
|
||
|
||
/**
|
||
* VIP 到期时间:以 {@code applyDate} 为起算日,若当天为周六、周日则先顺延到下一工作日,再顺延 2 个工作日;
|
||
* 时刻与调用时「当前时刻」的时分秒相同(仅调整日期)。用于开通同事特权。
|
||
*/
|
||
public static LocalDateTime computeVipExpireTime(LocalDate applyDate) {
|
||
return computeVipExpireTime(applyDate, 2);
|
||
}
|
||
|
||
/**
|
||
* VIP 到期时间:以 {@code applyDate} 为起算日,若当天为周六、周日则先顺延到下一工作日,
|
||
* 再顺延 {@code workdaysAfterAlign} 个工作日;时刻与当前时分秒一致。
|
||
*/
|
||
public static LocalDateTime computeVipExpireTime(LocalDate applyDate, int workdaysAfterAlign) {
|
||
if (applyDate == null) {
|
||
throw new IllegalArgumentException("applyDate不能为空");
|
||
}
|
||
if (workdaysAfterAlign < 0) {
|
||
throw new IllegalArgumentException("workdaysAfterAlign不能为负");
|
||
}
|
||
LocalDateTime now = LocalDateTime.now();
|
||
LocalDate d = applyDate;
|
||
while (IsoWorkdayUtils.isWeekend(d)) {
|
||
d = d.plusDays(1);
|
||
}
|
||
for (int i = 0; i < workdaysAfterAlign; i++) {
|
||
d = d.plusDays(1);
|
||
while (IsoWorkdayUtils.isWeekend(d)) {
|
||
d = d.plusDays(1);
|
||
}
|
||
}
|
||
return LocalDateTime.of(d, now.toLocalTime());
|
||
}
|
||
|
||
/**
|
||
* VIP 到期时间:以 {@code applyDate} 为起算日顺延 {@code calendarDays} 个自然日,时刻与当前时分秒一致。
|
||
*/
|
||
public static LocalDateTime computeVipExpireTimePlusCalendarDays(LocalDate applyDate, int calendarDays) {
|
||
if (applyDate == null) {
|
||
throw new IllegalArgumentException("applyDate不能为空");
|
||
}
|
||
if (calendarDays < 0) {
|
||
throw new IllegalArgumentException("calendarDays不能为负");
|
||
}
|
||
LocalDateTime now = LocalDateTime.now();
|
||
return LocalDateTime.of(applyDate.plusDays(calendarDays), now.toLocalTime());
|
||
}
|
||
|
||
/**
|
||
* 分页拉取用户列表,{@code data} 解析为 {@link }(与 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);
|
||
}
|
||
|
||
/**
|
||
* 调用第三方接口
|
||
* 按手机号查询用户列表,返回第一条。
|
||
*/
|
||
public Optional<HxrUserRow> fetchFirstUserByMobile(String mobile) throws Exception {
|
||
String cookieHeader = properties.resolveCookieHeader();
|
||
if (cookieHeader == null || cookieHeader.isBlank()) {
|
||
log.warn("hxr.admin 未配置 cookie 或 phpsid,跳过 user/select");
|
||
return Optional.empty();
|
||
}
|
||
if (mobile == null || mobile.isBlank()) {
|
||
return Optional.empty();
|
||
}
|
||
|
||
String uri = UriComponentsBuilder.fromUriString(properties.getUserSelectUrl())
|
||
.replaceQueryParam("mobile", mobile.trim())
|
||
.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 {} mobile={} bodyPrefix={}",
|
||
status, mobile, abbreviate(response.body(), 400));
|
||
return Optional.empty();
|
||
}
|
||
|
||
HxrUserSelectResponse body = JSON.readValue(response.body(), HxrUserSelectResponse.class);
|
||
if (body.code() != 0) {
|
||
log.warn("hxr admin user/select api code={} msg={} mobile={}", body.code(), body.msg(), mobile);
|
||
return Optional.empty();
|
||
}
|
||
List<HxrUserRow> data = body.data();
|
||
if (data == null || data.isEmpty()) {
|
||
return Optional.empty();
|
||
}
|
||
HxrUserRow first = data.get(0);
|
||
if (first.id() == null) {
|
||
return Optional.empty();
|
||
}
|
||
return Optional.of(first);
|
||
}
|
||
|
||
/**
|
||
* 更新用户 VIP 相关字段({@code application/x-www-form-urlencoded})。
|
||
*
|
||
* @return 接口 {@code code==0} 时为 true
|
||
*/
|
||
public boolean updateUserVipFields(long userId, int maxOrder, String viptimeFormatted) throws Exception {
|
||
return updateUserVipFields(userId, maxOrder, viptimeFormatted, 1);
|
||
}
|
||
|
||
/**
|
||
* 更新用户 VIP 相关字段({@code application/x-www-form-urlencoded})。
|
||
*
|
||
* @param maxOrder 为 null 时不传 max_order,第三方保持原值
|
||
* @param isVip 1 开通特权,0 关闭特权
|
||
* @return 接口 {@code code==0} 时为 true
|
||
*/
|
||
public boolean updateUserVipFields(long userId, Integer maxOrder, String viptimeFormatted, int isVip)
|
||
throws Exception {
|
||
String cookieHeader = properties.resolveCookieHeader();
|
||
if (cookieHeader == null || cookieHeader.isBlank()) {
|
||
log.warn("hxr.admin 未配置 cookie 或 phpsid,跳过 user/update");
|
||
return false;
|
||
}
|
||
|
||
String form = buildUpdateFormBody(userId, maxOrder, viptimeFormatted, isVip);
|
||
|
||
HttpClient client = HttpClient.newBuilder()
|
||
.connectTimeout(java.time.Duration.ofSeconds(35))
|
||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||
.build();
|
||
|
||
HttpRequest request = HttpRequest.newBuilder()
|
||
.uri(URI.create(properties.getUserUpdateUrl()))
|
||
.timeout(java.time.Duration.ofSeconds(120))
|
||
.header("Accept", "application/json, text/javascript, */*; q=0.01")
|
||
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||
.header("Cookie", cookieHeader)
|
||
.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")
|
||
.POST(HttpRequest.BodyPublishers.ofString(form, StandardCharsets.UTF_8))
|
||
.build();
|
||
|
||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||
int status = response.statusCode();
|
||
if (status < 200 || status >= 300) {
|
||
log.warn("hxr admin user/update HTTP {} id={} bodyPrefix={}",
|
||
status, userId, abbreviate(response.body(), 400));
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
HxrSimpleApiResponse api = JSON.readValue(response.body(), HxrSimpleApiResponse.class);
|
||
if (api.code() != 0) {
|
||
log.warn("hxr admin user/update api code={} msg={} id={}", api.code(), api.msg(), userId);
|
||
return false;
|
||
}
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.warn("hxr admin user/update parse body id={} err={} bodyPrefix={}",
|
||
userId, e.getMessage(), abbreviate(response.body(), 400));
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public static String formatVipTime(LocalDateTime t) {
|
||
return t.format(VIP_TIME_FORMAT);
|
||
}
|
||
|
||
private static String buildUpdateFormBody(long userId, Integer maxOrder, String viptimeFormatted, int isVip) {
|
||
int vipFlag = isVip == 1 ? 1 : 0;
|
||
String viptime = viptimeFormatted != null ? viptimeFormatted : "";
|
||
StringBuilder form = new StringBuilder();
|
||
form.append("id=").append(userId);
|
||
if (maxOrder != null) {
|
||
form.append("&max_order=").append(maxOrder);
|
||
}
|
||
form.append("&is_vip=").append(vipFlag)
|
||
.append("&is_resell=1")
|
||
.append("&status=1")
|
||
.append("&viptime=").append(URLEncoder.encode(viptime, StandardCharsets.UTF_8));
|
||
return form.toString();
|
||
}
|
||
|
||
private static String abbreviate(String s, int maxLen) {
|
||
if (s == null) {
|
||
return "";
|
||
}
|
||
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
|
||
}
|
||
}
|