From da1f9a64d40ec84c5394bb92ebb2ed64445e624b Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Fri, 11 Sep 2026 01:07:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(hjc):=20=E7=9F=AD=E4=BF=A1=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=8F=91=E9=80=81=E6=88=90=E5=8A=9F=E5=8D=B4?= =?UTF-8?q?=E6=8A=A5=E3=80=8C=E6=9C=8D=E5=8A=A1=E6=9A=82=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E3=80=8D=E2=80=94=E2=80=94data=20=E4=B8=8D=E6=98=AF?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E6=97=B6=E8=A7=A3=E6=9E=90=E5=B0=B1=E7=82=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CoreResult.of 用 json.getJSONObject("data") 取 data。fastjson 见到 String 值会 当成 JSON 再解析一次,而核心实例 ApiResult.data 是 Object,/sendSmsCaptcha 成功 时给的就是阿里云返回的字符串 "OK"(core 的 MainController 是 success("发送成功", result.get("Message"))),解析 "OK" 抛 JSONException。 该异常发生在 of() 内部,不在那个只包住 parseObject(raw) 的 try 里,于是逃到 execute() 的兜底 catch,被改写成 code=-1「认证服务暂不可用,请稍后重试」。 实际后果是短信已经发出去了,用户和前端看到的却是发送失败:核心实例侧验证码已 写入 Redis、60 秒重发限制与当日 10 条计数均已消耗,用户一重试就撞上 「发送过于频繁」,看起来更像坏了。login/register/captcha 的 data 都是对象, 所以只有发送短信这一条路坏。 - data 改为只接受对象,取不到即 null,绝不抛异常(toObjectData 保留 Map 分支, 兼容 fastjson 桥接层给出的普通 Map);调用方本就只按 code 判成败,明细读 raw - 新增 HjcCoreAuthClientResultTest:用日志里的真实响应体走完整调用链 (sendSmsCaptcha → post → execute → of,只打桩 doExecute 出口)。 改前该用例复现出逐字相同的异常(栈同在 of(HjcCoreAuthClient.java:93)),改后绿 - 用例同时锁住:data 为对象(登录/验证码)、业务失败无 data 键、data 为数字/数组、 响应根本不是 JSON(网关 HTML)四种情形 - application.yml:hjc.one-stop.base-url 由空串改为 http://180.141.88.21:8810 (base-url 为空时一站式推送不会发起,此前上线一直缺这项配置) - 未改动核心实例代码,改动面限于 hjc 包与上述配置 --- .../gxwebsoft/hjc/auth/HjcCoreAuthClient.java | 28 +++- src/main/resources/application.yml | 2 +- .../hjc/auth/HjcCoreAuthClientResultTest.java | 127 ++++++++++++++++++ 3 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClientResultTest.java diff --git a/src/main/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClient.java b/src/main/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClient.java index e064d5e..4f21ef3 100644 --- a/src/main/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClient.java +++ b/src/main/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClient.java @@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.util.Map; /** * 汇吉采登录认证对核心实例的调用出口。 @@ -64,7 +65,7 @@ public class HjcCoreAuthClient { private int code; private String message; private String error; - /** 成功时的 data(核心实例的 LoginResult / CaptchaResult) */ + /** 成功时的 data(核心实例的 LoginResult / CaptchaResult);不是对象时留空,明细看 {@link #raw} */ private JSONObject data; /** 原始响应体,仅用于日志与异常排查 */ private String raw; @@ -90,10 +91,33 @@ public class HjcCoreAuthClient { r.code = json.getIntValue("code"); r.message = json.getString("message"); r.error = json.getString("error"); - r.data = json.getJSONObject("data"); + r.data = toObjectData(json.get("data")); r.ok = r.code == 0; return r; } + + /** + * 把 {@code data} 取成 JSONObject,取不到就是 null,绝不抛异常。 + * + *

