Explorar el Código

联调期关闭短信验证码校验,手机号登录随便填都能过

- 新增配置 yoshop.sms.verify-code(默认 false):短信通道未接入,
  手机号登录时 smsCode 不填或随便填都放行,仅在 DEBUG 日志打印跳过提示;
  接入短信平台后改为 true 即恢复 PHP 原语义(空值报错 + 次数递减 + 用后即焚)
- ApiPassportService.login:校验逻辑改为开关控制,手机号非空与格式校验不变
- tools/test_api_batch4.py:脚本按 yml 开关自动选择断言,关闭时断言
  「空验证码也能登录」「任意验证码可登录」,开启时跑原有 4 项严格校验用例;
  当前 73 项全过(开启后回到 77 项)
- tools/test_uniapp_contract.py:yml 路径改用 _root.ROOT 定位,脱离 tools 目录也能跑
- 联调说明.md 补短信开关说明与用例数
tanlie hace 10 horas
padre
commit
cf37980f72

+ 30 - 15
tools/test_api_batch4.py

@@ -37,6 +37,9 @@ YML = str(ROOT / "yoshop-java/src/main/resources/application.yml")
 TEXT = open(YML, encoding="utf-8").read()
 DB_PWD = re.findall(r"password:\s*(.+)", TEXT)[0].strip().strip("\"'")
 REDIS_PWD = re.findall(r"password:\s*(.+)", TEXT)[1].strip().strip("\"'")
+# 是否校验短信验证码:跟随后端 yoshop.sms.verify-code(未配置时为 false)
+_m = re.search(r"verify-code:\s*(\w+)", TEXT)
+SMS_VERIFY = bool(_m) and _m.group(1).lower() in ("true", "1", "yes")
 
 S = requests.Session()
 S.trust_env = False
@@ -290,28 +293,36 @@ def main():
 
     # ---------------------------------------------------------------- 登录
     print("\n[3] 手机号 + 短信验证码登录")
-    r = post("/api/passport/login", {"mobile": TEST_MOBILE}, token=None)
-    check("缺短信验证码报错", r.get("status") == 500 and "短信验证码不能为空" in (r.get("message") or ""), r)
     r = post("/api/passport/login", {"smsCode": "123456"}, token=None)
     check("缺手机号报错", r.get("status") == 500 and "手机号不能为空" in (r.get("message") or ""), r)
-    redis_del("yoshop:api:sms:" + TEST_MOBILE)
-    r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "123456"}, token=None)
-    check("验证码不存在报错", r.get("status") == 500 and "短信验证码不存在" in (r.get("message") or ""), r)
     r = post("/api/passport/login", {"mobile": "12345", "smsCode": "123456"}, token=None)
     check("手机号格式校验", r.get("status") == 500 and "手机号格式不正确" in (r.get("message") or ""), r)
 
-    _, sms_code, _ = send_sms(TEST_MOBILE)
-    r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "000000"}, token=None)
-    check("验证码错误报错", r.get("status") == 500 and "短信验证码不正确" in (r.get("message") or ""), r)
-    check("验证码错误后次数减一", json.loads(redis_get("yoshop:api:sms:" + TEST_MOBILE)).get("times") == 4,
-          redis_get("yoshop:api:sms:" + TEST_MOBILE))
+    if SMS_VERIFY:
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE}, token=None)
+        check("缺短信验证码报错", r.get("status") == 500 and "短信验证码不能为空" in (r.get("message") or ""), r)
+        redis_del("yoshop:api:sms:" + TEST_MOBILE)
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "123456"}, token=None)
+        check("验证码不存在报错", r.get("status") == 500 and "短信验证码不存在" in (r.get("message") or ""), r)
+
+        _, sms_code, _ = send_sms(TEST_MOBILE)
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "000000"}, token=None)
+        check("验证码错误报错", r.get("status") == 500 and "短信验证码不正确" in (r.get("message") or ""), r)
+        check("验证码错误后次数减一", json.loads(redis_get("yoshop:api:sms:" + TEST_MOBILE)).get("times") == 4,
+              redis_get("yoshop:api:sms:" + TEST_MOBILE))
+
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": sms_code}, token=None)
+        check("登录后验证码用后即焚", redis_get("yoshop:api:sms:" + TEST_MOBILE) is None)
+    else:
+        print("  (短信通道未接入,yoshop.sms.verify-code=false:验证码随便填)")
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE}, token=None)
 
-    r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": sms_code}, token=None)
     login_data = field(r, "data") or {}
     check("登录成功", r.get("status") == 200 and r.get("message") == "登录成功", r)
+    if not SMS_VERIFY:
+        check("空短信验证码也能登录(联调开关)", r.get("status") == 200, r)
     check("返回 userId 与 token", isinstance(login_data.get("userId"), int)
           and re.fullmatch(r"[0-9a-f]{32}", str(login_data.get("token"))) is not None, login_data)
-    check("登录后验证码用后即焚", redis_get("yoshop:api:sms:" + TEST_MOBILE) is None)
     user_id = login_data.get("userId")
     check("新用户写入库", bool(user_id) and db(f"SELECT COUNT(*) FROM yoshop_user WHERE user_id={user_id}")[0][0] == 1)
     nick = db(f"SELECT nick_name, platform FROM yoshop_user WHERE user_id={user_id}")[0] if user_id else ("", "")
@@ -324,9 +335,13 @@ def main():
     check("登录态可用", r.get("status") == 200 and info.get("userId") == user_id, r)
     check("用户信息手机号打码", info.get("mobile") == "139****1111", info.get("mobile"))
 
-    _, sms_code, _ = send_sms(TEST_MOBILE)
-    r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": sms_code},
-             token=None)
+    if SMS_VERIFY:
+        _, sms_code, _ = send_sms(TEST_MOBILE)
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": sms_code},
+                 token=None)
+    else:
+        r = post("/api/passport/login", {"mobile": TEST_MOBILE, "smsCode": "000000"},
+                 token=None)
     check("重复登录复用同一账号", field(r, "data", {}).get("userId") == user_id, r)
 
     # ---------------------------------------------------------------- 微信小程序登录

+ 2 - 1
tools/test_uniapp_contract.py

@@ -18,12 +18,13 @@ import sys
 import requests
 import pymysql
 
+from _root import ROOT
 from redis_probe import cmd
 
 BASE = "http://127.0.0.1:18080"
 MOBILE = "13900002222"
 
-YML = open("../yoshop-java/src/main/resources/application.yml", encoding="utf-8").read()
+YML = (ROOT / "yoshop-java/src/main/resources/application.yml").read_text(encoding="utf-8")
 DB_PWD = re.findall(r"password:\s*(.+)", YML)[0].strip().strip("\"'")
 REDIS_PWD = re.findall(r"password:\s*(.+)", YML)[1].strip().strip("\"'")
 

+ 21 - 4
yoshop-java/src/main/java/cn/yoshop/api/service/ApiPassportService.java

