小柒味来 - 微信登录与商品浏览功能开发

一、微信登录

1.1 需求分析

在小程序端实现用户微信登录,需要完成以下流程:

  • 小程序获取用户的授权码(code)。
  • code 发送给后端。
  • 后端携带 appidappSecretcode 调用微信官方登录接口,获取 openidsession_key
  • 在数据库中创建用户数据。
  • 生成自定义登录态返回给小程序,小程序本地存储并在后续请求中携带。

存在的问题与需求:

  • 每个 code 只能使用一次,重复使用会报错。
  • 新用户登录需要自动注册并返回主键id。
  • 接口调用需要保证自定义登录态校验有效,防止未授权访问。

1.1.1 产品原型

1.1.2 接口设计

后端请求官方登录效验接口

用户登录接口

接口地址:/user/user/login

请求方式:POST

请求数据类型:application/json

请求示例:

{
  "code": ""
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
userLoginDTO userLoginDTO body true UserLoginDTO UserLoginDTO
code false string

响应示例:

{
    "code": 0,
    "data": {
        "id": 0,
        "openid": "",
        "token": ""
    },
    "msg": ""
}

1.1.3 实现思路

  1. 小程序端调用 wx.login() 获取 code
  2. 后端收到 code 后,请求微信接口:
    • 请求地址:https://api.weixin.qq.com/sns/jscode2session
    • 请求参数:
      • appid
      • secret
      • js_code
      • grant_type=authorization_code
  3. 解析返回数据中的 openid
  4. 如果数据库不存在该 openid,则自动注册为新用户。
  5. 为用户生成 JWT 令牌并返回给小程序端。
  6. 小程序端保存 openidtoken,在业务请求时附带token

1.2 功能实现

1.2.1 微信配置类

qi-commonproperties包下新建

@Component
@ConfigurationProperties(prefix = "qi.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址

}

1.2.2 设计VO

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {
    private Long id;
    private String openid;
    private String token;
}

1.2.3 Controller 层

@RestController
@RequestMapping("/user/user")
@Api(tags="C端用户相关接口")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtProperties jwtProperties;

    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
        log.info("微信用户登录:{}", userLoginDTO.getCode());

        // 微信登录
        User user = userService.wxlogin(userLoginDTO);

        // 生成 JWT
        HashMap<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),
                                         jwtProperties.getUserTtl(),
                                         claims);

        // 封装返回
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();

        return Result.success(userLoginVO);
    }
}

1.2.4 Service 层

public interface UserService {
    User wxLogin(UserLoginDTO userLoginDTO);
}

1.2.5 Service 实现类

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;

    @Autowired
    private UserMapper userMapper;

    @Override
    public User wxlogin(UserLoginDTO userLoginDTO) {
        String openid = getOpenid(userLoginDTO.getCode());

        if (openid == null) {
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }

        // 查询是否已注册
        User user = userMapper.getByOpenid(openid);

        // 新用户注册
        if (user == null) {
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
        }

        return user;
    }

    private String getOpenid(String code) {
        Map<String, String> params = new HashMap<>();
        params.put("appid", weChatProperties.getAppid());
        params.put("secret", weChatProperties.getSecret());
        params.put("js_code", code);
        params.put("grant_type", "authorization_code");

        String json = HttpClientUtil.doGet(WX_LOGIN, params);
        JSONObject jsonObject = JSON.parseObject(json);
        return jsonObject.getString("openid");
    }
}

1.2.6 Mapper 层

@Mapper
public interface UserMapper {

    @Select("select * from user where openid = #{openid}")
    User getByOpenid(String openid);

    void insert(User user);
}

XML 映射文件:

<mapper namespace="com.sky.mapper.UserMapper">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into user (openid, name, phone, sex, id_number, avatar, create_time)
        values (#{openid}, #{name}, #{phone}, #{sex}, #{id_number}, #{avatar}, #{create_time})
    </insert>
</mapper>

1.2.7 Token 校验拦截器

编写拦截器校验小程序端发来的请求携带的token是否合法

@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        String token = request.getHeader(jwtProperties.getUserTokenName());

        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            BaseContext.setCurrentId(userId);
            return true;
        } catch (Exception ex) {
            response.setStatus(401);
            return false;
        }
    }
}

1.3 测试

  1. 在微信开发者工具导入小程序代码,填写自己的 appid(后续会详细介绍uniapp,这里我们先直接导入)
  1. 运行登录功能,小程序获取 code 并请求后端。

  2. 后端获取 openid 并生成 token,数据库记录用户信息。

  1. 后续业务请求携带 token,后端通过拦截器校验有效性。

二、商品浏览功能

这个部分较之前做过的查询并无较大区别,不再展示代码,这里给出接口设计,可以实操锻炼

2.1 查询分类

接口地址:/user/category/list

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
type type query false integer(int32)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data array Category
createTime string(date-time)
createUser integer(int64)
id integer(int64)
name string
sort integer(int32)
status integer(int32)
type integer(int32)
updateTime string(date-time)
updateUser integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": [
        {
            "createTime": "",
            "createUser": 0,
            "id": 0,
            "name": "",
            "sort": 0,
            "status": 0,
            "type": 0,
            "updateTime": "",
            "updateUser": 0
        }
    ],
    "msg": ""
}

2.2 根据分类id查询菜品

接口地址:/user/dish/list

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
categoryId categoryId query false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data array DishVO
categoryId integer(int64)
categoryName string
description string
flavors array DishFlavor
dishId integer
id integer
name string
value string
id integer(int64)
image string
name string
price number
status integer(int32)
updateTime string(date-time)
msg string

响应示例:

{
    "code": 0,
    "data": [
        {
            "categoryId": 0,
            "categoryName": "",
            "description": "",
            "flavors": [
                {
                    "dishId": 0,
                    "id": 0,
                    "name": "",
                    "value": ""
                }
            ],
            "id": 0,
            "image": "",
            "name": "",
            "price": 0,
            "status": 0,
            "updateTime": ""
        }
    ],
    "msg": ""
}

2.3 根据分类id查询套餐

接口地址:/user/setmeal/list

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
categoryId categoryId query false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data array Setmeal
categoryId integer(int64)
createTime string(date-time)
createUser integer(int64)
description string
id integer(int64)
image string
name string
price number
status integer(int32)
updateTime string(date-time)
updateUser integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": [
        {
            "categoryId": 0,
            "createTime": "",
            "createUser": 0,
            "description": "",
            "id": 0,
            "image": "",
            "name": "",
            "price": 0,
            "status": 0,
            "updateTime": "",
            "updateUser": 0
        }
    ],
    "msg": ""
}

2.4 根据套餐id查询包含的菜品

接口地址:/user/setmeal/dish/{id}

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
id id path true integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data array DishItemVO
copies integer(int32)
description string
image string
name string
msg string

响应示例:

{
    "code": 0,
    "data": [
        {
            "copies": 0,
            "description": "",
            "image": "",
            "name": ""
        }
    ],
    "msg": ""
}

关于微信登录与商品浏览功能就基本实现了,下一章小柒味来 - 菜品套餐缓存与购物车模块实现


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub