【Lilishop商城】No4-3.业务逻辑的代码开发,涉及到:会员B端第三方登录的开发-微信小程序登录接口开发

仅涉及后端,全部目录看顶部专栏,代码、文档、接口路径在: 

【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客


全篇会结合业务介绍重点设计逻辑,其中重点包括接口类、业务类,具体的结合源代码分析,源码读起来也不复杂~

谨慎:源代码中有一些注释是错误的,有的注释意思完全相反,有的注释对不上号,我在阅读过程中就顺手更新了,并且在我不会的地方添加了新的注释,所以在读源代码过程中一定要谨慎啊! 

目录

A1.会员登录模块

B1.微信小程序登录接口开发

业务逻辑:

代码逻辑:

        1.实体类Connect   及  分别通过 openid 和 unionid 与账号进行绑定

        2.获取系统里的微信小程序配置信息

        ​​​​​​​3.header的uuid作为 key 从缓存中获取的微信用户 


A1.会员登录模块

B1.微信小程序登录接口开发

微信小程序登录就一个接口,需要传参:临时登录凭证code、手机号码的对称解密的目标密文encryptedData、手机号码的对称解密算法初始向量iv、微信头像、微信用户昵称。header 里面传递 uuid

header的uuid:此次用户登录的uuid,用来防止多次调用接口导致多次调用第三方接口。前端需要根据后端的返回值来判断是否更新 uuid。        

临时登录凭证code:用来获取用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。拿到后用于解密和绑定帐号见:小程序登录 | 微信开放文档

手机号码的对称解密的目标密文encryptedData、手机号码的对称解密算法初始向量iv:是用来获取用户手机号码的,拿到后用于获取账号货或注册。此处使用的是旧的方法,具体可见之前的 No4-1 C1.微信小程序 文档说明

微信头像、微信用户昵称: 拿到后用于账号注册,wx.getUserProfile(Object object) | 微信开放文档

接口接受到参数后会判断是否能拿到账号,拿到这直接返回账号登陆的 Token ;若拿不到则注册账号并返回账号登录的Token。

业务逻辑:

在介绍业务逻辑时,会涉及到一些其他代码结构,有需要说明的就用绿色底纹标注,然后在后面的代码逻辑里面详细介绍。

此时需要新增一个实体类 Connect ,是联合登陆关联表 li_connect 的,用来将账号和第三方用户唯一标识绑定的。

ConnectServiceImpl 类:仅使用的 mybatis-plus 的,没有自定义mapper,针对实体类 li_connect

  1. 先以header的uuid作为 key 从缓存中获取的微信用户信息,若获取到只直接 2.,若获取不到则获取系统里的微信小程序配置信息,然后再结合前端传入的临时登录凭证code ,作为入参调用微信提供的获取接口,从响应中拿到微信小程序端用户基本信息openid、unionid、session_key。并将这些信息以header的uuid作为 key 存到缓存里面
  2. 根据session_key、手机号码对称解密的目标密文encryptedData、手机号码的对称解密算法初始向量iv进行解密,拿到用户手机号码。【解密逻辑看微信提供的~在No4-1里说明了】
  3. 查询手机号码绑定的账号,如果存在会员,则分别通过 openid 和 unionid 与账号进行绑定并存储到li_connect表,并且返回账号的登录 token;如果不存在账号则 4.
  4. 如果不存在会员,则根据手机号、微信头像、微信用户昵称注册会员,用户名也是手机号码拼接的,并且分别通过 openid 和 unionid 与账号进行绑定并存储到li_connect表,由于是注册账号所以也要处理会员注册的小事件【和平台注册的一样不再说明了】,并且最后返回账号的登录 token;

代码逻辑:

//cn.lili.controller.passport.connect.MiniProgramBuyerController
@RestController
@RequestMapping("/buyer/passport/connect/miniProgram")
@Api(tags = "买家端,小程序登录接口")
public class MiniProgramBuyerController {

    @Autowired
    public ConnectService connectService;

    @GetMapping("/auto-login")
    @ApiOperation(value = "小程序登录/自动注册")
    public ResultMessage<Token> autoLogin(@RequestHeader String uuid, WechatMPLoginParams params) {
        params.setUuid(uuid);
        return ResultUtil.data(this.connectService.miniProgramAutoLogin(params));
    }
。。。
}
//cn.lili.modules.connect.serviceimpl.ConnectServiceImpl
@Slf4j
@Service
public class ConnectServiceImpl extends ServiceImpl<ConnectMapper, Connect> implements ConnectService {