@@ -19,6 +19,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.RequiredArgsConstructor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -75,6 +76,15 @@ public class ApiPassportService {
     private final ApiSettingService apiSettingService;
     private final ClientSettingService clientSettingService;
 
+    /**
+     * 是否校验手机短信验证码。
+     * <p>短信通道尚未接入,联调期默认关闭(false):{@code smsCode} 随便填都能通过。
+     * 接入短信平台后把 {@code yoshop.sms.verify-code} 改为 true 即可恢复严格校验,
+     * 无需改动代码。
+     */
+    @Value("${yoshop.sms.verify-code:false}")
+    private boolean verifySmsCode;
+
     // ------------------------------------------------------------------ 手机号登录
 
     /** 手机号 + 短信验证码登录(自动注册) */
@@ -82,14 +92,21 @@ public class ApiPassportService {
     public Map<String, Object> login(Map<String, Object> form, String platform) {
         String smsCode = Params.str(form, "smsCode", "").trim();
         String mobile = Params.str(form, "mobile", "").trim();
-        if (smsCode.isBlank()) {
-            throw new BizException("短信验证码不能为空");
-        }
         if (mobile.isBlank()) {
             throw new BizException("手机号不能为空");
         }
         checkMobile(mobile);
-        smsService.check(mobile, smsCode);
+        // 短信通道尚未接入,联调期不校验验证码(smsCode 随便填)。
+        // 接入短信平台后把 yoshop.sms.verify-code 置为 true,即恢复与 PHP passport\Login::login
+        // 一致的严格校验:空值报「短信验证码不能为空」+ Redis 比对次数与用后即焚。
+        if (verifySmsCode) {
+            if (smsCode.isBlank()) {
+                throw new BizException("短信验证码不能为空");
+            }
+            smsService.check(mobile, smsCode);
+        } else {
+            log.warn("[passport] 跳过短信验证码校验:mobile={} code={}", mobile, smsCode);
+        }
 
         Map<String, Object> partyData = partyData(form);
         Map<String, Object> wxSession = wxSessionOf(partyData);

+ 4 - 0
yoshop-java/src/main/resources/application.yml

@@ -62,3 +62,7 @@ yoshop:
     # 客户端(小程序/H5)用户登录态:只存用户 ID,业务接口回查用户表
     api-token-prefix: "yoshop:api:token:"
     api-token-ttl: 604800
+  sms:
+    # 是否校验手机短信验证码。短信通道尚未接入,联调期置 false:手机号登录时 smsCode 随便填(含不填)。
+    # 接入短信平台后改为 true 即恢复严格校验(空值报错 + Redis 比对次数 + 用后即焚),无需改代码。
+    verify-code: false

+ 9 - 2
联调说明.md

@@ -125,6 +125,11 @@ NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/vue-cli-service serve
   需登录的接口(购物车、订单、用户中心、售后、充值、收银台等)由接口内部判定并返回 401「请先登录后再访问」
 - 登录:`/api/passport/login`(手机号 + 短信验证码,自动注册)、`/api/passport/loginMpWx`(小程序一键登录)、
   `/api/passport/loginMpWxMobile`(小程序授权手机号);返回 `{userId, token}`,token 有效期 30 天
+- **短信验证码当前不校验**:`application.yml` 的 `yoshop.sms.verify-code: false`(默认),
+  手机号登录时 `smsCode` 随便填、不填都能登录;接入短信平台后改 `true` 即恢复
+  严格校验(空值报「短信验证码不能为空」+ Redis 比对次数 + 用后即焚),无需改代码。
+  uniapp 登录页的本地校验也已同步放开(`pages/login/components/main.vue` 的 SUBMIT_LOGIN 分支)。
+  注:`/api/user/bindMobile`(绑定手机号)不在该开关范围内,仍会校验真实验证码
 - 验证码:`/api/captcha/image` 返回 `{base64, key, md5}`(Redis `yoshop:api:captcha:{key}`,TTL 1800 秒);
   `/api/captcha/sendSmsCaptcha` 校验图形验证码后按手机号每日限 10 次
   (Redis `yoshop:api:sms:send:{mobile}`),验证码写入 `yoshop:api:sms:{mobile}` → `{"code","times"}`;
@@ -136,9 +141,11 @@ NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/vue-cli-service serve
   库内没有支付模板(未配置商户)时跳过验签直接处理,一旦存在支付模板即拒绝回调
 - 出参:驼峰,沿用 store / admin 口径;列表分页结构 `{total, perPage, currentPage, lastPage, data}`
 - 一键验证:
-  - `python tools/test_api_batch1.py`(首页 / 店铺 / 地区 / 文章 / 商品 / 营销,52 项)
+  - `python tools/test_api_batch1.py`(首页 / 店铺 / 地区 / 文章 / 商品 / 营销,53 项)
   - `python tools/test_api_batch2.py`(购物车 / 结算下单 / 订单 / 评价 / 地址,97 项)
   - `python tools/test_api_batch3.py`(用户中心 / 优惠券 / 明细 / 售后 / 充值 / 上传,99 项)
-  - `python tools/test_api_batch4.py`(验证码 / 登录 / 收银台 / 支付回调,77 项)
+  - `python tools/test_api_batch4.py`(验证码 / 登录 / 收银台 / 支付回调,73 项;
+    `yoshop.sms.verify-code: true` 时为 77 项,脚本会自动按开关选择断言)
+  - `python tools/test_uniapp_contract.py`(uniapp 真实 `{form:...}` 结构:setting / 验证码 / 登录 / userInfo,9 项)
 - 仍为骨架的部分:第三方支付下单与查询(微信 / 支付宝商户参数未配置)、支付回调验签、
   短信发送通道、微信头像外链下载;未配置时均返回明确文案,不会静默失败