不能用 {@code json.getJSONObject("data")}:fastjson 见到 String 会当成 JSON 再解析一次, + * 而核心实例的 {@code data} 是 {@code Object},{@code /sendSmsCaptcha} 成功时给的就是阿里云的 + * 字符串 {@code "OK"}(core 里是 {@code success("发送成功", result.get("Message"))})。 + * 解析 {@code "OK"} 抛的 {@code JSONException} 会一路逃到 {@link #execute} 的兜底 catch, + * 被改写成「认证服务暂不可用,请稍后重试」——短信其实已经发出去了,用户却看到发送失败, + * 核心实例那侧 60 秒重发限制和当日计数还已经消耗掉了。

+ * + *

调用方一律按 {@code code} 判成败;{@code data} 不是对象时不需要它的内容,要明细读 {@code raw}。

+ */ + private static JSONObject toObjectData(Object value) { + if (value instanceof JSONObject) { + return (JSONObject) value; + } + // fastjson 解析出来的嵌套对象可能只是普通 Map(不是 com.alibaba.fastjson.JSONObject 的实例) + if (value instanceof Map) { + return new JSONObject((Map) value); + } + return null; + } } /** diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 27cec07..db39ed2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -200,5 +200,5 @@ hjc: tenant-id: 10626 one-stop: # 一站式平台地址(+ path = 最终推送地址)。base-url 为空时推送不会发起,部署时需配置。 - base-url: "" + base-url: "http://180.141.88.21:8810" create-purchase-details-path: /api/biz/createPurchaseDetails diff --git a/src/test/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClientResultTest.java b/src/test/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClientResultTest.java new file mode 100644 index 0000000..4e62880 --- /dev/null +++ b/src/test/java/com/gxwebsoft/hjc/auth/HjcCoreAuthClientResultTest.java @@ -0,0 +1,127 @@ +package com.gxwebsoft.hjc.auth; + +import cn.hutool.http.HttpRequest; +import com.gxwebsoft.common.core.config.ConfigProperties; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 核心实例响应解析:{@code data} 不一定是对象。 + * + *

核心实例的 {@code ApiResult.data} 是 {@code Object},各接口给什么类型都有可能。 + * {@code /sendSmsCaptcha} 成功时 {@code data} 是阿里云返回的字符串 {@code "OK"} + * (core {@code MainController.sendSmsCaptchaInternal} 里是 + * {@code success("发送成功", result.get("Message"))})。

+ * + *

此前 {@code CoreResult.of} 无条件调 {@code json.getJSONObject("data")}:fastjson 见到 + * 字符串会当成 JSON 去解析,解析 {@code "OK"} 直接抛 {@code JSONException}。异常逃出 + * {@code of()} 后被 {@code execute()} 的兜底 catch 吞掉,对外变成 + * 「认证服务暂不可用,请稍后重试」——短信其实已经发出去了,用户却看到发送失败, + * 而且核心实例那侧的重发限制(60 秒)和当日计数已经消耗掉。所以这里锁死: + * {@code data} 不是对象也必须按 {@code code} 判定成功。

+ */ +class HjcCoreAuthClientResultTest { + + /** 线上抓到的真实响应体(2026-09-11 00:29:23.708 日志原文) */ + static final String SMS_OK = "{\"code\":0,\"message\":\"发送成功\",\"data\":\"OK\"}"; + + /** 走真实调用链(sendSmsCaptcha → post → execute → CoreResult.of),只打桩 HTTP 出口 */ + @Test + void 短信发送成功时data是字符串OK也必须判成功() { + HjcCoreAuthClient.CoreResult r = clientReturning(SMS_OK).sendSmsCaptcha("13800000000"); + + assertTrue(r.isOk(), "核心实例已返回 code=0「发送成功」,不能因为 data 是字符串就判为失败;实际 code=" + + r.getCode() + " message=" + r.getMessage() + " error=" + r.getError()); + assertEquals(0, r.getCode()); + assertEquals("发送成功", r.getMessage()); + assertEquals(SMS_OK, r.getRaw(), "原始响应体必须保留,否则日志里看不到真相"); + } + + /** data 是对象时照样要能取到(登录 / 图形验证码走的是这条路) */ + @Test + void data是对象时正常解析() { + HjcCoreAuthClient.CoreResult r = clientReturning( + "{\"code\":0,\"message\":\"登录成功\",\"data\":{\"access_token\":\"t\",\"user\":{\"userId\":7}}}") + .login("企业A", "pwd", "1234"); + + assertTrue(r.isOk()); + assertNotNull(r.getData(), "data 是对象时必须解析出来"); + assertEquals(7, r.getData().getJSONObject("user").getIntValue("userId")); + } + + /** 业务失败时核心实例不给 data(ApiResult 标了 NON_NULL,data 键直接不出现) */ + @Test + void 业务失败没有data字段时按message透传() { + HjcCoreAuthClient.CoreResult r = clientReturning( + "{\"code\":1,\"message\":\"发送过于频繁,请 60 秒后再试\"}").sendSmsCaptcha("13800000000"); + + assertFalse(r.isOk()); + assertEquals(1, r.getCode()); + assertEquals("发送过于频繁,请 60 秒后再试", r.getMessage()); + assertNull(r.getData()); + } + + /** data 是数字 / 数组这类非对象 JSON 值时也不能炸 */ + @Test + void data是其他非对象类型时不抛异常() { + HjcCoreAuthClient.CoreResult number = clientReturning("{\"code\":0,\"message\":\"ok\",\"data\":123}") + .captcha(); + assertTrue(number.isOk(), "data=123 不该让整个调用失败:" + number.getError()); + assertNull(number.getData(), "非对象 data 留空,明细看 raw"); + + HjcCoreAuthClient.CoreResult array = clientReturning("{\"code\":0,\"message\":\"ok\",\"data\":[1,2]}") + .captcha(); + assertTrue(array.isOk(), "data=[1,2] 不该让整个调用失败:" + array.getError()); + assertNull(array.getData()); + } + + /** 响应根本不是 JSON(网关 502 / HTML 错误页)时给既有文案,不把解析异常漏出去 */ + @Test + void 响应不是JSON时给认证服务异常文案() { + HjcCoreAuthClient.CoreResult r = clientReturning("502 Bad Gateway").sendSmsCaptcha("13800000000"); + + assertFalse(r.isOk()); + assertEquals(-1, r.getCode()); + assertEquals("认证服务返回异常", r.getMessage()); + } + + private static HjcCoreAuthClient clientReturning(String raw) { + HjcCoreAuthClient client = new HjcCoreAuthClient() { + @Override + protected String doExecute(HttpRequest request, Integer tenantId, String path) { + return raw; + } + }; + ConfigProperties configProperties = new ConfigProperties(); + configProperties.setServerUrl("https://server.websoft.top/api"); + set(client, "configProperties", configProperties); + HjcAuthProperties authProperties = new HjcAuthProperties(); + set(authProperties, "tenantId", 10626); + set(client, "hjcAuthProperties", authProperties); + return client; + } + + private static void set(Object target, String field, Object value) { + Class type = target.getClass(); + while (type != null) { + try { + Field f = type.getDeclaredField(field); + f.setAccessible(true); + f.set(target, value); + return; + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + throw new IllegalStateException("找不到字段 " + field); + } +}