    @Autowired
    private SettingService settingService;
    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberTokenGenerate memberTokenGenerate;
    @Autowired
    private Cache cache;
    /**
     * RocketMQ 配置
     */
    @Autowired
    private RocketmqCustomProperties rocketmqCustomProperties;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    @Transactional
    public Token miniProgramAutoLogin(WechatMPLoginParams params) {

        Object cacheData = cache.get(CachePrefix.WECHAT_SESSION_PARAMS.getPrefix() + params.getUuid());
        Map<String, String> map = new HashMap<>(3);
        if (cacheData == null) {
            //通过前端传入的微信返回的登录凭证code ,去换取微信小程序端用户基本信息
            JSONObject json = this.getConnect(params.getCode());
            //存储session key 后续登录用得到
            String sessionKey = json.getStr("session_key");
            String unionId = json.getStr("unionid");
            String openId = json.getStr("openid");
            map.put("sessionKey", sessionKey);
            map.put("unionId", unionId);
            map.put("openId", openId);
            cache.put(CachePrefix.WECHAT_SESSION_PARAMS.getPrefix() + params.getUuid(), map, 900L);
        } else {
            map = (Map<String, String>) cacheData;
        }
        //手机号 绑定 且 自动登录
        return this.phoneMpBindAndLogin(map.get("sessionKey"), params, map.get("openId"), map.get("unionId"));
    }


    /**
     * 通过微信返回等code 获取openid 等信息
     *
     * @param code 微信code
     * @return 微信返回的信息
     */
    public JSONObject getConnect(String code) {
        //获取系统里的微信小程序配置
        WechatConnectSettingItem setting = this.getWechatMPSetting();
        String url = "https://api.weixin.qq.com/sns/jscode2session?" +
                "appid=" + setting.getAppId() + "&" +
                "secret=" + setting.getAppSecret() + "&" +
                "js_code=" + code + "&" +
                "grant_type=authorization_code";
        String content = HttpUtils.doGet(url, "UTF-8", 100, 1000);
        log.error(content);
        return JSONUtil.parseObj(content);
    }

