Przeglądaj źródła

Merge remote-tracking branch 'origin/ScrmStores' into ScrmStores

yjwang 5 dni temu
rodzic
commit
64d9a6da03

+ 134 - 0
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -6,11 +6,13 @@ import cn.hutool.core.date.DateTime;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.app.annotation.Login;
 import com.fs.app.param.*;
+import com.fs.app.utils.UniVerifyUtil;
 import com.fs.app.utils.WxUtil;
 import com.fs.common.VerifyCodeUtil;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.service.ISmsService;
 import com.fs.common.utils.ServletUtils;
@@ -71,6 +73,9 @@ public class AppLoginController extends AppBaseController{
     @Autowired
     private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
 
+    @Autowired
+    private UniVerifyUtil uniVerifyUtil;
+
     @ApiOperation("注册app用户")
     @PostMapping("/register")
     @RepeatSubmit
@@ -965,4 +970,133 @@ public class AppLoginController extends AppBaseController{
         return R.ok().put("data", res);
     }
 
+    @ApiOperation("UniApp一键登录(安全)")
+    @PostMapping("/loginByUniverify")
+    @Transactional
+    public R loginByUniverify(@RequestBody FsUserLoginParam param) {
+        if (StringUtils.isEmpty(param.getAccessToken()) || StringUtils.isEmpty(param.getOpenId())) {
+            return R.error("一键登录凭证不完整");
+        }
+        String phone;
+        try {
+            phone = uniVerifyUtil.getPhoneNumber(param.getAccessToken(), param.getOpenId());
+        } catch (CustomException e) {
+            return R.error(e.getMessage());
+        } catch (Exception e) {
+            logger.error("UniVerify获取手机号失败: {}", e.getMessage());
+            return R.error("获取手机号失败,请重试");
+        }
+        if (StringUtils.isEmpty(phone)) {
+            return R.error("获取手机号失败");
+        }
+        param.setPhone(phone);
+        return handleSecurePhoneLogin(param);
+    }
+
+    @ApiOperation("UniApp一键登录选择账号(安全)")
+    @PostMapping("/loginByUserIdUniverify")
+    @Transactional
+    public R loginByUserIdUniverify(@RequestBody FsUserLoginParam param) {
+        if (StringUtils.isEmpty(param.getAccessToken()) || StringUtils.isEmpty(param.getOpenId())) {
+            return R.error("一键登录凭证不完整");
+        }
+        String phone;
+        try {
+            phone = uniVerifyUtil.getPhoneNumber(param.getAccessToken(), param.getOpenId());
+        } catch (CustomException e) {
+            return R.error(e.getMessage());
+        } catch (Exception e) {
+            logger.error("UniVerify获取手机号失败: {}", e.getMessage());
+            return R.error("获取手机号失败,请重试");
+        }
+        if (StringUtils.isEmpty(phone)) {
+            return R.error("获取手机号失败");
+        }
+        param.setPhone(phone);
+        return handleSecurePhoneLoginByUserId(param);
+    }
+
+    /**
+     * UniApp 一键登录(安全):手机号已由 uniCloud 校验,不信任客户端传入
+     * <ul>
+     *   <li>无用户 → 自动注册新用户</li>
+     *   <li>单用户 → 直接登录</li>
+     *   <li>多用户 → 返回 users 列表,前端选择后调 /loginByUserIdUniverify</li>
+     * </ul>
+     */
+    private R handleSecurePhoneLogin(FsUserLoginParam param) {
+        String phone = param.getPhone();
+        List<FsUser> usersByPhone = findUsersByPhone(phone);
+        if (CollectionUtil.isEmpty(usersByPhone)) {
+            // 新用户自动注册
+            String appid = openProperties.getAppId();
+            Integer bizType = fsCoursePlaySourceConfigService.resolveBizTypeByAppId(appid);
+            if (bizType == null) {
+                bizType = appid.equals("wx7796a33a71912e32") ? 2 : 1;
+            }
+            param.setAppId(appid);
+            param.setBizType(bizType);
+            FsUser newUser = createNewUser(param);
+            if (StringUtils.isNotEmpty(param.getJpushId())) {
+                updateExistingUserJpushId(newUser, param.getJpushId());
+            }
+            updateLoginDevice(newUser.getUserId(), param.getLoginDevice(), param.getSource());
+            return generateTokenAndReturn(newUser);
+        }
+        if (usersByPhone.size() == 1) {
+            FsUser user = usersByPhone.get(0);
+            // 历史用户首次一键登录:手机号还是明文,加密后落库
+            if (user.getUnionId() == null) {
+                if (user.getPhone() != null && user.getPhone().length() <= 11) {
+                    FsUser fsUser = new FsUser();
+                    fsUser.setUserId(user.getUserId());
+                    fsUser.setPhone(encryptPhone(phone));
+                    userMapper.updateFsUser(fsUser);
+                }
+                return R.ok().put("isNew", true).put("phone", encryptPhone(phone));
+            }
+            if (StringUtils.isNotEmpty(param.getJpushId())) {
+                updateExistingUserJpushId(user, param.getJpushId());
+            }
+            updateLoginDevice(user.getUserId(), param.getLoginDevice(), param.getSource());
+            return generateTokenAndReturn(user);
+        }
+        // 多账号场景,脱敏后返回让前端选择
+        for (FsUser fsUser : usersByPhone) {
+            fsUser.setPhone("***********");
+        }
+        return R.ok().put("users", usersByPhone);
+    }
+
+    /**
+     * 多账号场景下按 userId 登录,手机号已由 uniCloud 校验
+     */
+    private R handleSecurePhoneLoginByUserId(FsUserLoginParam param) {
+        List<FsUser> usersByPhone = findUsersByPhone(param.getPhone());
+        if (CollectionUtil.isEmpty(usersByPhone)) {
+            return R.error("账号不存在,请先注册账号");
+        }
+        if (usersByPhone.size() == 1) {
+            FsUser user = usersByPhone.get(0);
+            if (StringUtils.isNotEmpty(param.getJpushId())) {
+                updateExistingUserJpushId(user, param.getJpushId());
+            }
+            updateLoginDevice(user.getUserId(), param.getLoginDevice(), param.getSource());
+            return generateTokenAndReturn(user);
+        }
+        if (param.getUserId() == null) {
+            return R.error("请选择登录账号");
+        }
+        for (FsUser fsUser : usersByPhone) {
+            if (fsUser.getUserId().equals(param.getUserId())) {
+                if (StringUtils.isNotEmpty(param.getJpushId())) {
+                    updateExistingUserJpushId(fsUser, param.getJpushId());
+                }
+                updateLoginDevice(fsUser.getUserId(), param.getLoginDevice(), param.getSource());
+                return generateTokenAndReturn(fsUser);
+            }
+        }
+        return R.error("账号不匹配,请检查账号");
+    }
+
 }

