Parcourir la source

feat(miniprogram): 新增微信小程序一键登录功能

- 在MainStudent实体类中新增openid、unionId、sessionKey字段
- 实现miniAppLogin方法,支持通过code和phoneCode进行小程序登录
- 集成WechatMiniAppUtil工具类,实现微信接口调用
- 创建miniappAuthController控制器,提供登录相关接口
- 添加协议服务接口和实现,支持协议类型查询
- 配置微信小程序相关配置类WechatMiniAppConfig
- 返回登录成功后的token、用户信息及是否为新用户标识
西格玛许 il y a 1 semaine
Parent
commit
240b8f2d74

+ 13 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/config/WechatMiniAppConfig.java

@@ -0,0 +1,13 @@
+package org.dromara.main.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "wechat.miniapp")
+public class WechatMiniAppConfig {
+    private String appId;
+    private String appSecret;
+}

+ 46 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/controller/miniapp/miniappAuthController.java

@@ -0,0 +1,46 @@
+package org.dromara.main.controller.miniapp;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.dromara.main.domain.vo.MainAgreementVo;
+import org.dromara.main.domain.vo.MiniappLoginVo;
+import org.dromara.main.service.IMainAgreementService;
+import org.dromara.main.service.IMainStudentService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/miniapp/auth")
+public class miniappAuthController {
+    private final IMainAgreementService agreementService;
+    private final IMainStudentService studentService;
+    @SaIgnore
+    @GetMapping("/agreement")
+    public R<MainAgreementVo> getAgreement(@RequestParam String type) {
+        return R.ok(agreementService.selectByType(type));
+    }
+    /**
+     * 微信小程序一键登录
+     */
+    @SaIgnore
+    @PostMapping("/login")
+    public R<MiniappLoginVo> wechatLogin(@RequestBody Map<String, String> body) {
+        String code = body.get("code");
+        String phoneCode = body.get("phoneCode");
+
+        if (code == null || code.isEmpty() || phoneCode == null || phoneCode.isEmpty()) {
+            return R.fail("登录参数不完整");
+        }
+        return R.ok(studentService.miniAppLogin(code, phoneCode));
+    }
+    @PostMapping("/logout")
+    public R<Void> logout() {
+        StpUtil.logout(); // 如果废弃版使用了自定义的 StudentLoginHelper,请改为相应的登出调用
+        return R.ok("退出成功");
+    }
+}

+ 55 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainAgreement.java

@@ -0,0 +1,55 @@
+package org.dromara.main.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("main_agreement")
+public class MainAgreement extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 协议标题
+     */
+    private String title;
+
+    /**
+     * 协议富文本内容
+     */
+    private String content;
+
+    /**
+     * 协议类型: service-服务协议, privacy-隐私政策
+     */
+    private String type;
+
+    /**
+     * 协议版本号
+     */
+    private String version;
+
+    /**
+     * 删除标志(0代表存在 2代表删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+    /**
+     * 创建部门
+     */
+    private Long createDept;
+}
+

+ 13 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/MainStudent.java

@@ -134,4 +134,17 @@ public class MainStudent extends BaseEntity {
      */
     private String remark;
 
+    /**
+     * 微信小程序 openid
+     */
+    private String openid;
+    /**
+     * 微信小程序 unionId
+     */
+    private String unionId;
+    /**
+     * 微信小程序 sessionKey
+     */
+    private String sessionKey;
+
 }

+ 15 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MainAgreementVo.java

@@ -0,0 +1,15 @@
+package org.dromara.main.domain.vo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.main.domain.MainAgreement;
+
+import java.io.Serializable;
+
+@Data
+@AutoMapper(target = MainAgreement.class)
+public class MainAgreementVo implements Serializable {
+    private String title;
+    private String content;
+    private String version;
+}

+ 19 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/domain/vo/MiniappLoginVo.java

@@ -0,0 +1,19 @@
+package org.dromara.main.domain.vo;
+
+
+import lombok.Data;
+
+/**
+ * 小程序登录成功后返回前端的数据
+ */
+@Data
+public class MiniappLoginVo {
+    private String token;
+    private Long studentId;
+    private String mobile;
+    private String name;
+    private String avatarUrl;
+    private Boolean isNewUser;
+    private Boolean hasResume;  // 是否有简历
+}
+

+ 9 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/mapper/MainAgreementMapper.java