    /**
     * 手机号 绑定 且 自动登录
     *
     * @param sessionKey 微信sessionKey
     * @param params     微信小程序自动登录参数
     * @param openId     微信openid
     * @param unionId    微信unionid
     * @return token
     */
    @Transactional(rollbackFor = Exception.class)
    public Token phoneMpBindAndLogin(String sessionKey, WechatMPLoginParams params, String openId, String unionId) {
        String encryptedData = params.getEncryptedData();
        String iv = params.getIv();
        //使用旧版本解密方式,获取微信信息
        JSONObject userInfo = this.getUserInfo(encryptedData, sessionKey, iv);
        log.info("联合登陆返回:{}", userInfo.toString());
        //拿到手机号码
        String phone = (String) userInfo.get("purePhoneNumber");

        //手机号登录
        LambdaQueryWrapper<Member> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(Member::getMobile, phone);
        //查询手机号码绑定的账号
        Member member = memberService.getOne(lambdaQueryWrapper);
        //如果存在会员,则进行绑定微信openid 和 unionid,并且登录
        if (member != null) {
            //会员绑定 绑定微信小程序
            this.bindMpMember(openId, unionId, member);
            return memberTokenGenerate.createToken(member, true);
        }

        //如果没有会员,则根据手机号注册会员
        Member newMember = new Member("m" + phone, "111111", phone, params.getNickName(), params.getImage());
        memberService.save(newMember);
        newMember = memberService.findByUsername(newMember.getUsername());
        //会员绑定 绑定微信小程序
        this.bindMpMember(openId, unionId, newMember);
        //发送会员注册信息
        applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("new member register", rocketmqCustomProperties.getMemberTopic(), MemberTagsEnum.MEMBER_REGISTER.name(), newMember));
        return memberTokenGenerate.createToken(newMember, true);
    }

    /**
     * 解密,获取微信信息,此类的逻辑是根据微信的加密数据解密算法逻辑开发的,具体见:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
     *  此方法是旧版本~~~详见:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/deprecatedGetPhoneNumber.html
     *  新版本是拿到code后调用HTTPS接口来获取手机号码~~~这个就很简单就是用 HttpUtils 调用接口获取响应就可以了~
     *  详见:
     *      获取手机号码组件:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
     *      获取手机号码接口:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html
     *
     * @param encryptedData 加密信息
     * @param sessionKey    微信sessionKey
     * @param iv            微信揭秘参数
     * @return 用户信息
     */
    public JSONObject getUserInfo(String encryptedData, String sessionKey, String iv) {

        log.info("encryptedData:{},sessionKey:{},iv:{}", encryptedData, sessionKey, iv);
        //被加密的数据
        byte[] dataByte = Base64.getDecoder().decode(encryptedData);
        //加密秘钥
        byte[] keyByte = Base64.getDecoder().decode(sessionKey);
        //偏移量
        byte[] ivByte = Base64.getDecoder().decode(iv);
        try {
            //如果密钥不足16位,那么就补足.  这个if 中的内容很重要
            int base = 16;
            if (keyByte.length % base != 0) {
                int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
                byte[] temp = new byte[groups * base];
                Arrays.fill(temp, (byte) 0);
                System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
                keyByte = temp;
            }
            //初始化  需要导入包
            Security.addProvider(new BouncyCastleProvider());
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
            parameters.init(new IvParameterSpec(ivByte));
            //初始化
            cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
            byte[] resultByte = cipher.doFinal(dataByte);
            if (null != resultByte && resultByte.length > 0) {
                String result = new String(resultByte, StandardCharsets.UTF_8);
                return JSONUtil.parseObj(result);
            }
        } catch (Exception e) {
            log.error("解密,获取微信信息错误", e);
        }
        throw new ServiceException(ResultCode.USER_CONNECT_ERROR);
    }

    /**
     * 会员绑定 绑定微信小程序
     * <p>
     * 如果openid 已经绑定其他账号,则这里不作处理,如果未绑定,则绑定最新的会员
     * 这样,微信小程序注册之后,其他app 公众号页面,都可以实现绑定自动登录功能
     * </p>
     *
     * @param openId  微信openid
     * @param unionId 微信unionid
     * @param member  会员
     */
    private void bindMpMember(String openId, String unionId, Member member) {

        //如果 unionid 不为空  则为账号绑定unionid
        if (CharSequenceUtil.isNotEmpty(unionId)) {
            LambdaQueryWrapper<Connect> lambdaQueryWrapper = new LambdaQueryWrapper();
            lambdaQueryWrapper.eq(Connect::getUnionId, unionId);
            lambdaQueryWrapper.eq(Connect::getUnionType, ConnectEnum.WECHAT.name());
            List<Connect> connects = this.list(lambdaQueryWrapper);
            //只有为绑定过的才会绑定,已绑定过的不会再次绑定!!!!
            if (connects.isEmpty()) {
                Connect connect = new Connect();
                connect.setUnionId(unionId);
                connect.setUserId(member.getId());
                connect.setUnionType(ConnectEnum.WECHAT.name());
                this.save(connect);
            }
        }//如果 openid 不为空  则为账号绑定openid
        if (CharSequenceUtil.isNotEmpty(openId)) {
            LambdaQueryWrapper<Connect> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(Connect::getUnionId, openId);
            lambdaQueryWrapper.eq(Connect::getUnionType, ConnectEnum.WECHAT_MP_OPEN_ID.name());
            List<Connect> connects = this.list(lambdaQueryWrapper);
            //只有为绑定过的才会绑定,已绑定过的不会再次绑定!!!!
            if (connects.isEmpty()) {
                Connect connect = new Connect();
                connect.setUnionId(openId);
                connect.setUserId(member.getId());
                connect.setUnionType(ConnectEnum.WECHAT_MP_OPEN_ID.name());
                this.save(connect);
            }
        }
    }

    /**
     * 获取微信小程序配置
     *
     * @return 微信小程序配置
     */
    private WechatConnectSettingItem getWechatMPSetting() {
        //从数据库中拿到微信 联合登陆设置
        Setting setting = settingService.get(SettingEnum.WECHAT_CONNECT.name());
        //然后将设置转化成使用JavaBean对象。此对象里面是用list存放多种微信应用设置的
        WechatConnectSetting wechatConnectSetting = JSONUtil.toBean(setting.getSettingValue(), WechatConnectSetting.class);

        if (wechatConnectSetting == null) {
            throw new ServiceException(ResultCode.WECHAT_CONNECT_NOT_EXIST);
        }
        //寻找对应对微信小程序登录配置
        for (WechatConnectSettingItem wechatConnectSettingItem : wechatConnectSetting.getWechatConnectSettingItems()) {
            //拿到微信小程序应用的配置
            if (wechatConnectSettingItem.getClientType().equals(ClientTypeEnum.WECHAT_MP.name())) {
                return wechatConnectSettingItem;
            }
        }
        throw new ServiceException(ResultCode.WECHAT_CONNECT_NOT_EXIST);
    }

