[Lilishop Mall] No4-3. Code development of business logic, involving: development of member B-side third-party login-WeChat applet login interface development

Only the backend is involved, see the top column for all directories, codes, documents, and interface paths are: 

 

[Lilishop Mall] Record the study notes of the B2B2C mall system~


The whole article will combine the business introduction to focus on the design logic, which includes the interface class and business class, and the specific source code analysis, the source code is not complicated to read~

Caution: Some comments in the source code are wrong, some comments have completely opposite meanings, and some comments are not correct. I updated them during the reading process and added new comments where I did not know. So be careful when reading the source code! 

Table of contents

A1. Member login module

B1. Wechat applet login interface development

Business logic:

Code logic:

        1. The entity class Connect and bind the account with openid and unionid respectively

        2. Obtain the configuration information of the WeChat applet in the system

        ​​​​​​​​3. The uuid of the header is used as the key to obtain the WeChat user from the cache 


A1. Member login module

B1. Wechat applet login interface development

There is only one interface for WeChat applet login, and parameters need to be passed: temporary login credential code, target ciphertext encryptedData of symmetrical decryption of mobile phone number, initial vector iv of symmetrical decryption algorithm of mobile phone number, WeChat avatar, and WeChat user nickname. Pass the uuid in the header

The uuid of the header: the uuid of the user login this time, which is used to prevent multiple calls to the third-party interface caused by multiple calls to the interface. The front end needs to judge whether to update the uuid according to the return value of the back end.        

Temporary login credential code: used to obtain the user's unique identifier OpenID, the user's unique identifier UnionID under the WeChat open platform account (if the current Mini Program has been bound to the WeChat open platform account) and the session key session_key. After getting it, it is used to decrypt and bind the account. See: Mini Program Login | WeChat Open Documentation

The target ciphertext encryptedData of the symmetric decryption of the mobile phone number, the initial vector iv of the symmetric decryption algorithm of the mobile phone number : used to obtain the user's mobile phone number, and then used to obtain the account number or registration. The old method is used here, see the previous No4-1 C1. WeChat applet documentation for details ;

WeChat avatar, WeChat user nickname : used for account registration after getting it, wx.getUserProfile(Object object) | WeChat open document

After the interface receives the parameters, it will judge whether the account can be obtained, and if it is obtained, it will directly return the Token for account login; if not, it will register an account and return the Token for account login.

Business logic:

When introducing business logic, some other code structures will be involved. If there is any need to explain, it will be marked with green shading , and then it will be introduced in detail in the following code logic.

At this time, a new entity class Connect needs to be added , which is combined with the login association table li_connect, and is used to bind the account with the unique identifier of the third-party user.

ConnectServiceImpl class: only mybatis-plus used, no custom mapper, for the entity class li_connect

  1. First use the uuid of the header as the key to obtain the WeChat user information from the cache. If it is obtained, it is only 2. If it is not obtained, it obtains the configuration information of the WeChat applet in the system , and then combines the temporary login credential code passed in from the front end. , as an input parameter, call the acquisition interface provided by WeChat, and get the basic information of the WeChat applet user's openid, unionid, and session_key from the response. And store this information in the cache with the uuid of the header as the key
  2. Decrypt according to the session_key, the target ciphertext encryptedData of the symmetrical decryption of the mobile phone number, and the initial vector iv of the symmetrical decryption algorithm of the mobile phone number to obtain the user's mobile phone number. [The decryption logic is provided by WeChat ~ explained in No4-1]
  3. Query the account bound to the mobile phone number. If there is a member, bind the account through openid and unionid respectively and store it in the li_connect table, and return the login token of the account; if there is no account, then 4.
  4. If there is no member, register a member according to the mobile phone number, WeChat avatar, and WeChat user nickname. The user name is also concatenated with the mobile phone number, and is bound to the account through openid and unionid respectively and stored in the li_connect table. Since it is a registered account, it is also To handle the small event of member registration [the same as the platform registration, no longer explained], and finally return the login token of the account;

Code logic:

//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. The entity class Connect   and   bind the account through openid and unionid respectively

Let’s talk about the entity class Connect first. The following three fields are important in it. Each joint login association table will be associated with an account id, joint (third-party) user id, and joint (third-party) type. As long as the joint user id has not been associated with an account, an association will be added.

Union types include: different openids (mini-programs, mobile applications, and website applications) under each product of WeChat, WeChat unionid, each product of QQ, Weibo (I don’t know if Weibo has multiple applications, and I haven’t used it yet), Alipay, etc., you need to understand the third party and then subdivide it.

The same userid may be bound to multiple joint userids. 

But it should be noted that generally speaking, a real user should be associated with the same account when using any third party to authorize login at any end , but this is not the case in the shop, so you need to pay attention~~~~【Why? Please read all the member login methods and you will understand】

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

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

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

Therefore, there is no need to explain the logic of binding the account through openid and unionid respectively, which is to bind the third-party user id with the platform account.

Binding is also to first determine whether the joint user id already exists, that is, whether the account has been bound, if not, it will be bound.

2. Obtain the configuration information of the WeChat applet in the system

 The configuration information includes appId, appSecret, and clientType. These must be applied to WeChat. After the approval, WeChat will issue them. Each product has its own uniqueness. For example, in the shop project, WeChat applets and WeChat website applications are both are different!

This information belongs to the platform, so it can be handed over to the operation end for management. Since these are small data, it is not worth opening a single data table, so you can use the system setting logic of the operation M end to save it according to K: V, and save it in V as json type. ​​​​​​​​

When using it, it is distinguished which product it is based on clientType.

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

 

3. The uuid of the header is used as the key to obtain the WeChat user from the cache 

In fact, there is no problem without caching. Adding caching here is to avoid calling this.getConnect(params.getCode()); method multiple times. This method will eventually call the interface provided by WeChat. And the interface of WeChat is frequently used! ! ! There is a limit! ! ! Avoid users being unable to make calls after too many times due to other errors.

 

On the other hand, if the number of small program calls is too high, the performance can be improved. After all, the information obtained by the same user is the same, so it is stored first. If there are many frequent calls, it can be obtained directly. The information in the cache is used.

This is to pay more attention, the front-end needs to update or clear the uuid according to the response of the back-end, otherwise the logic is useless~~~

If the response of the login authorization is unsuccessful, then there is no need to clear the uuid, if the login is successful, the uuid can be cleared

055e0a05fddf48d1895e4522ced70e0e.png (1080×2408)

Guess you like

Origin blog.csdn.net/vaevaevae233/article/details/128450098