@@ -0,0 +1,9 @@
+package org.dromara.main.mapper;
+
+import com.sun.tools.javac.Main;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.main.domain.MainAgreement;
+import org.dromara.main.domain.vo.MainAgreementVo;
+
+public interface MainAgreementMapper extends BaseMapperPlus<MainAgreement, MainAgreementVo> {
+}

+ 10 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainAgreementService.java

@@ -0,0 +1,10 @@
+package org.dromara.main.service;
+
+import org.dromara.main.domain.vo.MainAgreementVo;
+
+public interface IMainAgreementService {
+    /**
+     * 根据类型查询协议
+     */
+    MainAgreementVo selectByType(String type);
+}

+ 4 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/IMainStudentService.java

@@ -4,6 +4,8 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.main.domain.bo.MainStudentBo;
 import org.dromara.main.domain.vo.MainStudentVo;
+import org.dromara.main.domain.vo.MiniappLoginVo;
+
 
 import java.util.Collection;
 import java.util.List;
@@ -42,4 +44,6 @@ public interface IMainStudentService {
     Boolean updateStatus(Long id,String status);
 
     Boolean updateUserType(Long id,String userType);
+
+    MiniappLoginVo miniAppLogin(String code, String phoneCode);
 }

+ 21 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainAgreementServiceImpl.java

@@ -0,0 +1,21 @@
+package org.dromara.main.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import org.dromara.main.domain.MainAgreement;
+import org.dromara.main.domain.vo.MainAgreementVo;
+import org.dromara.main.mapper.MainAgreementMapper;
+import org.dromara.main.service.IMainAgreementService;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class MainAgreementServiceImpl implements IMainAgreementService {
+    private final MainAgreementMapper baseMapper;
+    @Override
+    public MainAgreementVo selectByType(String type) {
+        // 直接根据 type 查询唯一的协议
+        return baseMapper.selectVoOne(new LambdaQueryWrapper<MainAgreement>()
+            .eq(MainAgreement::getType, type));
+    }
+}

+ 90 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/service/impl/MainStudentServiceImpl.java

@@ -1,11 +1,16 @@
 package org.dromara.main.service.impl;
 
+import cn.dev33.satoken.stp.StpUtil;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.json.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.main.domain.MainStudent;
@@ -14,16 +19,22 @@ import org.dromara.main.domain.MainStudentExperience;
 import org.dromara.main.domain.MainStudentProject;
 import org.dromara.main.domain.bo.MainStudentBo;
 import org.dromara.main.domain.vo.MainStudentVo;
+
+import org.dromara.main.domain.vo.MiniappLoginVo;
 import org.dromara.main.mapper.MainStudentEducationMapper;
 import org.dromara.main.mapper.MainStudentExperienceMapper;
 import org.dromara.main.mapper.MainStudentMapper;
 import org.dromara.main.mapper.MainStudentProjectMapper;
 import org.dromara.main.service.IMainStudentService;
