一、项目简要介绍
1、技术介绍
- 前端:微信小程序
- 后端:Java、SpringBoot、MyBatisPlus、RabbitMQ
- 数据库:Redis、MySQL
- 运维:Docker、Rancher容器编排工具
2、项目功能
- 基础点单功能
- JWT登陆校验
- 优惠劵功能(无门槛优惠劵、满减劵、新用户自动发放新人优惠劵)
- 积分功能(积分兑换、等级提升)
- 订单后端校验+优惠劵使用
- 自动生成递增的取餐号(加锁预防高并发问题)
- 15分钟未支付自动取消订单+回滚商品库存和优惠劵数据
3、页面展示
二、封装后端请求
为了代码的可读性和可维护性,可以将请求接口进行封装;需要在根目录下创建一个 http 文件夹,然后在里面创建 request.js 和 api.js 两个文件(名字都可以随意);
1、request.js
reqeust.js 对 wx.request() 进行二次封装
module.exports = {
request: (url, method,token,data = {}) => {
// 拼凑url
let _url = `http://localhost:8093/${url}`
// 发送请求
return new Promise((resolve, reject) => {
// 显示加载界面
wx.showLoading({
title: '正在加载',
})
// 拼装请求
wx.request({
url: _url,
data: data,
method: method,
header: {
'content-type': 'application/json',
'token': token // 将token放在请求头中
},
success: (res) => {
let {code} = res.data
// 请求成功
if (code == 0) {
// 结束加载界面
wx.hideLoading()
// 返回后端数据
resolve(res.data)
}
}
})
})
}
}
2、api.js
请求的接口
// 引入二次封装后的request
const {request} = require('./request')
module.exports = {
/**
* 用户登陆:POST请求
*/
userLogin: (e,token) => {
return request('tea/api/v1/user/login', 'POST', token,{code: e})
},
/**
* 获取商品列表信息:GET请求
*/
getProductList: () => {
return request('tea/api/v1/product/list', 'GET');
},
}
三、组件库的安装使用
这里采用的是 Lin UI 微信小程序原生语法 实现的组件库
1、组件库安装
直接通过git下载 Lin UI 源代码,并将 dist 目录(Lin-UI 组件库)拷贝到自己的项目中。
git clone https://github.com/TaleLin/lin-ui.git
2、组件库的使用
这里采用按需加载组件,在需要使用组件的页面的json配置文件中进行配置
三、JWT登陆校验
JWT是一个开放标准,简单来说就是通过一定的规范生成token,然后可以通过解密算法逆向解密token,尽而获取用户信息;由于生成的 token 是存储在客户端,所以并不占用服务端的内存资源;
微信为每个小程序的用户准备了一套 openId ,是该用户在当前小程序中的唯一标识,后端可以通过 openId 来确定当前操作的用户。前端将登陆凭证 code 发送给后端,后端携带 code 请求微信服务器获取 openId,然后后端生成 token 返回给前端;
1、前端:获取登陆凭证
通过调用 wx.login() 接口获取登陆凭证(code),登陆凭证有效期五分钟;将后端返回的 token 保存到本地storage中
userLogin(){
wx.login({
success(res) {
if (res.code) {
// 将 code 发送给后端
userLogin(res.code).then(res => {
if (res.code == 0) {
// 将token保存到缓存中
wx.setStorageSync('token', res.data.token)
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
},
/**
* 页面渲染时触发
*/
onLoad(){
this.userLogin()
},
2、后端:获取 openid
(1)调用 auth.code2Session 换取 openid
private String getOpenId(String code) throws IOException {
// 构造查询url
String url = "https://api.weixin.qq.com/sns/jscode2session";
url += "?appid=微信开发工具右上角点击详情可见"; // Appid
url += "&secret=登陆小程序管理网页可见";// AppSecret
url += "&js_code=" + code; // 前端传入的登陆凭证
url += "&grant_type=authorization_code";
url += "&connect_redirect=1";
// 构造请求
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
// DefaultHttpClient();
HttpGet httpget = new HttpGet(url); //GET方式
CloseableHttpResponse response = null;
// 配置信息
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000) // 设置连接超时时间(单位毫秒)
.setConnectionRequestTimeout(5000) // 设置请求超时时间(单位毫秒)
.setSocketTimeout(5000) // socket读写超时时间(单位毫秒)
.setRedirectsEnabled(false) // 设置是否允许重定向(默认为true)
.build();
httpget.setConfig(requestConfig); // 由客户端执行(发送)Get请求
// 获取响应实体
String res = null;
response = httpClient.execute(httpget); // 从响应模型中获取响应实体
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
res = EntityUtils.toString(responseEntity);
}
// 释放资源
if (httpClient != null) {
httpClient.close();
}
if (response != null) {
response.close();
}
// 解析响应对象 res,获取 JsonId
JSONObject jo = JSON.parseObject(res);
String openid = jo.getString("openid");
return openid;
}
(2)将用户信息保存到数据库中
可以暂时只创建这三个字段:id、openId、createTime
3、后端:token生成和校验
(1)添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
(2)生成token
private static final String SUBJECT = "luomiaosen"; // JWT主题
private static final long EXPIRE = 1000*60*60*24; // Token 过期时间 24 小时
private static final String SECRET = "skjdlasjdljsajddsad"; // 密钥(!超级重要,不可以泄漏)
private static final String TOKEN_PREFIX = "luomiaosen:"; // 令牌前缀
public static String geneJsonWebToken(UserDO userDO, HttpServletRequest request){
// 生成 token
String token = Jwts.builder().setSubject(SUBJECT)
.claim("id",userDO.getId()) // 进行加密判断的信息1
.claim("agent",request.getHeader("user-agent")) // 进行加密判断的信息2
.setIssuedAt(new Date()) // 设置发布时间
.setExpiration(new Date(System.currentTimeMillis()+EXPIRE)) // 设置过去时间
.signWith(SignatureAlgorithm.HS256, SECRET) // 设置签名方式
.compact(); // 生成token
// 加上前缀
token = TOKEN_PREFIX + token;
return token;
}
(2)校验token
private static final String SECRET = "skjdlasjdljsajddsad"; // 密钥(!超级重要,不可以泄漏)
private static final String TOKEN_PREFIX = "luomiaosen:"; // 令牌前缀
public static Claims checkJWT(String token){
try{
final Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
return claims;
}catch (Exception e){
log.info("解密失败");
return null;
}
}
4、后端:登陆拦截器
(1)登陆拦截器
/**
* 登陆拦截器
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
// 定义线程局部变量
public static ThreadLocal<LoginUser>threadLocal = new ThreadLocal<>();
// 登陆拦截器处理-前处理
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler){
// 1.获取请求传入的token(token可能在请求头,也可能在请求参数上)
String token = request.getHeader("token");
// 2.若token问空,返回false
if(token==null){
// 返回未登陆信息给客户端
CommonUtils.sentJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
// 3.若token不为空,校验token
Claims claims = JWTUtils.checkJWT(token);
// 3.1 校验失败
if(claims == null){
CommonUtils.sentJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
// 3.2 校验成功(获取token中的用户信息)
String idInfo = claims.get("id").toString();
Integer id = Integer.valueOf(idInfo);
// 3.3 传递用户信息
LoginUser loginUser = new LoginUser();
loginUser.setId(id);
threadLocal.set(loginUser);
return true;
}
}
LoginUser.class
/**
* 登陆用户信息
*/
@Data
public class LoginUser {
private Integer id;
// 可以添加其他
}
CommonUtils.sentJsonMessage 方法
public class CommonUtils {
/**
* 返回Json数据
*/
public static void sentJsonMessage(HttpServletResponse response, Object obj) {
// 提供一些功能将转换成Java对象匹配JSON结构
ObjectMapper objectMapper = new ObjectMapper();
// 设置响应类型
response.setContentType("application/json;charset=utf-8");
try {
PrintWriter writer = response.getWriter();
// 将传入对象写入response
writer.print(objectMapper.writeValueAsString(obj));
writer.close();
// 刷新response缓存
response.flushBuffer();
} catch (IOException e) {
log.info("响应Json数据给前端异常:{}",e);
}
}
}
(2)拦截器配置
/**
* 登陆拦截器和放行路径开发配置
*/
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
// 登陆拦截器
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/user/**","/product/**","/order/**")
.excludePathPatterns("/user/login","/product/get/*","/product/list")
.order(0);
}
}
四、发放新人优惠劵
后端通过 openId 来判断用户是否是新用户,若为新用户则发放新人优惠劵,并除了返回token外,还返回 couponMsg 优惠劵信息 以及 show=true,供前端判定是否需要新人优惠劵弹窗;
发放优惠劵采用 RabbitMQ 消息队列的方式,实现高性能、高可用,防止优惠劵方法失败导致用户登陆失败;
1、前端:新人优惠劵弹窗
l-popup 是 Lin UI 的弹窗组件
<l-popup show="{{show}}">
<view>新人优惠劵弹窗内容:自定义</view>
</l-popup>
首页渲染时就触发用户登陆函数,后端返回 show 判断是否需要展示弹窗
/**
* 用户登陆函数
*/
userLogin(){
var that = this
wx.login({
success(res) {
if (res.code) {
userLogin(res.code).then(res => {
if (res.code == 0) {
wx.setStorageSync('token', res.data.token)
// 新增:后端传回 show ,判断是否需要弹窗
if(res.data.show=="true"){
that.data.show = true
}else if(res.data.show=="false"){
that.data.show = false
}
that.setData({
coupon: res.data.couponMsg,
show: that.data.show
})
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
},
/**
* 页面渲染时触发
*/
onLoad(){
this.userLogin()
},
2、后端:用户登陆判断
@Override
@Transactional
public JsonData getUserInfo(String code, HttpServletRequest request) throws IOException {
HashMap<String, Object> map = new HashMap<>();
// 获取用户openId
String openId = getOpenId(code);
// 判断数据库中是否存在该用户
UserDO userDO = userMapper.selectOne(new LambdaQueryWrapper<UserDO>().eq(UserDO::getOpenId, openId));
if(userDO==null){
// 插入新用户
UserDO newUserDO = new UserDO();
newUserDO.setOpenId(openId);
newUserDO.setCreateTime(new Date());
newUserDO.setBonus(0);
userMapper.insert(newUserDO);
// 自动领取新人优惠劵(消息队列)
CouponMQMessage couponMQMessage = new CouponMQMessage();
couponMQMessage.setCouponId(1);
couponMQMessage.setUserId(newUserDO.getId());
rabbitTemplate.convertAndSend(rabbitMQConfig.getCouponEventExchange(),rabbitMQConfig.getCouponRoutingKey(),couponMQMessage);
// 返回token
String token = JWTUtils.geneJsonWebToken(newUserDO, request);
map.put("token",token);
// 获取新人优惠劵信息
List<CouponVO> couponVOS = getCouponMsg();
map.put("couponMsg",couponVOS);
map.put("show","true");
return JsonData.buildSuccess(map);
}
// 存在,返回token信息
String token = JWTUtils.geneJsonWebToken(userDO, request);
map.put("token",token);
map.put("show","false");
return JsonData.buildSuccess(map);
}
3、后端:MQ发放优惠劵
(1)配置MQ
@Configuration
@Data
public class RabbitMQConfig {
/**
* Json方式序列化
*/
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
// 名称
@Value("${mqconfig.coupon_event_exchange}")
private String couponEventExchange;
@Value("${mqconfig.coupon_queue}")
private String couponQueue;
@Value("${mqconfig.coupon_routing_key}")
private String couponRoutingKey;
/**
* 交换机
*/
@Bean
public Exchange couponEventExchange(){
return new TopicExchange(couponEventExchange,true,false);
}
/**
* 队列
*/
@Bean
public Queue couponQueue(){
return new Queue(couponQueue,true,false,false);
}
/**
* 队列与交换机绑定
*/
@Bean
public Binding couponBinding(){
return new Binding(couponQueue,Binding.DestinationType.QUEUE,couponEventExchange,couponRoutingKey,null);
}
}
(2)发送消息
@Resource
private RabbitMQConfig rabbitMQConfig;
@Resource
private RabbitTemplate rabbitTemplate;
// 参数:交换机名称、routingKey、发送的消息
rabbitTemplate.convertAndSend(rabbitMQConfig.getCouponEventExchange(),rabbitMQConfig.getCouponRoutingKey(),couponMQMessage);
(3)接收处理消息
@Component
@RabbitListener(queues = "${mqconfig.coupon_queue}")
public class CouponMQListener {
@Resource
private CouponServiceImpl couponServiceImpl;
/**
* 自动发放优惠劵:注意 发送的消息类型 和 接受消息的类型要一致,我这里是一个类 CouponMQMessage
*/
@RabbitHandler
@Transactional
public void orderHandler(CouponMQMessage couponMQMessage, Message message, Channel channel) throws IOException {
// msgTag消息投递序号
long msgTag = message.getMessageProperties().getDeliveryTag();
// 发放优惠劵
couponServiceImpl.addCouponRecord(couponMQMessage.getCouponId(),couponMQMessage.getUserId());
// 消费成功
channel.basicAck(msgTag, false);
}
}
五、订单后端校验
待补充
六、十五分钟未支付自动取消订单
待补充