Day1 框架搭建2.25
一开始了解这个项目都说是烂大街的项目,但是当开始学习的这一刻发现对自己来说还是有很多没有学过的知识。前两天没有打算写学习总结,但是感觉自己学完肯定会忘记,所以打算记录下来。
业务总体功能模块
前端环境搭建:
前端页面是基于nginx运行,只能安装在英文路径下,双击运行。
Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx详解(一文带你搞懂Nginx)-CSDN博客

项目的整体结构
sky-common 子模块中存放的是一些公共类
sky-pojo 子模块中存放的是一些 entity、DTO、VO,
DTO | 数据传输对象,通常用于程序中各层之间传递数据 |
VO | 视图对象,为前端展示数据提供的对象 |
sky-server 子模块中存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等
第一次使用的gitee创建了自己的本地仓库,创建选择项目的根目录层
原来前端发送来的请求先到nginx再转发到后端服务器
nginx 反向代理的好处:


完善登录功能
数据库中用的是明文加密,采用MD5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
导入接口文档
Apifox可以看到使用的什么方法,看管理端和用户端接口
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档 Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,可以接口测试,不用去前端页面做测试了
Day2 新建员工2.26
@DATA是 Lombok 的注解,它会自动为EmployeeDTO
类生成getter
、setter
、toString
、equals
和hashCode
等方法。
public static void copyProperties(Object source, Object target) throws BeansException { copyProperties(source, target, (Class)null, (String[])null); }
BeanUtils.copyProperties(employeeDTO, employee);前面DTO封装了员工用到的属性,被拷贝到employee中。
再去
第一次接口测试新建员工数据
显示401报错,通过断点调试,看到是jwt令牌校验错误,去员工登陆测试会给一个token,再添加到全局参数里面,把过期时间调长一点就不用隔断时间去重新添加新令牌
sky: jwt: # 设置jwt签名加密时使用的秘钥 admin-secret-key: itcast # 设置jwt过期时间 admin-ttl: 720000000000 # 设置前端传递过来的令牌名称 admin-token-name: token
老师讲到会存在两个问题,一个名字已存在,没有抛出异常,在全局异常处理处理器中添加
Duplicate entry
是在数据库操作中常见的错误提示信息,通常在执行插入(INSERT
)或更新(UPDATE
)操作时出现
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
在MessageConstant可以添加信息错误常量public static final String ALREADY_EXISTS = "已存在";
还有一个就是CreateUser,UpdateUser设置的id值还没有确定
通过令牌来获取前端传来的id值
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("当前线程的id"+Thread.currentThread().getId());
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
//jwtProperties.getAdminSecretKey() 用于获取管理员 JWT 的密钥,token 是要解析的 JWT 字符串。
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);//parseJWT 是 JwtUtil 类中的静态方法,用于解析 JWT
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;
}
}
}
解析出员工id,要通过线程隔离处理,这样这个线程不会被线程外的打扰。
ThreadLocal
为每个线程提供单独一份存储空间, BaseContext.setCurrentId(empId);
-
public void set(T value) 设置当前线程的线程局部变量的值
-
public T get() 返回当前线程所对应的线程局部变量的值
-
public void remove() 移除当前线程的线程局部变量
员工分页查询
用到了一个插件 MyBatis - PageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10
//开始分页查询
//获取当前页码,获取每页显示的记录数
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);
}
接口测试后前端时间数据不是标准样式,方法一在每个属性上加上
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
方法二:WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
MappingJackson2HttpMessageConverter
是 Spring MVC 中用于处理 JSON 数据的消息转换器,它可以将 Java 对象序列化为 JSON 格式的数据
/**
* 方法二:扩展springMVC消息转换器,日期类型的格式化
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//拆功能键一个消息转化器的对象
MappingJackson2HttpMessageConverter converter =new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为jason数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到自己转换器中
converters.add(0,converter);
//表示将自定义的消息转换器插入到列表的第一个位置,这样在进行消息转换时,会优先使用自定义的消息转换器
}
时间格式定义:
package com.sky.json;
public class JacksonObjectMapper extends ObjectMapper {
//.......
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
//.......
}
}
启用禁用员工账号
传status和id,通过id设置status
学到了一个建造者模式,简化创造对象
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
编辑mapper的sql语句时,学到了一个插件MyBatisX,好用!
编辑员工
1.根据id查询员工信息
employee.setPassword("****");加上密码不会被前端看到
2.编辑员工信息
Day3导入分类模块功能代码2.27
-
新增分类
-
分类分页查询
-
根据id删除分类
-
修改分类
-
启用禁用分类
-
根据类型查询分类
我直接将写好的代码导入了》。。。
按照mapper-->service-->controller依次导入,这样代码不会显示相应的报错
Day4 休息3.1
Day5 3.2公共字段自动填充
1.自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
/**
* 数据库操作类型枚举类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
/**
* 自定义注解,用于标识某个方法进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)//指定注解加入到方法上
@Retention(RetentionPolicy.RUNTIME)//固定写法
public @interface AutoFill {
//数据库操作类型:UPDATE,INSERT
OperationType value();//枚举类型
}
2.自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")//拦截mapper类的方法并且是在定义的注解的那些方法
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值,在mapper方法执行之前赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {//连接点,拦截到的方法的参数和参数类型
log.info("开始进行公共字段自动填充..");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill=signature.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){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
Method setCreateUser = 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);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == operationType.UPDATE){
//为2个公共字段赋值
try {
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);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.在 Mapper 的方法上加入 AutoFill 注解
例如:@AutoFill(value = OperationType.UPDATE) void update(Employee employee);
4.新增菜品
Day6 3.3 文件上传
老师用的是阿里云,我的账号有问题。
所以我用的用了七牛云存储,其中以为和阿里云的格式差不多,最后一直卡在图片url的拼写,最终靠住AI完成,感谢AI,呜呜呜.图片上传限制在200*200内。
util类:
package com.sky.utils;
import com.google.gson.Gson;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "sky.qiniu")
public class QiniuOssUtil {
private String accessKey;
private String secretKey;
private String bucketName;
private String bindDomain; // 新增字段,用于存储绑定域名
private UploadManager uploadManager;
private Auth auth;
@PostConstruct
public void init() {
log.info("七牛云配置初始化...");
log.info("Endpoint: {}", endpoint);
log.info("Bucket: {}", bucketName);
log.info("Bind Domain: {}", bindDomain); // 记录绑定域名
Configuration cfg = new Configuration(Region.autoRegion());
this.uploadManager = new UploadManager(cfg);
this.auth = Auth.create(accessKey.trim(), secretKey.trim());
}
public String upload(byte[] bytes, String objectName) {
try {
String upToken = auth.uploadToken(bucketName);
Response response = uploadManager.put(bytes, objectName, upToken);
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
// 使用绑定域名构建访问URL
String imageUrl = String.format("http://%s/%s", bindDomain, putRet.key);
log.info("图片上传成功,访问URL为: {}", imageUrl);
return imageUrl;
} catch (QiniuException ex) {
log.error("七牛云上传失败 => 错误码:{}", ex.code());
log.error("错误详情:{}", ex.response.toString());
throw new RuntimeException("文件上传失败: " + ex.getMessage());
} catch (Exception ex) {
log.error("上传出现未知异常", ex);
throw new RuntimeException("文件上传服务异常");
}
}
}
commomController:通过设置uuid上传同文件不同名
package com.sky.controller.admin;
import com.sky.result.Result;
import com.sky.utils.QiniuOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private QiniuOssUtil qiniuOssUtil; // 修改为注入七牛云工具类
@ApiOperation("文件上传")
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}", file);
try {
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = UUID.randomUUID() + extension;
// 修改为调用七牛云上传
String filePath = qiniuOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败{}", e);
return Result.error("文件上传失败");
}
}
}
qiniu:
access-key: ak
secret-key: sk
bucket-name: bucket名字
bindDomain: 外链域名
Day7 3.4 新增菜品功能
@Autowired//可以找到对应的bean,例如 可以引用dishMapper.insert(dish); private DishMapper dishMapper;
添加数据最好加上事务回滚@Transactional
菜品和口味是一对多的关系,先插入一条数据和多种口味,所以用
BeanUtils.copyProperties(dishDTO,dish);把DTO的数据拷贝到dish中
遍历flavors时没有产生dishId,所以要从dishmapper中回显id值,可以用到
<insert id="insert" useGeneratedKeys="true" keyProperty="id">//主键回显
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired//可以找到对应的bean,例如 dishMapper.insert(dish);
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和口味
* @param dishDTO
* @return
*/
@Override
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插入一条数据
dishMapper.insert(dish);
//获取insert语句获取到的主键值
Long dishId = dish.getId();
//遍历
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors !=null && flavors.size()>0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}
}
其中,在写口味表的mapper层中,flavors已经是形参了,但是在collection中写会出现找不到flavors,在前面加上@Param("flavors"),或者将mapper中改为collection=“list”
void insertBatch(@Param("flavors") List<DishFlavor> flavors);
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
菜品sql语句
select d.*,c.name as categoryName from dish d left outer join category c on d.category_id = c.id
删除菜品功能
/**
* 菜品批量删除
* @param ids
*/
@Override
@Transactional
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能被删除--是否起售中??
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == StatusConstant.ENABLE){
//当前菜品起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断当前菜品是否被套餐关联??
List<Long> setmealIds = setmealDishMapper.getSetmealMapperByDishIds(ids);
if(setmealIds !=null && setmealIds.size() >0){
//当前被套餐关联不能删
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除关联菜品口味数据
dishFlavorMapper.deleteByDishId(id);
}
}
删除的数据多,sql会一步步执行操作太多,代码优化:
/** * 根据菜品id集合批量删除菜品 * @param ids */ void deleteByIds(@Param("ids") List<Long> ids);
<delete id="deleteByIds"> delete from dish where id in <foreach collection="ids" open="(" close=")" separator="," item="id"> #{id} </foreach> </delete>
修改菜品功能:
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//修改菜品表基本信息
dishMapper.update(dish);
//删除原有的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors !=null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
Day8 新增套餐3.5
在新增套餐中添加菜品没有显示菜品,后面发现是因为@GetMapping("/list")没有写
/**
* 根据分类ID查找菜品(套餐)
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}
Day9 第一天学习redis3.6
在redis.windows.conf中查找pass后将#删掉后面改为自己设置的密码,保存退出后再重新运行,不然密码会没有被录入进去
redis-server.exe redis.windows.conf
再新增一个cmd命令执行客户端,运行可视化工具时不要退出命令
redis-cli.exe -h localhost -p 6379 -a 123456
成功显示:
Redis五种常用数据类型
字符串String 哈希hash(类似于Java中的hashmap,比如名字性别..)
列表list(按照插入顺序,可以有重复元素,类似于java中的LinkedList,比如点赞等)
集合set(无序集合,没有重复元素,类似java中hashset,比如朋友有各自朋友圈,共同朋友可以计算交集)
有序集合sorted set/zset(每一个元素关联一个score,根据分数升序排序,没有重复元素,比如排行榜)
Redsi常用命令
Redis 字符串类型常用命令:
SET key value 设置指定key的值
GET key 获取指定key的值
SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒(短信验证码)
SETNX key value 只有在 key 不存在时设置 key 的值(分布式锁)
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
HSET key field value 将哈希表 key 中的字段 field 的值设为 value
HGET key field 获取存储在哈希表中指定字段的值
HDEL key field 删除存储在哈希表中的指定字段
HKEYS key 获取哈希表中所有字段
HVALS key 获取哈希表中所有值
Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:
LPUSH key value1 [value2] 将一个或多个值插入到列表头部(左边)
LRANGE key start stop 获取列表指定范围内的元素
RPOP key 移除并获取列表最后一个元素(右边)
LLEN key 获取列表长度
Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据,常用命令:
SADD key member1 [member2] 向集合添加一个或多个成员
SMEMBERS key 返回集合中的所有成员
SCARD key 获取集合的成员数
SINTER key1 [key2] 返回给定所有集合的交集
SUNION key1 [key2] 返回所有给定集合的并集
SREM key member1 [member2] 删除集合中一个或多个成员
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:
ZADD key score1 member1 [score2 member2] 向有序集合添加多个成员,根据分数顺序
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
ZREM key member [member ...] 移除有序集合中的一个或多个成员
Redis的通用命令是不分数据类型的,都可以使用的命令:
KEYS pattern 查找所有符合给定模式( pattern)的 key
EXISTS key 检查给定 key 是否存在
TYPE key 返回 key 所储存的值的类型
DEL key 该命令用于在 key 存在是删除 key
Spring Data Redis使用方式:
操作步骤:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
②配置Redis数据源
dev:
redis:
host: localhost
password: 123456
port: 6379
database:
redis:
host: ${sky.redis.host}
password: ${sky.redis.password}
port: ${sky.redis.port}
database: ${sky.redis.database} #当前数据库,不是必须,默认0
③编写配置类,创建RedisTemplate对象
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redis模板对象》》》》");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key 的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
//这里可以设置作为值的序列化器,用来将对象序列化为 JSON 格式存储到 Redis 中,以及从 Redis 中读取 JSON 数据后反序列化为 Java 对象
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
在Spring Boot 2.2.x之前使用import org.junit.Test —— Junit4
ShopController在用户端和管理端都有,可以用
@RequestMapping("/user/shop")
@RestController("userShopController")
@RequestMapping("/admin/shop")
@RestController("adminShopController")
区分
PUT修改,POST添加
@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate redisTemplate;
/**
* 设置店铺的营业状态
* @param status
* @return
*/
@PutMapping("/{status}")
@ApiOperation("设置店铺的营业状态")
public Result setStatus(@PathVariable Integer status){
log.info("设置店铺的营业状态为:{}",status ==1 ? "营业中" : "打烊中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
@GetMapping("/{status}")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("设获取店铺的营业状态为:{}",status ==1 ? "营业中" : "打烊中");
return Result.success(status);
}
}
调试时出现500:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
发现是设置redis values的序列化器没有注释掉
接口测试get状态一直时404 No mapping for GET /admin/shop/,前后端联调就没有问题,目前没找到解决方法。
------解决:getmapping给status加了{},去掉成功
Day10 3.7HttpClient
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
HttpClient作用:
-
发送HTTP请求
-
接收响应数据
HttpClient的maven坐标:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
HttpClient的核心API:
-
HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
-
HttpClients:可认为是构建器,可创建HttpClient对象。
-
CloseableHttpClient:实现类,实现了HttpClient接口。
-
HttpGet:Get方式请求类型。
-
HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
-
创建HttpClient对象
-
创建Http请求对象
-
调用HttpClient的execute方法发送请求
微信小程序开发
先注册一个微信小程序选择个人注册,进入开发工具勾选设置不校验合法域名
注:开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败。
小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录
文件说明:
app.js:必须存在,主要存放小程序的逻辑代码
app.json:必须存在,小程序配置文件,主要存放小程序的公共配置
app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式
对小程序主体三个文件了解后,其实一个小程序又有多个页面。比如说,有商品浏览页面、购物车的页面、订单支付的页面、商品的详情页面等等,会存放在pages目录。
编写小程序代码测试时,获取不到用户信息,将基础调试库版本设置到2.27.0以下
Day11 3.8 休息
Day12 3.9 微信小程序导入
将资料中的代码导入开发者工具
微信登录流程
-
小程序端,调用wx.login()获取code,就是授权码。
-
小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
-
开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
-
开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
-
开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
-
小程序端,收到自定义登录态,存储storage。
-
小程序端,后绪通过wx.request()发起业务请求时,携带token。
-
开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
-
开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
postman一直sending request。。。。所以就用apifox,可以获取到openid
在idea里面一直登陆失败,获取不到openid
Day13 3.10 找bug @requestbody注解选错了
今天小程序重启项目成功了.
但是发现新建套餐前后端联调竟然出问题了更改套餐时,没有传值进去就显示更新成功...,找了半天发现是@requestbody注解选错了。
Spring 的 @RequestBody:
位于 org.springframework.web.bind.annotation 包中,用于将 HTTP 请求体绑定到方法参数。
Swagger 的 @RequestBody:
位于 io.swagger.v3.oas.annotations.parameters 包中,用于生成 API 文档。Swagger学习⑨——@RequestBody注解_swagger requestbody-CSDN博客
Day14 3.11跟进学习--缓存菜品
用户端菜品展示都是通过查询数据库获得,访问量过大时,数据库有压力。
缓存时redis配置values的序列化器就会报Type id handling not implemented for type java.lang.Object (by serializer of type com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer)
时间不能被GenericJackson2JsonRedisSerializer转化,所以注释掉就成功了
缓存套餐
SPring Cache框架
实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
-
EHCache
-
Caffeine
-
Redis(常用)
-
依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version> </dependency>
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 说明 @EnableCaching 开启缓存注解功能,通常加在启动类上 @Cacheable 加在方法上面,在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中,可放可取 @CachePut 将方法的返回值放到缓存中,只放不取 @CacheEvict 将一条或多条数据从缓存中删除 @CacheEvict(cacheNames = "setmealCache",allEntries = true)
@CachePut 说明
作用: 将方法返回值,放入缓存
value: 缓存的名称, 每个缓存名称下面可以有很多key
key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法
#开头 #result . 当前方法的返回值
#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;
#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
添加购物车
前后端联调发现再一次忘记加上@RequestBody,并且发现userid的值没有传过来,看到弹幕一条消息检查webmvcConfiguration中user前面少了一个/
/**
*添加购物车
* @param shoppingCartDTO
* @return
*/
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
/**
* 添加购物车
* @param shoppingCartDTO
*/
@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.size()>0){
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber()+1);//update shopping_cart set number =? where id =?
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);
}
}
动态查询购物车
<select id="list" resultType="com.sky.entity.ShoppingCart">
select * from shopping_cart
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="dishId != null">
and dish_id = #{dishId}
</if>
<if test="setmealId != null">
and setmeal_id = #{setmealId}
</if>
<if test="dishFlavor != null">
and dish_flavor = #{dishFlavor}
</if>
</where>
</select>
查看购物车
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list(){
List<ShoppingCart> list=shoppingCartService.showShoppingCart();
return Result.success(list);
}
/**
* 查看购物车
* @return
*/
@Override
public List<ShoppingCart> showShoppingCart() {
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart= ShoppingCart.builder()
.userId(userId)
.build();
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
return list;
}
删除购物车
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//设置查询条件,查询当前登录用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if(list != null && list.size() > 0){
shoppingCart = list.get(0);
Integer number = shoppingCart.getNumber();
if(number == 1){
//当前商品在购物车中的份数为1,直接删除当前记录
shoppingCartMapper.deleteById(shoppingCart.getId());
}else {
//当前商品在购物车中的份数不为1,修改份数即可
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}
/**
* 根据id删除购物车数据
* @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);
Day15 3.12 导入地址簿功能
导入完成
用户下单
@Autowired
private OrderService orderService;
/**
* 用户下单
* @param ordersSubmitDTO
* @return
*/
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO){
log.info("用户下单参数为:{}",ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}
/**
* 用户下单
* @param ordersSubmitDTO
* @return
*/
@Override
@Transactional
public OrderSubmitVO submitOrder(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> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if(shoppingCartList ==null || shoppingCartList.size() ==0){
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//2.像订单表插入一条数据
Orders orders =new Orders();//为什么不能用builder(),因为要先建造后再拷贝。
BeanUtils.copyProperties(ordersSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));//使用系统时间戳作为订单号
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userId);
orderMapper.insert(orders);
List<OrderDetail> orderDetailList = new ArrayList<>();
//3.向订单明细表插入n条数据
for (ShoppingCart cart : shoppingCartList) {
OrderDetail orderDetail=new OrderDetail();//订单明细
BeanUtils.copyProperties(cart,orderDetail);
orderDetail.setOrderId(orders.getId());//设置当前订单明细关联的订单id
orderDetailList.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetailList);
//4.如果下单成功,清空购物车数据
shoppingCartMapper.deleteByUserId(userId);
//5.封装VO返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderAmount(orders.getAmount())
.orderNumber(orders.getNumber())
.build();
return orderSubmitVO;
}
<insert id="insertBatch" parameterType="list">
insert into order_detail
(name, order_id, dish_id, setmeal_id, dish_flavor, number, amount, image)
values
<foreach collection="orderDetails" item="od" separator=",">
(#{od.name},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},
#{od.number},#{od.amount},#{od.image})
</foreach>
</insert>
前后端联调测试时,一直显示insertBatch语句有问题,发现是前面没有把orderDetail传入到list当中,传空报错了!
Day18 3.15 微信支付
下载cpolar,执行命令:
cpolar.exe authtoken 隧道 执行一次
cpolar.exe http 8080 //获取域名
域名映射到本地
验证临时域名生效
跳过微信支付:苍穹外卖跳过微信支付(全网最强,最详细,最容易理解)_跳过网页微信付费-CSDN博客
用户端
查询历史订单:
业务规则
-
分页查询历史订单
-
可以根据订单状态查询
-
展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)
/** * 历史订单查询 * * @param page * @param pageSize * @param status 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 * @return */ @GetMapping("/historyOrders") @ApiOperation("历史订单记录") public Result<PageResult> page(int page, int pageSize, Integer status) { PageResult pageResult = orderService.pageQuery4User(page, pageSize, status); return Result.success(pageResult); }
/** * 用户端订单分页查询 * * @param pageNum * @param pageSize * @param status * @return */ @Override public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) { // 设置分页 PageHelper.startPage(pageNum, pageSize); OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO(); ordersPageQueryDTO.setUserId(BaseContext.getCurrentId()); ordersPageQueryDTO.setStatus(status); // 分页条件查询 Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); List<OrderVO> list = new ArrayList(); // 查询出订单明细,并封装入OrderVO进行响应 if (page != null && page.getTotal() > 0) { for (Orders orders : page) { Long orderId = orders.getId();// 订单id // 查询订单明细 List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId); OrderVO orderVO = new OrderVO(); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetails); list.add(orderVO); } } return new PageResult(page.getTotal(), list); }
OrderMapper.xml: <select id="pageQuery" resultType="Orders"> select * from orders <where> <if test="number != null and number!=''"> and number like concat('%',#{number},'%') </if> <if test="phone != null and phone!=''"> and phone like concat('%',#{phone},'%') </if> <if test="userId != null"> and user_id = #{userId} </if> <if test="status != null"> and status = #{status} </if> <if test="beginTime != null"> and order_time >= #{beginTime} </if> <if test="endTime != null"> and order_time <= #{endTime} </if> </where> order by order_time desc </select> OrderDetailMapper: /** * 根据订单id查询订单明细 * @param orderId * @return */ @Select("select * from order_detail where order_id = #{orderId}") List<OrderDetail> getByOrderId(Long orderId);
查询订单详情:
/**
* 查询订单详情
*
* @param id
* @return
*/
@GetMapping("/orderDetail/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> details(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.details(id);
return Result.success(orderVO);
}
/**
* 查询订单详情
*
* @param id
* @return
*/
public OrderVO details(Long id) {
// 根据id查询订单
Orders orders = orderMapper.getById(id);
// 查询该订单对应的菜品/套餐明细
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
// 将该订单及其详情封装到OrderVO并返回
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetailList);
return orderVO;
}
/**
* 根据id查询订单
* @param id
*/
@Select("select * from orders where id=#{id}")
Orders getById(Long id);
取消订单:微信支付功能没有实现
微信支付功能没有实现,所以不要使用 //调用微信支付退款接口
weChatPayUtil.refund,注释掉,把订单取消挪到if中
/**
* 用户取消订单
*
* @return
*/
@PutMapping("/cancel/{id}")
@ApiOperation("取消订单")
public Result cancel(@PathVariable("id") Long id) throws Exception {
orderService.userCancelById(id);
return Result.success();
}
/**
* 用户取消订单
*
* @param id
*/
public void userCancelById(Long id) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
if (ordersDB.getStatus() > 2) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 订单处于待接单状态下取消,需要进行退款
if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
/* //调用微信支付退款接口
weChatPayUtil.refund(
ordersDB.getNumber(), //商户订单号
ordersDB.getNumber(), //商户退款单号
new BigDecimal(0.01),//退款金额,单位 元
new BigDecimal(0.01));//原订单金额
//支付状态修改为 退款
orders.setPayStatus(Orders.REFUND); */
// 更新订单状态、取消原因、取消时间
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("用户取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
再来一单:
/**
* 再来一单
*
* @param id
* @return
*/
@PostMapping("/repetition/{id}")
@ApiOperation("再来一单")
public Result repetition(@PathVariable Long id) {
orderService.repetition(id);
return Result.success();
}
/**
* 再来一单
*
* @param id
*/
public void repetition(Long id) {
// 查询当前用户id
Long userId = BaseContext.getCurrentId();
// 根据订单id查询当前订单详情
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
// 将订单详情对象转换为购物车对象
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
ShoppingCart shoppingCart = new ShoppingCart();
// 将原订单详情里面的菜品信息重新复制到购物车对象中
BeanUtils.copyProperties(x, shoppingCart, "id");
shoppingCart.setUserId(userId);
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
// 将购物车对象批量添加到数据库
shoppingCartMapper.insertBatch(shoppingCartList);
}
<insert id="insertBatch" parameterType="list">
insert into shopping_cart
(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
values
<foreach collection="shoppingCartList" item="sc" separator=",">
(#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
</foreach>
</insert>
商家端
订单搜索:
业务规则:
- 输入订单号/手机号进行搜索,支持模糊搜索
- 根据订单状态进行筛选
- 下单时间进行时间筛选
- 搜索内容为空,提示未找到相关订单
- 搜索结果页,展示包含搜索关键词的内容
- 分页展示搜索到的订单数据
admin包下
/**
* 订单管理
*/
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Slf4j
@Api(tags = "订单管理接口")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 订单搜索
*
* @param ordersPageQueryDTO
* @return
*/
@GetMapping("/conditionSearch")
@ApiOperation("订单搜索")
public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
return Result.success(pageResult);
}
}
/**
* 订单搜索
*
* @param ordersPageQueryDTO
* @return
*/
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);
// 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
List<OrderVO> orderVOList = getOrderVOList(page);
return new PageResult(page.getTotal(), orderVOList);
}
private List<OrderVO> getOrderVOList(Page<Orders> page) {
// 需要返回订单菜品信息,自定义OrderVO响应结果
List<OrderVO> orderVOList = new ArrayList<>();
List<Orders> ordersList = page.getResult();
if (!CollectionUtils.isEmpty(ordersList)) {
for (Orders orders : ordersList) {
// 将共同字段复制到OrderVO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
String orderDishes = getOrderDishesStr(orders);
// 将订单菜品信息封装到orderVO中,并添加到orderVOList
orderVO.setOrderDishes(orderDishes);
orderVOList.add(orderVO);
}
}
return orderVOList;
}
/**
* 根据订单id获取菜品信息字符串
*
* @param orders
* @return
*/
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());
// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}
各个状态的订单数量统计:
/**
* 各个状态的订单数量统计
*
* @return
*/
@GetMapping("/statistics")
@ApiOperation("各个状态的订单数量统计")
public Result<OrderStatisticsVO> statistics() {
OrderStatisticsVO orderStatisticsVO = orderService.statistics();
return Result.success(orderStatisticsVO);
}
/**
* 各个状态的订单数量统计
*
* @return
*/
public OrderStatisticsVO statistics() {
// 根据状态,分别查询出待接单、待派送、派送中的订单数量
Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);
// 将查询出的数据封装到orderStatisticsVO中响应
OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
orderStatisticsVO.setConfirmed(confirmed);
orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
return orderStatisticsVO;
}
/**
* 根据状态统计订单数量
* @param status
*/
@Select("select count(id) from orders where status = #{status}")
Integer countStatus(Integer status);
查询订单详情:
业务规则:
-
订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
-
订单详情页面需要展示订单明细数据(商品名称、数量、单价)
/** * 订单详情 * * @param id * @return */ @GetMapping("/details/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details(@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
接单:
/**
* 接单
*
* @return
*/
@PutMapping("/confirm")
@ApiOperation("接单")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
orderService.confirm(ordersConfirmDTO);
return Result.success();
}
/**
* 接单
*
* @param ordersConfirmDTO
*/
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
Orders orders = Orders.builder()
.id(ordersConfirmDTO.getId())
.status(Orders.CONFIRMED)
.build();
orderMapper.update(orders);
}
拒单: 跳过wx支付
业务规则:
-
商家拒单其实就是将订单状态修改为“已取消”
-
只有订单处于“待接单”状态时可以执行拒单操作
-
商家拒单时需要指定拒单原因
-
商家拒单时,如果用户已经完成了支付,需要为用户退款
/**
* 拒单
*
* @return
*/
@PutMapping("/rejection")
@ApiOperation("拒单")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
orderService.rejection(ordersRejectionDTO);
return Result.success();
}
/**
* 拒单
*
* @param ordersRejectionDTO
*/
public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());
// 订单只有存在且状态为2(待接单)才可以拒单
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == Orders.PAID) {
//跳过 weChatPayUtil.refund调用,模拟给用户退款,直接更新数据库订单支付状态为 ”已退款 “
//用户已支付,需要退款
// String refund = weChatPayUtil.refund(
// ordersDB.getNumber(),
// ordersDB.getNumber(),
// new BigDecimal(0.01),
// new BigDecimal(0.01));
// log.info("申请退款:{}", refund);
log.info("给订单{}退款", ordersDB.getNumber());
}
// 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
Orders orders = new Orders();
orders.setPayStatus(Orders.REFUND);//修改订单支付状态为 ”已退款 “
orders.setId(ordersDB.getId());
orders.setStatus(Orders.CANCELLED);
orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
取消订单: 跳过wx支付
业务规则:
-
取消订单其实就是将订单状态修改为“已取消”
-
商家取消订单时需要指定取消原因
-
商家取消订单时,如果用户已经完成了支付,需要为用户退款
-
/** * 取消订单 * * @param ordersCancelDTO */ public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception { // 根据id查询订单 Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId()); //支付状态 Integer payStatus = ordersDB.getPayStatus(); if (payStatus == 1) { //跳过 weChatPayUtil.refund调用,模拟给用户退款,直接更新数据库订单支付状态为 ”已退款 “ //用户已支付,需要退款 // String refund = weChatPayUtil.refund( // ordersDB.getNumber(), // ordersDB.getNumber(), // new BigDecimal(0.01), // new BigDecimal(0.01)); // log.info("申请退款:{}", refund); log.info("给订单{}退款", ordersDB.getNumber()); } // 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间 Orders orders = new Orders(); orders.setPayStatus(Orders.REFUND);//修改订单支付状态为 ”已退款 “ orders.setId(ordersCancelDTO.getId()); orders.setStatus(Orders.CANCELLED); orders.setCancelReason(ordersCancelDTO.getCancelReason()); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
派送订单:
-
/** * 派送订单 * * @return */ @PutMapping("/delivery/{id}") @ApiOperation("派送订单") public Result delivery(@PathVariable("id") Long id) { orderService.delivery(id); return Result.success(); } /** * 派送订单 * * @param id */ public void delivery(Long id) { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在,并且状态为3 if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders(); orders.setId(ordersDB.getId()); // 更新订单状态,状态转为派送中 orders.setStatus(Orders.DELIVERY_IN_PROGRESS); orderMapper.update(orders); }
完成订单:
-
/** * 完成订单 * * @return */ @PutMapping("/complete/{id}") @ApiOperation("完成订单") public Result complete(@PathVariable("id") Long id) { orderService.complete(id); return Result.success(); } /** * 完成订单 * * @param id */ public void complete(Long id) { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在,并且状态为4 if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders(); orders.setId(ordersDB.getId()); // 更新订单状态,状态转为完成 orders.setStatus(Orders.COMPLETED); orders.setDeliveryTime(LocalDateTime.now()); orderMapper.update(orders); }
Day 19 3.16 spring task定时任务工具
Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架
作用:定时自动执行某段Java代码
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
cron表达式在线生成器:在线Cron表达式生成器
Spring Task使用步骤
1). 导入maven坐标 spring-context
2). 启动类添加注解 @EnableScheduling 开启任务调度
3). 自定义定时任务类
/**
* 自定义定时任务,实现订单状态定时处理
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理支付超时订单
*/
@Scheduled(cron = "0 * * * * ?")//每分钟触发一次
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
//当前时间加上-15
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
if(ordersList != null && ordersList.size() >0){
for (Orders orders : ordersList) {
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
/**
* 处理“派送中”状态的订单
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("处理派送中订单:{}", LocalDateTime.now());
// select * from orders where status = 4 and order_time < 当前时间-1小时
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
});
}
}
}
/**
* 根据状态和下单时间查询订单
* @param status
* @param localDateTime
* @return
*/
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status,LocalDateTime localDateTime);
WebSocket
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
-
HTTP是短连接
-
WebSocket是长连接
-
HTTP通信是单向的,基于请求响应模式
-
WebSocket支持双向通信
-
HTTP和WebSocket底层都是TCP连接
WebSocket缺点:
服务器长期维护长连接需要一定的成本 各个浏览器支持程度不一 WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
发现前面写的课程day9的有问题,具体参考了这篇解决了苍穹外卖学习-day08-day10(跳过微信支付,订单取消,来电提醒问题解决)_orderspaymentdto-CSDN博客
支付成功语音播报
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单号查询订单
// Orders ordersDB = orderMapper.getByNumber(outTradeNo);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
//通过WebSocket实现来单提醒 type orderId content
Map map = new HashMap<>();
map.put("type", 1);//消息类型,1表示来单提醒 2表示客户催单
map.put("orderID", ordersDB.getId());//订单id
map.put("content", "订单号:" + outTradeNo);
//通过websocket向客户端浏览器推送消息
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
客户催单
/**
* 用户催单
*
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("用户催单")
public Result reminder(@PathVariable("id") Long id) {
orderService.reminder(id);
return Result.success();
}
/**
* 用户催单
*
* @param id
*/
public void reminder(Long id) {
// 查询订单是否存在
Orders orders = orderMapper.getById(id);
if (orders == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
//基于WebSocket实现催单
Map map = new HashMap();
map.put("type", 2);//2代表用户催单
map.put("orderId", id);
map.put("content", "订单号:" + orders.getNumber());
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
Day20 营业额统计3.17
Apache ECharts
Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。官网地址:https://echarts.apache.org/zh/index.html
业务规则:
-
营业额指订单状态为已完成的订单金额合计
-
基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额
-
根据时间选择区间,展示每天的营业额数据
@Autowired
private ReportService reportService;
/**
* 营业额数据统计
*
* @param begin
* @param end
* @return
*/
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额数据统计")
public Result<TurnoverReportVO> turnoverStatistics (
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){//日期类型需要注解描述日期格式
log.info("营业额数据统计:{},{}",begin,end);
return Result.success(reportService.getTurnoverStatistics(begin,end));
}
@Autowired
private OrderMapper orderMapper;
/**
* 根据时间区间统计营业额
* @param begin
* @param end
* @return
*/
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
//当前集合用于存放从begin到end范围内的每天的日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin); /*
dateList.add(begin.plusDays(1));*/
while (!begin.equals(end)){
//日期计算,计算指定日期的后一天对应 的日期
begin = begin.plusDays(1);
dateList.add(begin);
}
//存放每天的营业额
List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
//查询date日期对应的营业额,营业额是指状态为已完成的订单金额合计
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
//select sum(amount) from order where order_time > ? and order_time < ? and status = 5(已完成)
Map map = new HashMap();
map.put("begin",beginTime);
map.put("end",endTime);
map.put("status", Orders.COMPLETED);
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ?0.0 : turnover;
turnoverList.add(turnover);
}
return TurnoverReportVO
.builder()
.dateList(StringUtils.join(dateList,",")) //通过工具类把list每个元素取出来,,进行分割拼成字符串
.turnoverList(StringUtils.join(turnoverList,","))
.build();
}
<select id="sumByMap" resultType="java.lang.Double">
select sum(amount) from orders
<where>
<if test="begin !=null">
and order_time >= #{begin}
</if>
<if test="end !=null">
and order_time <= #{end}
</if>
<if test="status !=null">
and status = #{status}
</if>
</where>
</select>
营业额可以运行出来,但是数据都为0,原来是sql语句写错了
ctrl+shift+小键盘挪代码位置
sql语句非常容易写错啊,写错了也不报错,其他都差不多,就是sql语句还要联合查询
Day21 3.18工作台
工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率。
cv工程结束!!
Apache POI
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI 都是用于操作 Excel 文件。
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.16</version>
</dependency>
在导出excel表是发现是指向空指针,在classes文件中没有扫描到template目录,自己手动复制了一份成功了!