。。。
}

1.实体类Connect   及  分别通过 openid 和 unionid 与账号进行绑定

先说实体类 Connect, 里面重要的就是下面这三个字段,每一个联合登录关联表都会关联一个账号id、联合(第三方)用户id、联合(第三方)类型。只要联合用户id未关联过账号就会添加一份关联。

联合类型包括:微信的各个产品下的不同openid(小程序、移动应用、网站应用)、微信unionid、QQ的各个产品、微博(不知道微博有没有多个应用,还没用过)、支付宝等等,需要了解第三方后细分哦。

同一个 userid 可能和多个联合用户id 绑定哦。 

但是要注意,通常来说一个现实用户在任何端使用任意第三方进行授权登录时应该都关联同一个账号,但是shop里面可不是,所以需要注意~~~~【为啥?请看完所有的会员登录方式,就明白了】

    @ApiModelProperty("用户id")
    private String userId;

    @ApiModelProperty("联合登录id")
    private String unionId;

    /**
     * @see cn.lili.modules.connect.entity.enums.ConnectEnum
     */
    @ApiModelProperty(value = "联合登录类型")
    private String unionType;

所以  分别通过 openid 和 unionid 与账号进行绑定  这个逻辑也就不用说明了,就是将第三方的用户id与平台账号绑定。

绑定也是先判断此联合用户id是否已存在,也就是是否已绑定过账号,没有则会绑定。

2.获取系统里的微信小程序配置信息

 配置信息都包括appId、appSecret、clientType,这些都是必须向微信申请的,审核通过后微信下发的,每个产品都有自己唯一的,例如shop项目里面微信小程序的和微信网站应用的都是不一样的!

这些信息是属于平台的,所以可以交给运营端管理,由于这些都是小数据单开一个数据表不值得,所以可以使用运营M端的系统设置逻辑,根据 K:V保存起来,V里面保存为 json类型的。​​​​​​​

使用时根据 clientType 区分是哪个产品的。

//WECHAT_CONNECT 的
{
    "wechatConnectSettingItems":[
        {
            "clientType":"PC",
            "appId":"XXXXXXX",
            "appSecret":"XXXXXXX"
        },
        {
            "clientType":"H5",
            "appId":"XXXXXXX",
            "appSecret":"XXXXXXX"
        },
        {
            "clientType":"WECHAT_MP",
            "appId":"XXXXXXX",
            "appSecret":"XXXXXXX"
        }
    ]
}

3.header的uuid作为 key 从缓存中获取的微信用户 

其实不缓存也不会有问题,这里添加缓存是避免多次调用 this.getConnect(params.getCode());方法,该方法最终是会调用微信提供的接口的。而微信的接口都是有使用频率的!!!是有限制的!!!避免由于其他错误导致次数过多后面用户无法进行调用。

另一方面也是如果小程序端调用次数频率过多时,能过提高性能,毕竟同一个用户拿到的信息是一样的,所以先存储起来,如果真的碰到多次频繁调用,就能够直接获取缓存中的信息使用了。

这就是要多注意,前端需要根据后端的响应情况来更新或清除 uuid 哦,否则该逻辑就没用~~~

如果此次登录授权的响应是未成功,那么就不用清除 uuid ,如果登录成功就能清除 uuid 了

055e0a05fddf48d1895e4522ced70e0e.png (1080×2408)

猜你喜欢

转载自blog.csdn.net/vaevaevae233/article/details/128450098
今日推荐