适配新系统

This commit is contained in:
2026-09-02 07:45:02 +08:00
parent 4ab7b276d6
commit 0e797a2a9b
13 changed files with 2206 additions and 68 deletions

View File

@@ -0,0 +1,381 @@
package com.hxrd;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rj.util.HxrdSignUtil;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.zip.GZIPInputStream;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* hxrd 拉货包两步测试:
* <ol>
* <li>模拟登录 POST {@code /trade-app/api/app/login} → 保存 token/user 信息(静态字段)。</li>
* <li>带 {@code Authorization: Bearer <token>} 和签名头,GET {@code /trade-app/api/app/sale/goods-list?page=1&size=20} → 控制台打印货物列表。</li>
* </ol>
*
* <p>签名由 {@link HxrdSignUtil#sign()} 生成(已与前端 sign.js lx() 对齐,并在真实抓包两组样本上逐 hex 相等验证通过)。
*/
class GoodsTest {
// ---------- 环境配置 ----------
private static final String BASE_URL = "https://hxrdm.hxrd777.com";
private static final String UA =
"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Edg/152.0.0.0 Mobile Safari/537.36";
// ---------- 登录信息 ----------
private static final String PHONE = "19290117050";
private static final String PASSWORD = "123456";
// ---------- 登录态缓存(被第二步引用) ----------
/** 登录接口返回的 JWT token(不含 "Bearer " 前缀)。 */
private static String loginToken;
/** 原始登录响应,便于调试。 */
private static JSONObject loginResponse;
// ====================================================================
// 第一步:模拟登录
// ====================================================================
@Test
void step1_login() throws IOException {
JSONObject body = new JSONObject();
body.put("phone", PHONE);
body.put("password", PASSWORD);
JSONObject resp = request("POST", BASE_URL + "/trade-app/api/app/login", null, body.toJSONString(), null);
loginResponse = resp;
System.out.println("===== 登录响应 =====");
System.out.println(JSON.toJSONString(resp, true));
// 兼容多种返回格式
assertTrue(isSuccessResponse(resp), "登录失败,响应: " + JSON.toJSONString(resp));
// 取 token(兼容多种常见返回格式 + 去掉已经自带的 "Bearer " 前缀)
String rawToken = null;
Object data = resp.get("data");
if (data instanceof JSONObject d) {
rawToken = d.getString("token");
if (rawToken == null) rawToken = d.getString("accessToken");
if (rawToken == null) rawToken = d.getString("jwt");
}
if (rawToken == null) rawToken = resp.getString("token");
if (rawToken == null) rawToken = resp.getString("accessToken");
assertNotNull(rawToken, "登录成功,但响应中未找到 token 字段,请检查 data 结构: " + JSON.toJSONString(resp));
// 后端可能直接返回 "Bearer xxx",去重前缀
if (rawToken.regionMatches(true, 0, "bearer ", 0, 7)) {
rawToken = rawToken.substring(7);
}
loginToken = rawToken;
System.out.println("Token 前缀: " + loginToken.substring(0, Math.min(24, loginToken.length())) + "...");
}
// ====================================================================
// 第二步:根据登录 token 拉取货物包
// ====================================================================
static boolean isSuccessResponse(JSONObject resp) {
Object success = resp.get("success");
if (success != null) {
return Boolean.TRUE.equals(success)
|| "true".equalsIgnoreCase(String.valueOf(success))
|| (success instanceof Number && ((Number) success).intValue() == 1);
}
Object code = resp.get("code");
return (code instanceof Number && ((Number) code).intValue() == 200) || "200".equals(String.valueOf(code));
}
@Test
void step2_fetchGoodsList_print() throws IOException {
if (loginToken == null) step1_login(); // 单步跑 step2 也能自动登录
String url = BASE_URL + "/trade-app/api/app/sale/goods-list?page=1&size=20";
JSONObject resp = request("GET", url, null, null, loginToken);
assertTrue(isSuccessResponse(resp), "拉货失败,响应: " + JSON.toJSONString(resp));
System.out.println();
System.out.println("===== 拉取货物包 goods-list page=1 size=20 =====");
// 分页数据兼容两种位置:顶层(当前后端实现)/ data.records 包裹
Object total = pickFirst(resp, "total", "count", "totalCount");
Object pages = pickFirst(resp, "pages", "totalPages");
Object size = pickFirst(resp, "size", "pageSize");
Object current = pickFirst(resp, "page", "current", "pageNum");
System.out.println("总条数 : " + (total != null ? total : "(未知)"));
System.out.println("总页数 : " + (pages != null ? pages : "(未知)"));
System.out.println("本页大小 : " + (size != null ? size : "(未知)"));
System.out.println("当前页 : " + (current != null ? current : "(未知)"));
// 先在顶层找列表,找不到再去 data 对象里(防止后端有不同封装)
Object list = pickFirst(resp, "records", "list", "rows", "data");
Object data = resp.get("data");
List<?> rows = null;
if (list instanceof List<?> l) {
rows = l;
} else if (data instanceof List<?> l) {
rows = l;
} else if (data instanceof JSONObject d) {
Object inner = pickFirst(d, "records", "list", "rows", "data");
if (inner instanceof List<?> l) rows = l;
}
if (rows == null || rows.isEmpty()) {
System.out.println("本页无数据。resp = " + JSON.toJSONString(resp));
return;
}
System.out.println("本页条数 : " + rows.size());
System.out.println();
System.out.println("货物列表(逐行打印):");
for (int i = 0; i < rows.size(); i++) {
System.out.println(" [" + (i + 1) + "] " + JSON.toJSONString(rows.get(i)));
}
}
/** 快速手动全跑:登录 → 拉货 → 打印。 */
public static void main(String[] args) throws IOException {
GoodsTest t = new GoodsTest();
t.step1_login();
t.step3_fetchAllGoods();
}
// ====================================================================
// 拉取一页货物(纯数据,不打印)
// ====================================================================
/**
* 拉取一页货物,返回分页对象。
* 解析后的字段(与当前后端对齐):
* - total : 总条数(Integer)
* - pages : 总页数(Integer)
* - size : 每页大小(Integer)
* - page : 当前页(Integer)
* - rows : List<JSONObject>(本页货物列表)
*/
static class GoodsPage {
public final int total;
public final int pages;
public final int size;
public final int page;
public final java.util.List<JSONObject> rows;
GoodsPage(int total, int pages, int size, int page, java.util.List<JSONObject> rows) {
this.total = total; this.pages = pages; this.size = size; this.page = page; this.rows = rows;
}
}
static GoodsPage fetchGoodsPage(int page, int size, String token) throws IOException {
String url = BASE_URL + "/trade-app/api/app/sale/goods-list?page=" + page + "&size=" + size;
JSONObject resp = request("GET", url, null, null, token);
if (!isSuccessResponse(resp)) throw new IOException("拉第 " + page + " 页失败: " + JSON.toJSONString(resp));
int total = toInt(pickFirst(resp, "total", "count", "totalCount"), 0);
int pages = toInt(pickFirst(resp, "pages", "totalPages"), 0);
int psize = toInt(pickFirst(resp, "size", "pageSize"), size);
int ppage = toInt(pickFirst(resp, "page", "current", "pageNum"), page);
// 列表查找
Object list = pickFirst(resp, "records", "list", "rows", "data");
Object data = resp.get("data");
java.util.List<?> rows = null;
if (list instanceof java.util.List<?> l) rows = l;
else if (data instanceof java.util.List<?> l) rows = l;
else if (data instanceof JSONObject d) {
Object inner = pickFirst(d, "records", "list", "rows", "data");
if (inner instanceof java.util.List<?> l) rows = l;
}
java.util.List<JSONObject> out = new java.util.ArrayList<>();
if (rows != null) for (Object r : rows) if (r instanceof JSONObject o) out.add(o);
return new GoodsPage(total, pages, psize, ppage, out);
}
// ====================================================================
// 第三步:循环拉取全部货物,控制台打印进度 + 最终汇总
// ====================================================================
@Test
void step3_fetchAllGoods() throws IOException {
if (loginToken == null) step1_login();
final int PAGE_SIZE = 20;
java.util.List<JSONObject> all = new java.util.ArrayList<>(512);
int retry = 0, maxRetryPerPage = 3;
// 先拿第一页(确定总条数 & 总页数)
GoodsPage first = fetchGoodsPage(1, PAGE_SIZE, loginToken);
all.addAll(first.rows);
int total = first.total;
int pages = first.pages;
if (pages == 0) {
// 根据 total 兜底算页数,避免后端没返回 pages
pages = (total + PAGE_SIZE - 1) / PAGE_SIZE;
}
System.out.printf("=== [1/%d] 第一页 总条数=%d 总页数=%d 本页=%d条 累计=%d%n",
pages, total, pages, first.rows.size(), all.size());
// 拉第 2..pages 页
for (int p = 2; p <= pages; p++) {
GoodsPage gp;
try {
gp = fetchGoodsPage(p, PAGE_SIZE, loginToken);
retry = 0;
} catch (IOException e) {
if (++retry > maxRetryPerPage) throw e;
System.out.println(" !! 第 " + p + " 页失败,第 " + retry + " 次重试:" + e.getMessage());
try { Thread.sleep(500L * retry); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
p--;
continue;
}
all.addAll(gp.rows);
// 每 5 页打印一次进度(最后一页强制打)
if (p % 5 == 0 || p == pages) {
System.out.printf(" ... [%d/%d] 累计 %d 条%n", p, pages, all.size());
}
}
// 打印最终汇总
System.out.println();
System.out.println("===== 全部货物拉取完成 =====");
System.out.println("后端声明总条数 : " + total);
System.out.println("实际拉取条数 : " + all.size());
if (total > 0) {
System.out.println("数量一致? : " + (all.size() == total ? "是 ✅" : "否(差 " + (total - all.size()) + " 条)"));
}
System.out.println("总页数 : " + pages);
System.out.println();
System.out.println("前 3 条示例:");
for (int i = 0; i < Math.min(3, all.size()); i++) {
System.out.println(" [" + (i + 1) + "] id=" + all.get(i).get("id")
+ " seller=" + all.get(i).get("seller")
+ " price=" + all.get(i).get("sellingPrice")
+ " qty=" + all.get(i).get("quantity")
+ " title=" + all.get(i).get("title"));
}
System.out.println();
System.out.println("后 3 条示例:");
for (int i = Math.max(0, all.size() - 3); i < all.size(); i++) {
System.out.println(" [" + (i + 1) + "/" + all.size() + "] id=" + all.get(i).get("id")
+ " seller=" + all.get(i).get("seller")
+ " price=" + all.get(i).get("sellingPrice")
+ " qty=" + all.get(i).get("quantity"));
}
// =======================================================
// 在控制台输出每个货物包的完整信息(共 all.size() 条)
// =======================================================
System.out.println();
System.out.println("==============================================================");
System.out.println("【全部货物明细】 共 " + all.size() + " 条,逐条打印:");
System.out.println("==============================================================");
for (int i = 0; i < all.size(); i++) {
JSONObject g = all.get(i);
System.out.println("--- [" + (i + 1) + "/" + all.size() + "] ---------------------------");
System.out.println(" id : " + g.get("id"));
System.out.println(" oldId : " + g.get("oldId"));
System.out.println(" title : " + g.get("title"));
System.out.println(" seller : " + g.get("seller"));
System.out.println(" sellingPrice : " + g.get("sellingPrice"));
System.out.println(" quantity : " + g.get("quantity") + " unit=" + g.get("unit"));
System.out.println(" status : " + g.get("status") + " displayStatus=" + g.get("displayStatus"));
System.out.println(" image : " + g.get("image"));
System.out.println(" createTime : " + g.get("createTime"));
System.out.println(" updateTime : " + g.get("updateTime"));
}
System.out.println("==============================================================");
System.out.println("打印完成,共 " + all.size() + " 条。");
}
static int toInt(Object o, int d) {
if (o == null) return d;
if (o instanceof Number n) return n.intValue();
try { return Integer.parseInt(String.valueOf(o).trim()); }
catch (Exception ignore) { return d; }
}
// ====================================================================
// 底层:带签名的 HTTP 请求封装
// ====================================================================
static JSONObject request(String method, String url, String query, String bodyJson, String bearer) throws IOException {
String full = url;
if (query != null && !query.isEmpty()) full += (url.contains("?") ? "&" : "?") + query;
HttpURLConnection conn = (HttpURLConnection) new URL(full).openConnection();
try {
conn.setRequestMethod(method);
conn.setConnectTimeout(15_000);
conn.setReadTimeout(30_000);
conn.setInstanceFollowRedirects(true);
conn.setUseCaches(false);
// 通用头(对齐浏览器抓包)
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
conn.setRequestProperty("Accept-Encoding", "gzip");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Origin", BASE_URL);
conn.setRequestProperty("Referer", BASE_URL + "/");
conn.setRequestProperty("User-Agent", UA);
conn.setRequestProperty("Sec-Ch-Ua", "\"Chromium\";v=\"152\", \"Not?A_Brand\";v=\"24\", \"Microsoft Edge\";v=\"152\"");
conn.setRequestProperty("Sec-Ch-Ua-Mobile", "?1");
conn.setRequestProperty("Sec-Ch-Ua-Platform", "\"Android\"");
conn.setRequestProperty("Sec-Fetch-Dest", "empty");
conn.setRequestProperty("Sec-Fetch-Mode", "cors");
conn.setRequestProperty("Sec-Fetch-Site", "same-origin");
// ---- 签名三 Header(核心,对应前端 lx() ) ----
HxrdSignUtil.SignTriplet trip = HxrdSignUtil.sign();
conn.setRequestProperty("X-Sign-N", trip.nHeader());
conn.setRequestProperty("X-Sign-T", trip.tHeader());
conn.setRequestProperty("X-Sign-S", trip.sHeader());
// ---- 登录后才有 Authorization ----
if (bearer != null && !bearer.isEmpty()) {
conn.setRequestProperty("Authorization", "Bearer " + bearer);
}
// ---- 写 body(POST/PUT) ----
if (bodyJson != null) {
byte[] bb = bodyJson.getBytes(StandardCharsets.UTF_8);
conn.setDoOutput(true);
conn.setFixedLengthStreamingMode(bb.length);
try (OutputStream os = conn.getOutputStream()) { os.write(bb); }
}
int http = conn.getResponseCode();
InputStream is = (http >= 200 && http < 400) ? conn.getInputStream() : conn.getErrorStream();
byte[] raw = readAllAndDecompress(is, conn.getContentEncoding());
String text = new String(raw, StandardCharsets.UTF_8);
if (http != 200) {
System.err.println("HTTP " + http + " for " + method + " " + full);
System.err.println("Response Headers: " + conn.getHeaderFields());
System.err.println("BODY: " + text);
}
if (text.isEmpty()) throw new IOException("HTTP " + http + " empty body");
return JSON.parseObject(text);
} finally {
conn.disconnect();
}
}
// ====================== 工具方法 ======================
private static byte[] readAllAndDecompress(InputStream is, String encoding) throws IOException {
if (is == null) return new byte[0];
try (InputStream in = ("gzip".equalsIgnoreCase(encoding)) ? new GZIPInputStream(is) : is) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) baos.write(buf, 0, n);
return baos.toByteArray();
}
}
private static Object pickFirst(JSONObject data, String... keys) {
for (String k : keys) if (data.containsKey(k)) return data.get(k);
return null;
}
}

View File

@@ -25,8 +25,8 @@ public class buyerTest {
private static final String CLAIM_ORDER_URL = "https://web.qdwsmgs.com/api/order/%d/claim";
private static final String PHONE = "15313852029";
private static final String PASSWORD = "123456";
private static final int MAX_PRICE = 38000;
private static final int MAX_BUY_NUM = 2;
private static final int MAX_PRICE = 30000;
private static final int MAX_BUY_NUM = 1;
private static final int PAGE_SIZE = 20;