Просмотр исходного кода

修改订单列表待支付去掉已取消的订单

qmj 3 дней назад
Родитель
Сommit
46fd5fdbe4

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.claude/homunculus/observations.jsonl


+ 1 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/user/dto/OAuthLoginDto.java

@@ -14,7 +14,7 @@ public class OAuthLoginDto {
     /** 三方渠道 apple/google/line */
     private String provider;
 
-    /** 凭证:Apple identityToken / Google idToken / LINE accessToken */
+    /** 凭证:Apple identityToken / Google idToken / LINE authorization code */
     private String credential;
 
     /** 推送字段(与现有登录一致) */

+ 4 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/utils/ezPay/EzPay.java

@@ -183,8 +183,10 @@ public class EzPay {
             return false;
         }
         String decrypted = EzPayEncryptUtil.decrypt(resultHex, config.getHashKey(), config.getHashIV());
-        // decrypted 形如 BarCode=/ABC1234&IsExist=Y
-        return decrypted != null && decrypted.matches(".*IsExist=Y(\\b|$).*");
+        boolean exist = decrypted != null && decrypted.matches(".*IsExist=Y(\b|$).*");
+        // decrypted 形如 CellphoneBarcode=%2FABC1234&IsExist=Y
+        log.info("[EzPay] checkBarCode decrypted={}, isExist={}", decrypted, exist);
+        return exist;
     }
 
     /**

+ 67 - 10
ruoyi-admin/src/main/java/com/ruoyi/app/utils/oauth/OAuthVerifyService.java

@@ -15,6 +15,10 @@ import com.ruoyi.common.utils.MessageUtils;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.NameValuePair;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
@@ -25,6 +29,7 @@ import org.springframework.stereotype.Service;
 
 import java.net.URL;
 import java.net.URLEncoder;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 
@@ -34,7 +39,7 @@ import java.util.List;
  * <ul>
  *   <li>Apple:nimbus 验 ES256 identityToken + 校 iss/aud/exp → sub(忽略 email)</li>
  *   <li>Google:tokeninfo HTTP 验真 + 校 audience → sub</li>
- *   <li>LINE:v2/profile HTTP(Bearer)→ userId</li>
+ *   <li>LINE:code 换 access_token → v2/profile → userId</li>
  * </ul>
  * <p>注:Google 走 tokeninfo 而非 firebase-admin,避免 FirebaseApp+服务账号初始化;各家真实 token + clientId 需联调验证。</p>
  *
@@ -56,12 +61,20 @@ public class OAuthVerifyService {
     private String googleTokeninfoUrl;
     @Value("${oauth.line.profile-url}")
     private String lineProfileUrl;
+    @Value("${oauth.line.token-url}")
+    private String lineTokenUrl;
+    @Value("${oauth.line.client-id}")
+    private String lineClientId;
+    @Value("${oauth.line.client-secret}")
+    private String lineClientSecret;
+    @Value("${oauth.line.redirect-uri}")
+    private String lineRedirectUri;
 
     /**
      * 校验 provider 凭证,返回稳定的 providerUid。
      *
      * @param provider   apple/google/line
-     * @param credential Apple identityToken / Google idToken / LINE accessToken
+     * @param credential Apple identityToken / Google idToken / LINE authorization code
      */
     public String verify(String provider, String credential) {
         if (provider == null || provider.isEmpty()) {
@@ -172,19 +185,43 @@ public class OAuthVerifyService {
         }
     }
 
-    /** LINE:v2/profile HTTP(Bearer accessToken),返回 userId。 */
-    private String verifyLine(String accessToken) {
+    /**
+     * LINE:authorization code → 换 access_token → v2/profile 取 userId。
+     *
+     * <p>标准 LINE Login 流程:前端拿到授权 code 后传给后端,后端用 code + clientSecret + clientId
+     * 向 {@code https://api.line.me/oauth2/v2.1/token} 换 access_token(form-urlencoded),
+     * 再用 access_token 调 v2/profile 取稳定的 userId 作为 providerUid。
+     *
+     * @param code LINE 授权码(前端授权后获得,一次性)
+     * @return LINE userId
+     */
+    private String verifyLine(String code) {
         try {
-            log.debug("[OAuth][LINE] accessToken={}", accessToken);
-            JSONObject json = JSONObject.parseObject(httpGet(lineProfileUrl, accessToken));
-            log.debug("[OAuth][LINE] profile 返回: {}", json);
-            if (json == null) {
+            log.debug("[OAuth][LINE] code={}", code);
+            // 1. code 换 access_token
+            JSONObject token = JSONObject.parseObject(httpPostForm(lineTokenUrl, new String[][]{
+                    {"grant_type", "authorization_code"},
+                    {"code", code},
+                    {"redirect_uri", lineRedirectUri},
+                    {"client_id", lineClientId},
+                    {"client_secret", lineClientSecret}
+            }));
+            log.debug("[OAuth][LINE] token 返回: {}", token);
+            if (token == null || token.getString("access_token") == null) {
+                log.warn("[OAuth][LINE] 换 token 失败: {}", token);
+                throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
+            }
+            String accessToken = token.getString("access_token");
+            // 2. access_token 换用户信息
+            JSONObject profile = JSONObject.parseObject(httpGet(lineProfileUrl, accessToken));
+            log.debug("[OAuth][LINE] profile 返回: {}", profile);
+            if (profile == null) {
                 log.warn("[OAuth][LINE] profile 返回空");
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
             }
-            String userId = json.getString("userId");
+            String userId = profile.getString("userId");
             if (userId == null || userId.isEmpty()) {
-                log.warn("[OAuth][LINE] 无 userId: {}", json);
+                log.warn("[OAuth][LINE] 无 userId: {}", profile);
                 throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
             }
             log.info("[OAuth][LINE] 校验通过 userId={}", userId);
@@ -214,4 +251,24 @@ public class OAuthVerifyService {
             }
         }
     }
+
+    /** 简单 POST application/x-www-form-urlencoded(LINE 换 token 用)。带超时,防止 LINE 慢/不可达拖垮线程。 */
+    private String httpPostForm(String url, String[][] form) throws Exception {
+        try (CloseableHttpClient client = HttpClients.createDefault()) {
+            HttpPost post = new HttpPost(url);
+            post.setConfig(RequestConfig.custom()
+                    .setConnectTimeout(5000)
+                    .setSocketTimeout(10000)
+                    .setConnectionRequestTimeout(5000)
+                    .build());
+            List<NameValuePair> pairs = new ArrayList<>();
+            for (String[] kv : form) {
+                pairs.add(new BasicNameValuePair(kv[0], kv[1]));
+            }
+            post.setEntity(new UrlEncodedFormEntity(pairs, "UTF-8"));
+            try (CloseableHttpResponse resp = client.execute(post)) {
+                return EntityUtils.toString(resp.getEntity(), "UTF-8");
+            }
+        }
+    }
 }

+ 9 - 1
ruoyi-admin/src/main/resources/application.yml

@@ -58,7 +58,15 @@ oauth:
     client-id: com.twanmsdyh.app
     tokeninfo-url: https://oauth2.googleapis.com/tokeninfo
   line:
-    # 用 accessToken 换用户信息的端点(一般不改)
+    # Channel ID(LINE Developers Console)
+    client-id: "2010891313"
+    # Channel Secret(换 token 用,勿泄露)
+    client-secret: "880fb850c17a3399d207100144a1cb67"
+    # 授权回调地址(须与 LINE Console 回调白名单 + 前端拿 code 时一致;LINE 仅校验一致性,不实际回调)
+    redirect-uri: https://backend.amazewayhk.com/auth/line/callback
+    # 用 code 换 access_token 的端点(一般不改)
+    token-url: https://api.line.me/oauth2/v2.1/token
+    # 用 access_token 换用户信息的端点(一般不改)
     profile-url: https://api.line.me/v2/profile
 
 # 开发环境配置

+ 1 - 1
specs/017-oauth-login/spec.md

@@ -25,7 +25,7 @@ C 端 app 现仅支持「手机号 + 短信验证码」登录(`/infouser/user/
 ## Functional Requirements
 
 - **FR-001**: 提供 `POST /infouser/user/oauthLogin {provider, credential, ...}`,后端校验 provider 凭证换取稳定 providerUid。
-- **FR-002**: 凭证校验三选一:Apple=验 ES256 identityToken 取 sub;Google=tokeninfo HTTP 验真取 sub 并校 audience;LINE=v2/profile HTTP 取 userId。
+- **FR-002**: 凭证校验三选一:Apple=验 ES256 identityToken 取 sub;Google=tokeninfo HTTP 验真取 sub 并校 audience;LINE=前端传授权 code,后端用 code+clientSecret+clientId 向 `oauth2/v2.1/token` 换 access_token,再调 v2/profile 取 userId(Authorization Code 流程,不直接收前端 accessToken)
 - **FR-003**: 按 (provider, providerUid) 查 `info_user_oauth`:命中→校验用户 status/del_flag 正常后直接签发 token(claim `provider`)返回;未命中→缓存 {provider,providerUid} 到 Redis(短TTL),返回 `needPhone` + tempKey。
 - **FR-004**: 提供 `POST /infouser/user/oauthBindPhone {tempKey, phone, code, ...}`:取回缓存的 providerUid + 验短信码(复用 lodeing 逻辑,含万能码 8888)→ `getuser(phone)`:已注册→关联;未注册→`createUser` 新建;随后 insert `info_user_oauth(user_id, provider, provider_uid)`。
 - **FR-005**: 新建用户昵称统一用手机号(与现有 createUser 一致),avatar 留空,不用 provider 的昵称/头像。

Некоторые файлы не были показаны из-за большого количества измененных файлов