diff --git a/src/main/java/com/rj/dto/hxr/HxrSimpleApiResponse.java b/src/main/java/com/rj/dto/hxr/HxrSimpleApiResponse.java new file mode 100644 index 0000000..d2ff98e --- /dev/null +++ b/src/main/java/com/rj/dto/hxr/HxrSimpleApiResponse.java @@ -0,0 +1,4 @@ +package com.rj.dto.hxr; + +/** hxrd 后台部分写接口通用 {@code code/msg} 响应。 */ +public record HxrSimpleApiResponse(int code, String msg) {} diff --git a/src/main/java/com/rj/dto/hxr/HxrUserRow.java b/src/main/java/com/rj/dto/hxr/HxrUserRow.java new file mode 100644 index 0000000..1d6f667 --- /dev/null +++ b/src/main/java/com/rj/dto/hxr/HxrUserRow.java @@ -0,0 +1,6 @@ +package com.rj.dto.hxr; + +/** + * hxrd 后台 {@code /app/admin/user/select} 列表项(仅声明更新所需字段,其余字段忽略)。 + */ +public record HxrUserRow(Long id) {} diff --git a/src/main/java/com/rj/dto/hxr/HxrUserSelectResponse.java b/src/main/java/com/rj/dto/hxr/HxrUserSelectResponse.java new file mode 100644 index 0000000..11e35c8 --- /dev/null +++ b/src/main/java/com/rj/dto/hxr/HxrUserSelectResponse.java @@ -0,0 +1,6 @@ +package com.rj.dto.hxr; + +import java.util.List; + +/** hxrd 后台 {@code /app/admin/user/select} 顶层 JSON。 */ +public record HxrUserSelectResponse(int code, String msg, int count, List data) {} diff --git a/src/main/java/com/rj/service/HxrAdminUserService.java b/src/main/java/com/rj/service/HxrAdminUserService.java new file mode 100644 index 0000000..bd8be1e --- /dev/null +++ b/src/main/java/com/rj/service/HxrAdminUserService.java @@ -0,0 +1,201 @@ +package com.rj.service; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.rj.config.HxrAdminProperties; +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 { + + private static final ObjectMapper JSON = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + 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) { + if (applyDate == null) { + throw new IllegalArgumentException("applyDate不能为空"); + } + LocalDateTime now = LocalDateTime.now(); + LocalDate d = applyDate; + while (IsoWorkdayUtils.isWeekend(d)) { + d = d.plusDays(1); + } + for (int i = 0; i < 2; i++) { + d = d.plusDays(1); + while (IsoWorkdayUtils.isWeekend(d)) { + d = d.plusDays(1); + } + } + return LocalDateTime.of(d, now.toLocalTime()); + } + + /** + * 调用第三方接口 + * 按手机号查询用户列表,返回第一条。 + */ + 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 { + 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); + + 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, int maxOrder, String viptimeFormatted) { + return "id=" + userId + + "&max_order=" + maxOrder + + "&is_vip=1" + + "&is_resell=1" + + "&status=1" + + "&viptime=" + URLEncoder.encode(viptimeFormatted, StandardCharsets.UTF_8); + } + + private static String abbreviate(String s, int maxLen) { + if (s == null) { + return ""; + } + return s.length() <= maxLen ? s : s.substring(0, maxLen) + "..."; + } +}