+import org.dromara.main.utils.WechatMiniAppUtil;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+
 @RequiredArgsConstructor
 @Service
 public class MainStudentServiceImpl implements IMainStudentService {
@@ -123,4 +134,83 @@ public class MainStudentServiceImpl implements IMainStudentService {
         return baseMapper.updateById(student) > 0;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public MiniappLoginVo miniAppLogin(String code, String phoneCode) {
+        // 1. 调用微信接口, 用 code 换取 openid 和 session_key
+        JSONObject sessionData = WechatMiniAppUtil.code2Session(code);
+        if (sessionData == null) {
+            throw new ServiceException("微信登录失败,请重试");
+        }
+        String openid = sessionData.getStr("openid");
+        String sessionKey = sessionData.getStr("session_key");
+        String unionId = sessionData.getStr("unionid");
+        if (StringUtils.isBlank(openid)) {
+            throw new ServiceException("获取微信openid失败");
+        }
+        // 2. 获取手机号
+        JSONObject phoneInfo = WechatMiniAppUtil.getPhoneNumberByCode(phoneCode);
+        if (phoneInfo == null) {
+            throw new ServiceException("获取手机号失败,请重试");
+        }
+        String phoneNumber = phoneInfo.getStr("purePhoneNumber");
+        if (StringUtils.isBlank(phoneNumber)) {
+            throw new ServiceException("获取手机号失败");
+        }
+        // 3. 查询或创建学员账号
+        boolean isNewUser = false;
+
+        // 优先根据 openid 查询
+        MainStudent student = baseMapper.selectOne(
+            Wrappers.lambdaQuery(MainStudent.class).eq(MainStudent::getOpenid, openid)
+        );
+        if (student != null) {
+            // 已有记录,更新最新数据
+            student.setSessionKey(sessionKey);
+            student.setMobile(phoneNumber);
+            if (StringUtils.isNotBlank(unionId)) {
+                student.setUnionId(unionId);
+            }
+            baseMapper.updateById(student);
+        } else {
+            // openid 未查询到,根据手机号查询
+            student = baseMapper.selectOne(
+                Wrappers.lambdaQuery(MainStudent.class).eq(MainStudent::getMobile, phoneNumber)
+            );
+
+            if (student != null) {
+                // 绑定到已有手机号账号
+                student.setOpenid(openid);
+                student.setSessionKey(sessionKey);
+                if (StringUtils.isNotBlank(unionId)) {
+                    student.setUnionId(unionId);
+                }
+                baseMapper.updateById(student);
+            } else {
+                // 新用户,执行创建
+                isNewUser = true;
+                student = new MainStudent();
+                student.setName("用户_" + RandomUtil.randomString(6));
+                student.setGender("");
+                student.setOpenid(openid);
+                student.setSessionKey(sessionKey);
+                student.setUnionId(unionId);
+                student.setMobile(phoneNumber);
+                student.setUserType("2"); // 2: 标识为普通用户/新学员
+                student.setStatus("0");   // 0: 正常状态
+                baseMapper.insert(student);
+            }
+        }
+        // 4. 执行登录生成 token(如果你的项目用的是其他的 LoginHelper,请替换)
+        StpUtil.login(student.getId());
+        String token = StpUtil.getTokenValue();
+        // 5. 组合组装返回结果
+        MiniappLoginVo result = new MiniappLoginVo();
+        result.setToken(token);
+        result.setStudentId(student.getId());
+        result.setMobile(student.getMobile());
+        result.setName(student.getName());
+        result.setIsNewUser(isNewUser);
+        return result;
+    }
 }

+ 71 - 0
ruoyi-modules/ruoyi-main/src/main/java/org/dromara/main/utils/WechatMiniAppUtil.java

@@ -0,0 +1,71 @@
+package org.dromara.main.utils;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.main.config.WechatMiniAppConfig;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class WechatMiniAppUtil {
+    private static WechatMiniAppConfig config;
+    public WechatMiniAppUtil(WechatMiniAppConfig config) {
+        WechatMiniAppUtil.config = config;
+    }
+
+    private static final String CODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
+    /**
+     * 通过 code 换取 openid 和 session_key
+     */
+    public static JSONObject code2Session(String code) {
+        String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
+            CODE_2_SESSION_URL, config.getAppId(), config.getAppSecret(), code);
+        try {
+            String response = HttpUtil.get(url);
+            JSONObject result = JSONUtil.parseObj(response);
+            if (result.containsKey("errcode") && result.getInt("errcode") != 0) {
+                log.error("微信code2Session失败: {}", result.getStr("errmsg"));
+                return null;
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("调用微信code2Session接口异常", e);
+            return null;
+        }
+    }
+    /**
+     * 通过 phoneCode 获取手机号 (新版API)
+     */
+    public static JSONObject getPhoneNumberByCode(String phoneCode) {
+        String tokenUrl = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+            config.getAppId(), config.getAppSecret());
+        try {
+            String tokenResponse = HttpUtil.get(tokenUrl);
+            JSONObject tokenResult = JSONUtil.parseObj(tokenResponse);
+            if (tokenResult.containsKey("errcode")) {
+                log.error("获取access_token失败: {}", tokenResult.getStr("errmsg"));
+                return null;
+            }
+            String accessToken = tokenResult.getStr("access_token");
+
+            // 使用 access_token 获取手机号
+            String phoneUrl = String.format("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s", accessToken);
+            JSONObject requestBody = new JSONObject();
+            requestBody.set("code", phoneCode);
+
+            String phoneResponse = HttpUtil.post(phoneUrl, requestBody.toString());
+            JSONObject phoneResult = JSONUtil.parseObj(phoneResponse);
+
+            if (phoneResult.getInt("errcode") != 0) {
+                log.error("获取手机号失败: {}", phoneResult.getStr("errmsg"));
+                return null;
+            }
+            return phoneResult.getJSONObject("phone_info");
+        } catch (Exception e) {
+            log.error("调用微信获取手机号接口异常", e);
+            return null;
+        }
+    }
+}