微信登录功能实现 采用HTTPS 调用 1 GET https://api.weixin.qq.com/sns/jscode2session
请求参数
属性
类型
必填
说明
appid
string
是
小程序 appId
secret
string
是
小程序 appSecret
js_code
string
是
登录时获取的 code,可通过wx.login 获取
grant_type
string
是
授权类型,此处只需填写 authorization_code
返回参数
属性
类型
说明
session_key
string
会话密钥
unionid
string
用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明 。
errmsg
string
错误信息,请求失败时返回
openid
string
用户唯一标识
errcode
int32
错误码,请求失败时返回
功能测试 第一二项参数为小程序自带,第三项参数为每次调用时的唯一code,第四项参数为固定值。
可以获得唯一的用户标识openid,查看我们的开发文档:
尝试为用户写登录功能。
代码实现 首先在配置文件写用户的令牌生成配置信息。
写用户的DTO和VO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.sky.dto;import lombok.Data;import java.io.Serializable;@Data public class UserLoginDTO implements Serializable { private String code; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.sky.vo;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;@Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserLoginVO implements Serializable { private Long id; private String openid; private String token; }
继续三件套,controller->service->mapper,先写controller层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import ...@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); Map<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); } }
写service实现层
1 2 3 4 5 6 7 8 9 10 11 package com.sky.service;import com.sky.dto.UserLoginDTO;import com.sky.entity.User;public interface UserService { User wxLogin (UserLoginDTO userLoginDTO) ; }
它的实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Service @Slf4j public class UserServiceImpl implements UserService { public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session" ; @Autowired private WeChatProperties properties; @Autowired private UserMapper userMapper; 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> map = new HashMap <>(); map.put("appid" , properties.getAppid()); map.put("secret" , properties.getSecret()); map.put("js_code" , code); map.put("grant_type" , "authorization_code" ); String json = HttpClientUtil.doGet(WX_LOGIN,map); JSONObject jsonObject = JSON.parseObject(json); String openid = jsonObject.getString("openid" ); return openid; } }
mapper层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.sky.mapper;import com.sky.entity.User;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Select;@Mapper public interface UserMapper { @Select("select * from user where openid = #{openid}") User getByOpenid (String openid) ; void insert (User user) ; }
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <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},#{idNumber},#{avatar},#{createTime}) </insert > </mapper >
继续完善令牌校验,登录之后保存的token值,此代码没有手搓,是复制原有的管理员令牌改写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @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()); log.info("当前用户id:" , userId); BaseContext.setCurrentId(userId); return true ; } catch (Exception ex) { response.setStatus(401 ); return false ; } } }
同时需要去配置类里也注入此bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; protected void addInterceptors (InterceptorRegistry registry) { log.info("开始注册自定义拦截器..." ); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**" ) .excludePathPatterns("/admin/employee/login" ); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**" ) .excludePathPatterns("/user/user/login" ) .excludePathPatterns("/user/shop/status" ); } @Bean public Docket docket1 () { ApiInfo apiInfo = new ApiInfoBuilder () .title("苍穹外卖项目接口文档" ) .version("2.0" ) .description("苍穹外卖项目接口文档" ) .build(); Docket docket = new Docket (DocumentationType.SWAGGER_2) .groupName("管理端接口" ) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin" )) .paths(PathSelectors.any()) .build(); return docket; } @Bean public Docket docket2 () { ApiInfo apiInfo = new ApiInfoBuilder () .title("苍穹外卖项目接口文档" ) .version("2.0" ) .description("苍穹外卖项目接口文档" ) .build(); Docket docket = new Docket (DocumentationType.SWAGGER_2) .groupName("用户端接口" ) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user" )) .paths(PathSelectors.any()) .build(); return docket; } private static String FILELOCATION = "C:\\Users\\zhangbin\\Desktop\\sky-take-out\\sky-server\\src\\main\\resources\\uploads/" ; protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html" ).addResourceLocations("classpath:/META-INF/resources/" ); registry.addResourceHandler("/webjars/**" ).addResourceLocations("classpath:/META-INF/resources/webjars/" ); registry.addResourceHandler("/uploads/**" ) .addResourceLocations("file:" + FILELOCATION); } protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); converter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,converter); } }
菜品缓存 问题:用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大,从而导致系统响应慢、用户体验差。
解决思路:通过redis缓存查到的数据,如果数据没有发生改变,直接显示上次缓存的数据而不是查数据库
代码实现:
修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list (Long categoryId) { String key = "dish_" + categoryId; List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if (list != null && !list.isEmpty()) { return Result.success(list); } Dish dish = new Dish (); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE); list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key, list); return Result.success(list); }
功能测试
可以通过如下方式进行测试:查看控制台sql、前后端联调、查看Redis中的缓存数据。
套餐缓存 Spring Cache
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:EHCache、Caffeine和Redis。
maven坐标为
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > <version > 2.7.3</version > </dependency >
常用注解
常用注解
说明
@EnableCaching
开启缓存注解功能,通常加在启动类上
@Cacheable
在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut
将方法的返回值放到缓存中
@CacheEvict
将一条或多条数据从缓存中删除
实现思路
导入Spring Cache和Redis相关maven坐标。
在启动类上加入@EnableCaching注解,开启缓存注解功能。
在用户端接口SetmealController的 list 方法上加入@Cacheable注解。
在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解。
代码实现
在用户端接口SetmealController的 list 方法上加入@Cacheable注解:
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/list") @ApiOperation("根据分类id查询套餐") @Cacheable(cacheNames = "setmealCache", key = "#categoryId") public Result<List<Setmeal>> list (Long categoryId) { Setmeal setmeal = new Setmeal (); setmeal.setCategoryId(categoryId); setmeal.setStatus(StatusConstant.ENABLE); List<Setmeal> list = setmealService.list(setmeal); return Result.success(list); }
在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解:
1 2 3 @CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId") @CacheEvict(cacheNames = "setmealCache", allEntries = true)
功能测试
通过前后端联调方式来进行测试,同时观察redis中缓存的套餐数据。
添加购物车 需求分析和设计 产品原型
接口设计
数据库设计
字段名
数据类型
说明
备注
id
bigint
主键
自增
name
varchar(32)
商品名称
冗余字段
image
varchar(255)
商品图片路径
冗余字段
user_id
bigint
用户id
逻辑外键
dish_id
bigint
菜品id
逻辑外键
setmeal_id
bigint
套餐id
逻辑外键
dish_flavor
varchar(50)
菜品口味
number
int
商品数量
amount
decimal(10,2)
商品单价
冗余字段
create_time
datetime
创建时间
代码开发
1 2 3 4 5 6 7 8 @Data public class ShoppingCartDTO implements Serializable { private Long dishId; private Long setmealId; private String dishFlavor; }
根据添加购物车接口创建ShoppingCartController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController("userShoppingCart") @RequestMapping("/user/shoppingCart") @Api(tags = "C端购物车相关接口") @Slf4j public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService; @PostMapping("/add") @ApiOperation("添加购物车") public Result add (@RequestBody ShoppingCartDTO shoppingCartDTO) { log.info("添加购物车,商品信息为 {}" , shoppingCartDTO); shoppingCartService.addShoppingCart(shoppingCartDTO); return Result.success(); } }
1 2 3 4 5 6 7 8 9 public interface ShoppingCartService { void addShoppingCart (ShoppingCartDTO shoppingCartDTO) ; }
创建ShoppingCartServiceImpl实现类,并实现add方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Service @Slf4j public class ShoppingCartServiceImpl implements ShoppingCartService { @Autowired private ShoppingCartMapper shoppingCartMapper; @Autowired private DishMapper dishMapper; @Autowired private SetmealMapper setmealMapper; @Override public void addShoppingCart (ShoppingCartDTO shoppingCartDTO) { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); Long userId = BaseContext.getCurrentId(); shoppingCart.setUserId(userId); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); if (list != null && !list.isEmpty()) { ShoppingCart cart = list.get(0 ); cart.setNumber(cart.getNumber() + 1 ); shoppingCartMapper.updateNumberById(cart); } else { Long dishId = shoppingCartDTO.getDishId(); if (dishId != null ) { Dish dish = dishMapper.getById(dishId); shoppingCart.setName(dish.getName()); shoppingCart.setImage(dish.getImage()); shoppingCart.setAmount(dish.getPrice()); } else { Long setmealId = shoppingCartDTO.getSetmealId(); Setmeal setmeal = setmealMapper.getById(setmealId); shoppingCart.setName(setmeal.getName()); shoppingCart.setImage(setmeal.getImage()); shoppingCart.setAmount(setmeal.getPrice()); } shoppingCart.setNumber(1 ); shoppingCart.setCreateTime(LocalDateTime.now()); shoppingCartMapper.insert(shoppingCart); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Mapper public interface ShoppingCartMapper { List<ShoppingCart> list (ShoppingCart shoppingCart) ; @Update("update shopping_cart set number = #{number} where id = #{id}") void updateNumberById (ShoppingCart shoppingCart) ; @Insert("insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) " + "VALUES (#{name}, #{image}, #{userId}, #{dishId}, #{setmealId}, #{dishFlavor}, #{number}, #{amount}, #{createTime})") void insert (ShoppingCart shoppingCart) ; }
创建ShoppingCartMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.sky.mapper.ShoppingCartMapper" > <select id ="list" resultType ="com.sky.entity.ShoppingCart" > select * from shopping_cart <where > <if test ="userId != null" > and user_id = #{userId} </if > <if test ="setmealId != null" > and setmeal_id = #{setmealId} </if > <if test ="dishId != null" > and dish_id = #{dishId} </if > <if test ="dishFlavor != null" > and dish_flavor = #{dishFlavor} </if > </where > </select > </mapper >
功能测试
可以通过如下方式进行测试:查看控制台sql、Swagger接口文档测试、前后端联调。
查看购物车 需求分析和设计 产品原型
接口设计
代码开发
在ShoppingCartController中创建查看购物车的方法:
1 2 3 4 5 6 @GetMapping("/list") @ApiOperation("查看购物车") public Result<List<ShoppingCart>> list () { List<ShoppingCart> list = shoppingCartService.showShoppingCart(); return Result.success(list); }
在ShoppingCartService接口中声明查看购物车的方法:
1 List<ShoppingCart> showShoppingCart () ;
在ShoppingCartServiceImpl中实现查看购物车的方法:
1 2 3 4 5 6 7 8 @Override public List<ShoppingCart> showShoppingCart () { ShoppingCart shoppingCart = new ShoppingCart ().builder() .userId(BaseContext.getCurrentId()) .build(); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); return list; }
功能测试 可以通过接口文档进行测试,最后完成前后端联调测试即可。
清空购物车 需求分析和设计 产品原型
接口设计
代码开发
在ShoppingCartController中创建清空购物车的方法:
1 2 3 4 5 6 @DeleteMapping("/clean") @ApiOperation("清空购物车") public Result clean () { shoppingCartService.cleanShoppingCart(); return Result.success(); }
在ShoppingCartService接口中声明清空购物车的方法:
1 void cleanShoppingCart () ;
在ShoppingCartServiceImpl中实现清空购物车的方法:
1 2 3 4 5 @Override public void cleanShoppingCart () { Long userId = BaseContext.getCurrentId(); shoppingCartMapper.deleteByUserId(userId); }
在ShoppingCartMapper接口中创建根据用户id清空购物车的方法:
1 2 @Delete("delete from shopping_cart where user_id = #{userId}") void deleteByUserId (Long userId) ;
功能测试 通过Swagger接口文档进行测试,通过后再前后端联调测试即可。
删除购物车中一个商品 需求分析和设计 产品原型
接口设计
接口设计 代码开发
在ShoppingCartController中创建删除购物车中一个商品的方法:
1 2 3 4 5 6 7 @PostMapping("/sub") @ApiOperation("删除购物车中一个商品") public Result sub (@RequestBody ShoppingCartDTO shoppingCartDTO) { log.info("删除购物车中一个商品:{}" , shoppingCartDTO); shoppingCartService.subShoppingCart(shoppingCartDTO); return Result.success(); }
在ShoppingCartService接口中声明删除购物车中一个商品的方法:
1 void subShoppingCart (ShoppingCartDTO shoppingCartDTO) ;
在ShoppingCartServiceImpl中实现删除购物车中一个商品的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void subShoppingCart (ShoppingCartDTO shoppingCartDTO) { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); Long userId = BaseContext.getCurrentId(); shoppingCart.setUserId(userId); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); if (list != null && !list.isEmpty()) { shoppingCart = list.get(0 ); Integer number = shoppingCart.getNumber(); if (number == 1 ) { shoppingCartMapper.deleteById(shoppingCart.getId()); } else { shoppingCart.setNumber(number - 1 ); shoppingCartMapper.updateNumberById(shoppingCart); } } }
在ShoppingCartMapper接口中创建根据id删除购物车中一个商品的方法:
1 2 @Delete("delete from shopping_cart where id = #{id}") void deleteById (Long id) ;
功能测试 通过Swagger接口文档进行测试,通过后再前后端联调测试即可。