一.项目介绍
1.项目内容
苍穹外卖是一款为大学学子设计的校园外卖服务软件,旨在提供便捷的食堂外卖送至宿舍的服务。该软件包含系统管理后台和用户端(微信小程序)两部分,支持在线浏览菜品、添加购物车、下单等功能,并由学生兼职提供跑腿送餐服务。
2.技术栈
SpringBoot+Vue+Mybatis+Mysql+Redis+Nginx
3.Nginx
网页-->nginx-->服务器
nginx反向代理优势:
1.提高访问速度(nginx可以做缓存)
2.进行负载均衡(将大量请求均匀分发请求)
3.保证后端服务的安全
# 反向代理,处理管理端发送的请求
location /api/ {
proxy_pass http://localhost:8080/admin/;
#proxy_pass http://webservers/admin/;
}
# 反向代理,处理用户端发送的请求
location /user/ {
proxy_pass http://webservers/user/;
}
4.Swagger
@Bean
public Docket docket() {
log.info("准备生成接口文档");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
常用注解
5.HttpClient
1.概述:用来提供高效的,丰富的http协议的用户端编程工具包
2.核心api
- HttpClient
- HttpClients
- CloseableHttpClient
- HttpGet
- HttpPost
3.简单使用
发送请求步骤
1.创建HttpClient对象
2.创建Http请求对象
3.调用HttpClient的execute方法发送请求
发送Get请求
@Test
public void testGET() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet getstatus = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求
CloseableHttpResponse response = httpClient.execute(getstatus);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
HttpEntity entity = response.getEntity();
if (entity != null) {
String body = EntityUtils.toString(entity);
System.out.println(body);
}
//关闭资源
response.close();
httpClient.close();
}
发送POST请求
@Test
public void testPOST() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpPost post = new HttpPost("http://localhost:8080/admin/employee/login");
//构造请求的数据
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", "admin");
jsonObject.put("password", "123456");
StringEntity entity = new StringEntity(jsonObject.toString());
entity.setContentEncoding("UTF-8");
entity.setContentType("application/json");
post.setEntity(entity);
//发送请求
CloseableHttpResponse response = httpClient.execute(post);
//获取服务端返回的状态码
int statuscode = response.getStatusLine().getStatusCode();
System.out.println(statuscode);
//具体的响应数据
HttpEntity entity1 = response.getEntity();
if (entity1 != null) {
String body = EntityUtils.toString(entity1);
System.out.println(body);
}
//关闭资源
response.close();
httpClient.close();
}
6.微信小程序
概述:
- 开发者可以快速地开发一个小程序。小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验。
- 微信开发平台:微信开放平台
接入流程
- 注册
- 小程序信息完善
- 开发小程序
- 提交审核和发布
注册并且完善信息
微信开发者工具开发小程序
二.具体实现
一.登录功能
用户注册,输入密码-->对密码进行md5加密进行存储-->用户进行登录,密文解码进行比对
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密码比对,先进行md5加密再进行密码比较
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回实体对象
return employee;
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
二.公共字段自动填充
自定义注解AutoFill,用于标识需要公共字段自定义填充的方法
自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
在Mapper上加入AutoFill注解
public enum OperationType {
UPDATE,
INSERT
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
OperationType value();
}
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void AutoFillCut() {}
/**
* 前置通知,为公共字段进行赋值
*/
@Before("AutoFillCut()")
public void AutoFill(JoinPoint joinPoint) throws Exception {
log.info("AutoFill start");
//获取当前数据库操作的类型
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
//获取当前被拦截方法的操作实体
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
return;
}
Object entity=args[0];
//准备赋值的数据
LocalDateTime now= LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//为实体进行赋值
if (operationType == OperationType.INSERT) {
Method setCrateTime= entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCrateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setCrateTime.invoke(entity,now);
setCrateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} else if (operationType == OperationType.UPDATE) {
Method setUpdateTime=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
}
}
}
三.员工管理
新增员工
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工{}",employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
void save(EmployeeDTO employeeDTO);
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO,employee);
employee.setStatus(StatusConstant.ENABLE);
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录人的id
//TODO 后期改为当前用户的id
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
employeeMapper.insert(employee);
}
@Insert("INSERT INTO employee(name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user, status) " +
"VALUES (#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})")
void insert(Employee employee);
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
String message = ex.getMessage();
if (message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXIST;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
ThreadLocal
它为每个线程提供了一个独立的变量副本,使得每个线程可以独立地访问和修改自己的变量副本
一个请求一个线程
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
拦截器
BaseContext.setCurrentId(empId);
//新增员工时设置当前记录人的id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
查询员工
PageHelper 是一个基于 MyBatis 的分页插件,用于简化分页查询的实现。
它通过 MyBatis 的拦截器机制,自动在 SQL 查询中添加分页逻辑.
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
log.info("查询员工{}",employeePageQueryDTO);
PageResult pageResult = employeeService.page(employeePageQueryDTO);
return Result.success(pageResult);
}
PageResult page(EmployeePageQueryDTO employeePageQueryDTO);
@Override
public PageResult page(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
编辑员工
@PutMapping
@ApiOperation("修改员工信息")
public Result update(@RequestBody EmployeeDTO employeedao) {
employeeService.update(employeedao);
return Result.success();
}
void update(EmployeeDTO employee);
@Override
public void update(EmployeeDTO employeedao) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeedao,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
<update id="update" parameterType="Employee">
update employee
<set>
<if test="name!=null">name = #{name},</if>
<if test="username!=null">username = #{username},</if>
<if test="password!=null">password = #{password},</if>
<if test="phone!=null">phone = #{phone},</if>
<if test="sex!=null">sex = #{sex},</if>
<if test="idNumber!=null">id_number = #{idNumber},</if>
<if test="updateTime!=null">update_time = #{updateTime},</if>
<if test="updateUser!=null">update_user = #{updateUser},</if>
<if test="status!=null">status = #{status}, </if>
</set>
where id = #{id}
</update>
四.菜品管理
新增菜品
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishdao) {
log.info("新增菜品{}",dishdao);
dishService.saveWithFlavor(dishdao);
return Result.success();
}
public void saveWithFlavor(DishDTO dishdao);
@PostMapping
@Override
@Transactional
public void saveWithFlavor(DishDTO dishdao) {
//向菜品表插入1条数据
Dish dish = new Dish();
BeanUtils.copyProperties(dishdao, dish);
dishMapper.insert(dish);
//向口味表插入n条数据
//获取insert语句的主键值
long dish_id = dish.getId();
List<DishFlavor> flavors=dishdao.getFlavors();
if (flavors!=null&&flavors.size()>0){
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dish_id));
dishFloarMapper.insertBatch(flavors);
}
}
@AutoFill(OperationType.INSERT)
void insert(Dish dish);
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish(name,category_id,price,image,description,status,create_time,update_time,create_user,update_user)
values
(#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
void insertBatch(List<DishFlavor> flavors);
<insert id="insertBatch">
insert into dish_flavor(dish_id, name, value)
values
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId}, #{df.name}, JSON_ARRAY(#{df.value}))
</foreach>
</insert>
查询菜品
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> GetDish(DishPageQueryDTO dto){
log.info("菜品查询");
PageResult list =dishService.getDish(dto);
return Result.success(list);
}
PageResult getDish(DishPageQueryDTO dto);
public PageResult getDish(DishPageQueryDTO dto) {
PageHelper.startPage(dto.getPage(),dto.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dto);
return new PageResult(page.getTotal(),page.getResult());
}
Page<DishVO> pageQuery(DishPageQueryDTO dto);
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.* ,c.name as categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name !=null">
and d.name like concat('%', #{name},'%')
</if>
<if test="categoryId !=null">
and d.category_id = #{category_id}
</if>
<if test="status!=null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
删除菜品
@DeleteMapping()
@ApiOperation("删除菜品")
public Result delete(@RequestParam List<Long> ids) {
log.info("删除菜品{}",ids);
dishService.delete(ids);
return Result.success();
}
@Transactional
@Override
public void delete(List<Long> ids) {
//起售中的菜品不能删除
for (Long id : ids) {
Dish dish = dishMapper.geibyid(id);
if (dish.getStatus() == StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//被套餐关联的菜品不能删除
List<Long> SetmealIds = setmealDishMapper.getSetmealDishIdsBydishlId(ids);
if (SetmealIds!=null&&SetmealIds.size()>0){
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
/*可以删除一个菜品,也可以删除多个菜品
for (Long id : ids) {
dishMapper.delete(id);
//删除菜品后,关联的口味也需要删除
dishFloarMapper.delete(id);
}*/
//优化,根据菜品id集合批量删除
dishMapper.deletes(ids);
dishFloarMapper.deletes(ids);
}
void deletes(List<Long> ids);
<delete id="deletes">
delete from dish where id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>
修改菜品
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishdao) {
dishService.update(dishdao);
return Result.success();
}
void update(DishDTO dishdao);
public void update(DishDTO dishdao) {
//修改菜品表
Dish dish = new Dish();
BeanUtils.copyProperties(dishdao, dish);
dishMapper.updatedish(dish);
//修改口味表,先删除所有口味,在插入传过来的口味
dishFloarMapper.delete(dishdao .getId());
List<DishFlavor> flavors=dishdao.getFlavors();
if (flavors!=null&&flavors.size()>0){
flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dish.getId());});
flavors.forEach(dishFlavor -> dishFlavor.setValue(dishFlavor.getValue().toString()));
dishFloarMapper.insertBatch(flavors);
}
}
五.套餐管理
新增套餐
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
void saveWithDish(SetmealDTO setmealDTO);
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id
Long setmealId = setmeal.getId();
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal
(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
#{createUser}, #{updateUser})
</insert>
<insert id="insertBatch" parameterType="list">
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
</foreach>
</insert>
查询套餐
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
int pageNum = setmealPageQueryDTO.getPage();
int pageSize = setmealPageQueryDTO.getPageSize();
PageHelper.startPage(pageNum, pageSize);
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select
s.*,c.name categoryName
from
setmeal s
left join
category c
on
s.category_id = c.id
<where>
<if test="name != null">
and s.name like concat('%',#{name},'%')
</if>
<if test="status != null">
and s.status = #{status}
</if>
<if test="categoryId != null">
and s.category_id = #{categoryId}
</if>
</where>
order by s.create_time desc
</select>
修改套餐
@PutMapping
@ApiOperation("修改套餐")
public Result updateSetmeal(@RequestBody SetmealDTO setmealdto){
log.info("修改套餐{}",setmealdto);
setmealService.updateSetmeal(setmealdto);
return Result.success();
}
void updateSetmeal(SetmealDTO setmealdto);
public void updateSetmeal(SetmealDTO setmealdto) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealdto, setmeal);
//修改套餐信息
setmealMapper.updateSetmeal(setmeal);
//修改对应的套餐菜品
Long setmealId = setmeal.getId();
//删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
setmealDishMapper.deleteBySetmealId(setmealId);
List<SetmealDish> setmealDishes = setmealdto.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
setmealDishMapper.insertBatch(setmealDishes);
}
<update id="updateSetmeal">
update setmeal
<set>
<if test="categoryId!=null">category_id = #{categoryId},</if>
<if test="name!=null">name = #{name},</if>
<if test="price!=null">price = #{price},</if>
<if test="description!=null">description = #{description}, </if>
<if test="image!=null">image = #{image},</if>
<if test="status!=null">status = #{status},</if>
</set>
where id = #{id}
</update>
删除套餐
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
setmealService.deleteBatch(ids);
return Result.success();
}
void deleteBatch(List<Long> ids);
public void deleteBatch(List<Long> ids) {
//起售中的套餐无法删除
for (Long id : ids) {
Setmeal setmeal = setmealMapper.getsetmealbyid(id);
if(setmeal.getStatus()== StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
}
for (Long id : ids) {
//删除套餐表中的数据
setmealMapper.deleteById(id);
//删除套餐菜品关系表中的数据
setmealDishMapper.deleteBySetmealId(id);
}
}
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long id);
六.店铺状态设置
@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;
@PutMapping("/{status}")
@ApiOperation("管理端设置店铺营业状态")
public Result setStatus(@PathVariable Integer status){
log.info("设置店铺的营业状态为{}",status == 1 ? "营业中":"打样中");
redisTemplate.opsForValue().set("shop_status", status);
return Result.success();
}
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get("shop_status");
return Result.success(status);
}
}
@RestController("userShopController")
@RequestMapping("/user/shop")
@Slf4j
@Api(tags = "用户端店铺相关接口")
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get("shop_status");
return Result.success(status);
}
}
七.微信小程序登录
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
//微信登录
User user = userservice.wxLogin(userLoginDTO);
//为微信用户生成jwt令牌
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);
}
User wxLogin(UserLoginDTO userLoginDTO);
public User wxLogin(UserLoginDTO userLoginVO) {
Map<String, String> params = new HashMap<String, String>();
params.put("appid",weChatProperties.getAppid());
params.put("secret",weChatProperties.getSecret());
params.put("js_code",userLoginVO.getCode());
params.put("grant_type","authorization_code");
//调用微信接口服务,获得当前用户的openid
String res = HttpClientUtil.doGet(WX_LOGIN,params);
JSONObject jsonObject = JSONObject.parseObject(res);
String openid = jsonObject.getString("openid");
//判断openid,空失败
if (openid == null || openid.equals("")) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
//判断当前用户是否为新用户
User user = userMapper.getByUserId(openid);
//新用户自动注册
if (user == null){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
//返回这个用户对象
return user;
}
void insert(User user);
<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>
八.缓存
缓冲菜品
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问过大,访问数据库压力很大
通过Redis来缓存菜品数据,减少数据库查询操作
开始-->后端服务-->缓存是否存在-->存在,直接使用,不存在查询数据库
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//查询redis中是否存在菜品数据,规则:dish_分类id
String key = "dish_" + categoryId;
//如果存在,直接返回,无需查询数据库
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
//如果不存在,查询数据库,将查询到的数据放入redis
if (list != null) {
return Result.success(list);
}else{
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
List<DishVO> list2 = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key, list2);
return Result.success(list2);
}
}
存在问题,增加,修改,删除菜品以后需要删除缓存
//封装清理缓存数据
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
缓存套餐
- Spring Cache
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单的加一个注解,就能实现缓存功能
Spring Cache底层提供了一层抽象,底层可以切换不同的缓存实现
- EHCache
- Caffeine
- Redis
常用注解
九.购物车管理
添加购物车
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result addShoppingCart(@RequestBody ShoppingCartDTO shoppingCartDTO) {
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
//判断当前商品是否已经在购物车
List<ShoppingCart> list= shoppingMapper.list(shoppingCart);
//如果存在,将数量加1
if (list.size()>0){
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber()+1);
shoppingMapper.updateNumber(cart);
}
//如果不存在,将商品加入购物车
if (list.size()==0){
//判断是否是菜品还是套餐
Long dishId = shoppingCartDTO.getDishId();
Long setmealId = shoppingCartDTO.getSetmealId();
if(dishId != null){
Dish dish = dishMapper.geibyid(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
}else{
Setmeal setmeal = setmealMapper.getsetmealbyid(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
}
shoppingMapper.addShoppingCart(shoppingCart);
}
}
List<ShoppingCart> list(ShoppingCart shoppingCart);
<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>
@Insert("insert into shopping_cart(name,user_id,dish_id,setmeal_id,dish_flavor,number,amount,image,create_time) " +
"values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
void addShoppingCart(ShoppingCart shoppingCart);
查看购物车
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> listShoppingCart() {
Long userId = BaseContext.getCurrentId();
List<ShoppingCart> shoppingCart = shoppingCartService.getByUserId(userId);
return Result.success(shoppingCart);
}
List<ShoppingCart> getByUserId(Long userId);
public List<ShoppingCart> getByUserId(Long userId) {
List<ShoppingCart> list= shoppingMapper.getByUserId(userId);
return list;
}
@Select("select * from shopping_cart where user_id = #{userId}")
List<ShoppingCart> getByUserId(Long userId);
清空购物车
@DeleteMapping("/clean")
@ApiOperation("清空购物车")
public Result cleanShoppingCart() {
Long userId = BaseContext.getCurrentId();
shoppingCartService.deleteAll(userId);
return Result.success();
}
void deleteAll(Long userId);
public void deleteAll(Long userId) {
shoppingMapper.deleteAll(userId);
}
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteAll(Long userId);
删除购物车的某一个商品
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//设置查询条件,查询当前登录用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list = shoppingMapper.list(shoppingCart);
if(list != null && list.size() > 0){
shoppingCart = list.get(0);
Integer number = shoppingCart.getNumber();
if(number == 1){
//当前商品在购物车中的份数为1,直接删除当前记录
shoppingMapper.deleteById(shoppingCart.getId());
}else {
//当前商品在购物车中的份数不为1,修改份数即可
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingMapper.updateNumber(shoppingCart);
}
}
}
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);
十.用户下单
@PostMapping("/submit")
@ApiOperation("用户提交订单")
public Result<OrderSubmitVO> addOrder(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
log.info("提交订单{}",ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submit(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}
OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO);
@Override
public OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO) {
//1.处理业务异常
AddressBook addressBook =addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if(addressBook==null){
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List<ShoppingCart> shoppingCarts = shoppingMapper.getByUserId(userId);
if(shoppingCarts==null){
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//2.向订单表插入1条数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO, orders);
orders.setUserId(userId);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orderMapper.insert(orders);
//3.向订单明细表插入n条数据
List<OrderDetail> orderDetails = new ArrayList<OrderDetail>();
for (ShoppingCart cart : shoppingCarts) {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(cart, orderDetail);
orderDetail.setOrderId(orders.getId());
orderDetails.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetails);
//4.清空当前用户的购物出数据
shoppingMapper.deleteById(userId);
//5.封装VO返回结果
OrderSubmitVO orderSubmitVO =OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.build();
return orderSubmitVO;
}
十一.微信支付
微信支付时序图
cpolar内网穿透
设置authtoken,只需设置一次
cpolar.exe authtoken N2QwNzRlMjctNjFkNC00MmVmLWE5ZTYtY2M4NTcwNDIzZmIw
运行cpolar
cpolar.exe http 8080
微信支付相关配置
application.yml
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}
mchid: ${sky.wechat.mchid}
mch-serial-no: ${sky.wechat.mch-serial-no}
private-key-file-path: ${sky.wechat.private-key-file-path}
api-v3-key: ${sky.wechat.api-v3-key}
we-chat-pay-cert-file-path: ${sky.wechat.we-chat-pay-cert-file-path}
notify-url: ${sky.wechat.notify-url}
refund-notify-url: ${sky.wechat.refund-notify-url}
application-dev.yml
wechat:
appid: wx831281689c9e6ae9
secret: ae3115ef66575e05d2d075e287cb6270
mchid: 1561414331
mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
privateKeyFilePath: D:\pay\apiclient_key.pem
apiV3Key: CZBK51236435wxpay435434323FFDuv3
wechatPayCertFilePath: D:\pay\wechatpay_166096F876F45C7D07CE98952A96EC980368ACFC.pem
notifyUrl: https://6f67b0c.r5.cpolar.top//notify/paySuccess
refundNotifyUrl: https://6619cf50.r6.cpolar.top/notify/refundSuccess