适配新系统
This commit is contained in:
381
src/test/java/com/hxrd/GoodsTest.java
Normal file
381
src/test/java/com/hxrd/GoodsTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user