+ 3 - 0
fs-user-app/src/main/java/com/fs/app/param/FsUserLoginParam.java

@@ -41,4 +41,7 @@ public class FsUserLoginParam implements Serializable {
 
     /** 业务类型:1看课 2商城 */
     private Integer bizType;
+
+    /** UniApp 一键登录(univerify)access_token,由客户端从 uni.login 获取 */
+    private String accessToken;
 }

+ 97 - 0
fs-user-app/src/main/java/com/fs/app/utils/UniVerifyUtil.java

@@ -0,0 +1,97 @@
+package com.fs.app.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.http.HttpUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * UniApp 一键登录(univerify)工具类,通过 uniCloud 云函数 URL 化接口换取手机号。
+ * <p>
+ * 客户端调用 uni.login 拿到 access_token + openid 后,传给后端,
+ * 后端调用云函数换取真实手机号,避免信任客户端传入的手机号。
+ * </p>
+ *
+ * <h3>yml 配置</h3>
+ * <pre>{@code
+ * univerify:
+ *   cloud-function-url: https://fc-mp-xxx.next.bspapp.com/getLoginMobile
+ * }</pre>
+ *
+ * @author fs
+ */
+@Component
+public class UniVerifyUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(UniVerifyUtil.class);
+
+    @Value("${univerify.cloud-function-url:https://fc-mp-788b1d8f-c300-465c-9e38-c986b162ae93.next.bspapp.com/getLoginMobile}")
+    private String cloudFunctionUrl;
+
+    /**
+     * 使用 access_token 和 openid 向 uniCloud 换取真实手机号
+     *
+     * @param accessToken 客户端从 uni.login 获取的 access_token
+     * @param openId      客户端从 uni.login 获取的 openid
+     * @return 真实手机号
+     * @throws CustomException 凭证不完整、服务未配置、云函数返回失败时抛出
+     */
+    public String getPhoneNumber(String accessToken, String openId) {
+        if (StringUtils.isBlank(cloudFunctionUrl)) {
+            throw new CustomException("一键登录服务未配置,请联系管理员");
+        }
+        if (StringUtils.isAnyBlank(accessToken, openId)) {
+            throw new CustomException("一键登录凭证不完整");
+        }
+
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("access_token", accessToken);
+        requestBody.put("openid", openId);
+
+        String response = HttpUtils.doPost(cloudFunctionUrl, requestBody.toJSONString());
+        log.info("UniVerify 换号响应: {}", response);
+        if (StringUtils.isBlank(response)) {
+            throw new CustomException("获取手机号失败");
+        }
+
+        JSONObject json = JSON.parseObject(response);
+        if (Boolean.FALSE.equals(json.getBoolean("success"))) {
+            JSONObject error = json.getJSONObject("error");
+            if (error != null) {
+                throw new CustomException(StringUtils.defaultIfBlank(error.getString("message"), "获取手机号失败"));
+            }
+            throw new CustomException("获取手机号失败");
+        }
+
+        Integer code = json.getInteger("code");
+        if (code != null && code != 0) {
+            String message = json.getString("message");
+            if (StringUtils.isBlank(message)) {
+                message = json.getString("errMsg");
+            }
+            throw new CustomException(StringUtils.defaultIfBlank(message, "获取手机号失败"));
+        }
+
+        String phoneNumber = json.getString("phoneNumber");
+        if (StringUtils.isBlank(phoneNumber) && json.containsKey("data")) {
+            Object dataObj = json.get("data");
+            if (dataObj instanceof JSONObject) {
+                phoneNumber = ((JSONObject) dataObj).getString("phoneNumber");
+            } else if (dataObj instanceof String) {
+                JSONObject data = JSON.parseObject((String) dataObj);
+                if (data != null) {
+                    phoneNumber = data.getString("phoneNumber");
+                }
+            }
+        }
+        if (StringUtils.isBlank(phoneNumber)) {
+            throw new CustomException("获取手机号失败");
+        }
+        return phoneNumber;
+    }
+}