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 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 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 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 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 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 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) + "..."; } }