表设计
逻辑外键: 数据库中没有设计, 业务操作中注意
-
使得业务逻辑清楚命令
-
保证数据完整一致性:全由业务应用程序控制数据完整性
-
提高了性能
-
降低了开发维护难度
物理外键: 数据库中设计
常见的CRUD
以Category为例:
1. 增加
-
Controller
@RestController @RequestMapping("/admin/category") @Api(tags = "分类相关接口") @Slf4j public class CategoryController { @Autowired private CategoryService categoryService; @PostMapping @ApiOperation("新增分类") public Result<String> save(@RequestBody CategoryDTO categoryDTO){ log.info("新增分类:{}", categoryDTO); categoryService.save(categoryDTO); return Result.success(); } }
controller方法参数注解设置
与接口文档一致, 多封装成DTO对象传输给后端处理
-
query型 直接接受
-
json型 加@RequestBody
-
路径/{X} 加@PathVariable
-
application/x-www-form-urlencoded @RequestParam
-
Service
/** * 新增分类 * @param categoryDTO */ public void save(CategoryDTO categoryDTO) { Category category = new Category(); //属性拷贝 BeanUtils.copyProperties(categoryDTO, category); //分类状态默认为禁用状态0 category.setStatus(StatusConstant.DISABLE); categoryMapper.insert(category); }
-
Mapper
/** * 插入数据 * @param category */ @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" + " VALUES" + " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") @AutoFill(value = OperationType.INSERT) void insert(Category category);
AutoFill注解 实现 公共字段自动填充
作用于insert, update操作时, 自动更新create_*
, update_*
字段, 通过AOP实现
-
constant
/** * 公共字段自动填充相关常量 */ public class AutoFillConstant { /** * 实体类中的方法名称 */ public static final String SET_CREATE_TIME = "setCreateTime"; public static final String SET_UPDATE_TIME = "setUpdateTime"; public static final String SET_CREATE_USER = "setCreateUser"; public static final String SET_UPDATE_USER = "setUpdateUser"; }
-
aspect
/** * 自定义切面,实现公共字段自动填充处理逻辑 */ @Aspect @Component @Slf4j public class AutoFillAspect { /** * 切入点 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut(){} /** * 前置通知,在通知中进行公共字段的赋值 */ @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(); } } } }
-
annotation
/** * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { //数据库操作类型:UPDATE INSERT OperationType value(); }
-
enumeration
/** * 数据库操作类型 */ public enum OperationType { /** * 更新操作 */ UPDATE, /** * 插入操作 */ INSERT }
2. 删除
-
Controller
/** * 删除分类 * @param id * @return */ @DeleteMapping @ApiOperation("删除分类") public Result<String> deleteById(Long id){ log.info("删除分类:{}", id); categoryService.deleteById(id); return Result.success(); }
-
Service
/** * 根据id删除分类 * @param id */ public void deleteById(Long id) { //查询当前分类是否关联了菜品,如果关联了就抛出业务异常 Integer count = dishMapper.countByCategoryId(id); if(count > 0){ //当前分类下有菜品,不能删除 throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH); } //查询当前分类是否关联了套餐,如果关联了就抛出业务异常 count = setmealMapper.countByCategoryId(id); if(count > 0){ //当前分类下有菜品,不能删除 throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL); } //删除分类数据 categoryMapper.deleteById(id); }
-
Mapper
/** * 根据id删除分类 * @param id */ @Delete("delete from category where id = #{id}") void deleteById(Long id);
Result 结果返回类
统一返回结果
-
result
/** * 后端统一返回结果 * @param <T> */ @Data public class Result<T> implements Serializable { private Integer code; //编码:1成功,0和其它数字为失败 private String msg; //错误信息 private T data; //数据 public static <T> Result<T> success() { Result<T> result = new Result<T>(); result.code = 1; return result; } public static <T> Result<T> success(T object) { Result<T> result = new Result<T>(); result.data = object; result.code = 1; return result; } public static <T> Result<T> error(String msg) { Result result = new Result(); result.msg = msg; result.code = 0; return result; } }
-
constant
/** * 信息提示常量类 */ public class MessageConstant { public static final String PASSWORD_ERROR = "密码错误"; }
3. 查询
-
Controller
/** * 根据类型查询分类 * @param type * @return */ @GetMapping("/list") @ApiOperation("根据类型查询分类") public Result<List<Category>> list(Integer type){ List<Category> list = categoryService.list(type); return Result.success(list); }
-
Service
/** * 根据类型查询分类 * @param type * @return */ public List<Category> list(Integer type) { return categoryMapper.list(type); }
-
Mapper
/** * 根据类型查询分类 * @param type * @return */ List<Category> list(Integer type); <select id="list" resultType="Category"> select * from category where status = 1 <if test="type != null"> and type = #{type} </if> order by sort asc,create_time desc </select>
条件查询mapper写法
传参数时,可以提前封装成一个map对象,也可以都罗列出来 对于限制的参数, 使用<if>
标签连接
包装成实体再查询
避免每个条件查询都实现一次,而是用<if>
标签动态查询。
4. 修改
-
Controller
/** * 修改分类 * @param categoryDTO * @return */ @PutMapping @ApiOperation("修改分类") public Result<String> update(@RequestBody CategoryDTO categoryDTO){ categoryService.update(categoryDTO); return Result.success(); }
-
Service
/** * 修改分类 * @param categoryDTO */ public void update(CategoryDTO categoryDTO) { Category category = new Category(); BeanUtils.copyProperties(categoryDTO,category); categoryMapper.update(category); }
-
Mapper
/** * 根据id修改分类 * @param category */ @AutoFill(value = OperationType.UPDATE) void update(Category category); <update id="update" parameterType="Category"> update category <set> <if test="type != null"> type = #{type}, </if> <if test="name != null"> name = #{name}, </if> <if test="sort != null"> sort = #{sort}, </if> <if test="status != null"> status = #{status}, </if> <if test="updateTime != null"> update_time = #{updateTime}, </if> <if test="updateUser != null"> update_user = #{updateUser} </if> </set> where id = #{id} </update>
ThreadLocal拿到当前用户的id
-
context
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(); } }
校验jwt时, 将解析出的id放入
BaseContext.setCurrentId(empId);
5. 分页查询
-
Controller
/** * 分类分页查询 * @param categoryPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("分类分页查询") public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){ log.info("分页查询:{}", categoryPageQueryDTO); PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO); return Result.success(pageResult); }
-
Service
import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; /** * 分页查询 * @param categoryPageQueryDTO * @return */ public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) { PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize()); //下一条sql进行分页,自动加入limit关键字分页 Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO); return new PageResult(page.getTotal(), page.getResult()); }
-
Mapper
/** * 分页查询 * @param categoryPageQueryDTO * @return */ Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO); <select id="pageQuery" resultType="com.sky.entity.Category"> select * from category <where> <if test="name != null and name != ''"> and name like concat('%',#{name},'%') </if> <if test="type != null"> and type = #{type} </if> </where> order by sort asc , create_time desc </select>
PageResult 封装分页查询结果
/** * 封装分页查询结果 */ @Data @AllArgsConstructor @NoArgsConstructor public class PageResult implements Serializable { private long total; //总记录数 private List records; //当前页数据集合 }
管理端登录流程
基于jwt登录的流程
-
dto
@Data @ApiModel(description = "员工登录时传递的数据模型") public class EmployeeLoginDTO implements Serializable { @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; }
-
controller
/** * 登录 * * @param employeeLoginDTO * @return */ @PostMapping("/login") @ApiOperation(value = "员工登录") 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); }
JwtUtil工具类
实现跨域认证 ryf-jwt
-
util
import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; public class JwtUtil { /** * 生成jwt * 使用Hs256算法, 私匙使用固定秘钥 * * @param secretKey jwt秘钥 * @param ttlMillis jwt过期时间(毫秒) * @param claims 设置的信息 * @return */ public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) { // 指定签名的时候使用的签名算法,也就是header那部分 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成JWT的时间 long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date(expMillis); // 设置jwt的body JwtBuilder builder = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) // 设置过期时间 .setExpiration(exp); return builder.compact(); } /** * Token解密 * * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个 * @param token 加密后的token * @return */ public static Claims parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } }
-
service
/** * 员工登录 * * @param employeeLoginDTO * @return */ public Employee login(EmployeeLoginDTO employeeLoginDTO) { String username = employeeLoginDTO.getUsername(); String password = employeeLoginDTO.getPassword(); // 1. 根据用户名查询数据库中的数据 Employee employee = employeeMapper.getByUsername(username); // 处理各种异常情况(用户名不存在、密码不对、账号被锁定) if (employee == null) { //账号不存在 throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND); } // 2. 密码比对 // 对前端传过来的明文密码进行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; }
-
mapper
/** * 根据用户名查询员工 * @param username * @return */ @Select("select * from employee where username = #{username}") Employee getByUsername(String username);
-
vo
@Data @Builder @NoArgsConstructor @AllArgsConstructor @ApiModel(description = "员工登录返回的数据格式") public class EmployeeLoginVO implements Serializable { @ApiModelProperty("主键值") private Long id; @ApiModelProperty("用户名") private String userName; @ApiModelProperty("姓名") private String name; @ApiModelProperty("jwt令牌") private String token; }
请求拦截
-
handler
/** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ 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; } } }
-
config
/** * 配置类,注册web层相关组件 */ @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; /** * 注册自定义拦截器 * @param registry */ 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"); } }
properties类
相当于bean
-
properties
@Component @ConfigurationProperties(prefix = "sky.jwt") @Data public class JwtProperties { /** * 管理端员工生成jwt令牌相关配置 */ private String adminSecretKey; private long adminTtl; private String adminTokenName; /** * 用户端微信用户生成jwt令牌相关配置 */ private String userSecretKey; private long userTtl; private String userTokenName; }
-
application.yml
sky: jwt: # 设置jwt签名加密时使用的秘钥 admin-secret-key: itcast # 设置jwt过期时间 admin-ttl: 7200000 # 设置前端传递过来的令牌名称 admin-token-name: token # 用户端 user-secret-key: itheima user-ttl: 7200000 user-token-name: authentication
异常处理
-
exception
/** * 业务异常 */ public class BaseException extends RuntimeException { public BaseException() { } public BaseException(String msg) { super(msg); } }
BaseException继承
-
handler
/** * 全局异常处理器,处理项目中抛出的业务异常 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获业务异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(BaseException ex){ log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } /** * 处理SQL异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ //Duplicate entry 'zhangsan' for key 'employee.idx_username' 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); } } }
微信小程序登录
-
dto
/** * C端用户登录 */ @Data public class UserLoginDTO implements Serializable { private String code; }
-
controller
/** * 微信登录 * @param userLoginDTO * @return */ @PostMapping("/login") @ApiOperation("微信登录") public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){ log.info("微信用户登录:{}",userLoginDTO.getCode()); //微信登录 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); }
-
service
//微信服务接口地址 public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session"; /** * 微信登录 * @param userLoginDTO * @return */ public User wxLogin(UserLoginDTO userLoginDTO) { String openid = getOpenid(userLoginDTO.getCode()); //判断openid是否为空,如果为空表示登录失败,抛出业务异常 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; } /** * 调用微信接口服务,获取微信用户的openid * @param code * @return */ private String getOpenid(String code){ //调用微信接口服务,获得当前微信用户的openid Map<String, String> map = new HashMap<>(); map.put("appid",weChatProperties.getAppid()); map.put("secret",weChatProperties.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; }
HttpClient 支持HTTP协议的客户端编程工具包
/** * Http工具类 */ public class HttpClientUtil { static final int TIMEOUT_MSEC = 5 * 1000; /** * 发送GET方式请求 * @param url * @param paramMap * @return */ public static String doGet(String url,Map<String,String> paramMap){ // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); String result = ""; CloseableHttpResponse response = null; try{ URIBuilder builder = new URIBuilder(url); if(paramMap != null){ for (String key : paramMap.keySet()) { builder.addParameter(key,paramMap.get(key)); } } URI uri = builder.build(); //创建GET请求 HttpGet httpGet = new HttpGet(uri); //发送请求 response = httpClient.execute(httpGet); //判断响应状态 if(response.getStatusLine().getStatusCode() == 200){ result = EntityUtils.toString(response.getEntity(),"UTF-8"); } }catch (Exception e){ e.printStackTrace(); }finally { try { response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return result; } }
-
vo
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserLoginVO implements Serializable { private Long id; private String openid; private String token; }
接下来的管理就类似于管理端
消息转换器
-
将请求报文转化为Java对象
-
将Java对象转化为响应报文
-
WebMvcConfiguration.java
/** * 扩展Spring MVC框架的消息转化器 * @param converters */ protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..."); //创建一个消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据 converter.setObjectMapper(new JacksonObjectMapper()); //将自己的消息转化器加入容器中 converters.add(0,converter); }
-
json
/** * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] */ public class JacksonObjectMapper extends ObjectMapper { public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public JacksonObjectMapper() { super(); //收到未知属性时不报异常 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时,属性不存在的兼容处理 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); //注册功能模块 例如,可以添加自定义序列化器和反序列化器 this.registerModule(simpleModule); } }
缓存
Redis
Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,对reids底层开发包(Jedis,JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作。
-
config
@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()); return redisTemplate; } }
-
controller
/** * 根据分类id查询菜品 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list(Long categoryId) { //构造redis中的key,规则:dish_分类id String key = "dish_" + categoryId; //查询redis中是否存在菜品数据 List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if(list != null && list.size() > 0){ //如果存在,直接返回,无须查询数据库 return Result.success(list); } Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 //如果不存在,查询数据库,将查询到的数据放入redis中 list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key, list); return Result.success(list); }
Spring Cache
要么添加configuration类,注入容器; 要么启动类上加注解@EnableCaching//开发缓存注解功能
/** * 新增套餐 * * @param setmealDTO * @return */ @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100 public Result save(@RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDish(setmealDTO); return Result.success(); } /** * 批量删除套餐 * * @param ids * @return */ @DeleteMapping @ApiOperation("批量删除套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result delete(@RequestParam List<Long> ids) { setmealService.deleteBatch(ids); return Result.success(); } /** * 修改套餐 * * @param setmealDTO * @return */ @PutMapping @ApiOperation("修改套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result update(@RequestBody SetmealDTO setmealDTO) { setmealService.update(setmealDTO); return Result.success(); }
阿里云OSS
-
properties
@Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
-
application.yml
sky: alioss: endpoint: ${sky.alioss.endpoint} access-key-id: ${sky.alioss.access-key-id} access-key-secret: ${sky.alioss.access-key-secret} bucket-name: ${sky.alioss.bucket-name}
-
config
/** * 配置类,用于创建AliOssUtil对象 */ @Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){ log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }
-
util
@Data @AllArgsConstructor @Slf4j public class AliOssUtil { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; /** * 文件上传 * * @param bytes * @param objectName * @return */ public String upload(byte[] bytes, String objectName) { // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { // 创建PutObject请求。 ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes)); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } //文件访问路径规则 https://BucketName.Endpoint/ObjectName StringBuilder stringBuilder = new StringBuilder("https://"); stringBuilder .append(bucketName) .append(".") .append(endpoint) .append("/") .append(objectName); log.info("文件上传到:{}", stringBuilder.toString()); return stringBuilder.toString(); } }
-
controller
/** * 文件上传 * @param file * @return */ @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file){ log.info("文件上传:{}",file); try { //原始文件名 String originalFilename = file.getOriginalFilename(); //截取原始文件名的后缀 dfdfdf.png String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //构造新文件名称 String objectName = UUID.randomUUID().toString() + extension; //文件的请求路径 String filePath = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}", e); } return Result.error(MessageConstant.UPLOAD_FAILED); }
Spring Task 任务调度
启动类加@EnableScheduling //开启任务调度
-
task
/** * 定时任务类,定时处理订单状态 */ @Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; /** * 处理超时订单的方法 */ @Scheduled(cron = "0 * * * * ? ") //每分钟触发一次 public void processTimeoutOrder(){ log.info("定时处理超时订单:{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); // select * from orders where status = ? and order_time < (当前时间 - 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 * * ?") //每天凌晨1点触发一次 public void processDeliveryOrder(){ log.info("定时处理处于派送中的订单:{}",LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-60); List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time); if(ordersList != null && ordersList.size() > 0){ for (Orders orders : ordersList) { orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } }
WebSocket 向客户端推送消息
实现 来单提醒、催单
-
websocket
@Component @ServerEndpoint("/ws/{sid}") public class WebSocketServer { //存放会话对象 private static Map<String, Session> sessionMap = new HashMap(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接"); sessionMap.put(sid, session); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } /** * 连接关闭调用的方法 * * @param sid */ @OnClose public void onClose(@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } /** * 群发 * * @param message */ public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { //服务器向客户端发送消息 session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
-
service
/** * 支付成功,修改订单状态 * * @param outTradeNo */ public void paySuccess(String outTradeNo) { // 当前登录用户id Long userId = BaseContext.getCurrentId(); // 根据订单号查询当前用户的订单 Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId); // 根据订单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()); map.put("content","订单号:" + outTradeNo); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); } /** * 客户催单 * @param id */ public void reminder(Long id) { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在 if (ordersDB == null) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Map map = new HashMap(); map.put("type",2); //1表示来单提醒 2表示客户催单 map.put("orderId",id); map.put("content","订单号:" + ordersDB.getNumber()); //通过websocket向客户端浏览器推送消息 webSocketServer.sendToAllClient(JSON.toJSONString(map)); }
Apache POI 文档读写
-
controller
/** * 导出运营数据报表 * @param response */ @GetMapping("/export") @ApiOperation("导出运营数据报表") public void export(HttpServletResponse response){ reportService.exportBusinessData(response); }
-
service
/** * 导出运营数据报表 * @param response */ public void exportBusinessData(HttpServletResponse response) { //1. 查询数据库,获取营业数据---查询最近30天的运营数据 LocalDate dateBegin = LocalDate.now().minusDays(30); LocalDate dateEnd = LocalDate.now().minusDays(1); //查询概览数据 BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX)); //2. 通过POI将数据写入到Excel文件中 InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); try { //基于模板文件创建一个新的Excel文件 XSSFWorkbook excel = new XSSFWorkbook(in); //获取表格文件的Sheet页 XSSFSheet sheet = excel.getSheet("Sheet1"); //填充数据--时间 sheet.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd); //获得第4行 XSSFRow row = sheet.getRow(3); row.getCell(2).setCellValue(businessDataVO.getTurnover()); row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate()); row.getCell(6).setCellValue(businessDataVO.getNewUsers()); //获得第5行 row = sheet.getRow(4); row.getCell(2).setCellValue(businessDataVO.getValidOrderCount()); row.getCell(4).setCellValue(businessDataVO.getUnitPrice()); //填充明细数据 for (int i = 0; i < 30; i++) { LocalDate date = dateBegin.plusDays(i); //查询某一天的营业数据 BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); //获得某一行 row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString()); row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(3).setCellValue(businessData.getValidOrderCount()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(5).setCellValue(businessData.getUnitPrice()); row.getCell(6).setCellValue(businessData.getNewUsers()); } //3. 通过输出流将Excel文件下载到客户端浏览器 ServletOutputStream out = response.getOutputStream(); excel.write(out); //关闭资源 out.close(); excel.close(); } catch (IOException e) { e.printStackTrace(); } }
复杂业务详解
1. 菜品
新增 -> 事务操作, 插入菜品 并且 插入其口味
将前端传来的DishDTO对象拆解为 Dish和DishFlavor对象, 数据库中 dish_flavor表中的字段dish_id向上关联dish表
查询操作相似。
-
dto
@Data public class DishDTO implements Serializable { private Long id; //菜品名称 private String name; //菜品分类id private Long categoryId; //菜品价格 private BigDecimal price; //图片 private String image; //描述信息 private String description; //0 停售 1 起售 private Integer status; //口味 private List<DishFlavor> flavors = new ArrayList<>(); }
加入 口味
-
controller
/** * 新增菜品 * * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result save(@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}", dishDTO); dishService.saveWithFlavor(dishDTO); //清理缓存数据 String key = "dish_" + dishDTO.getCategoryId(); cleanCache(key); return Result.success(); }
-
service
/** * 新增菜品和对应的口味 * * @param dishDTO */ @Transactional public void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //向菜品表插入1条数据 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
通过加useGeneratedKeys="true" keyProperty="id"
返回主键值
/** * 插入菜品数据 * * @param dish */ @AutoFill(value = OperationType.INSERT) void insert(Dish dish); <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into dish (name, category_id, price, image, description, create_time, update_time, create_user, update_user, status) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status}) </insert> /** * 批量插入口味数据 * @param flavors */ 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},#{df.value}) </foreach> </insert>
删除 -> 是否起售,是否关联套餐, 关联口味
-
controller
/** * 菜品批量删除 * * @param ids * @return */ @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) { log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); }
-
service
/** * 菜品批量删除 * * @param ids */ @Transactional public void deleteBatch(List<Long> ids) { // 1.判断当前菜品是否能够删除---是否存在起售中的菜品?? for (Long id : ids) { Dish dish = dishMapper.getById(id); if (dish.getStatus() == StatusConstant.ENABLE) { //当前菜品处于起售中,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } // 2.判断当前菜品是否能够删除---是否被套餐关联了?? List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if (setmealIds != null && setmealIds.size() > 0) { //当前菜品被套餐关联了,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } // 3.删除菜品表中的菜品数据 for (Long id : ids) { dishMapper.deleteById(id); // 4.删除菜品关联的口味数据 dishFlavorMapper.deleteByDishId(id); } }
-
mapper
/** * 根据菜品id查询对应的套餐id * * @param dishIds * @return */ //select setmeal_id from setmeal_dish where dish_id in (1,2,3,4) List<Long> getSetmealIdsByDishIds(List<Long> dishIds); <select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="dishIds" item="dishId" separator="," open="(" close=")"> #{dishId} </foreach> </select>
修改 -> 改菜品,删除原有口味再重新添加
-
controller
/** * 修改菜品 * * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}", dishDTO); dishService.updateWithFlavor(dishDTO); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); }
-
service
/** * 根据id修改菜品基本信息和对应的口味信息 * * @param dishDTO */ public void updateWithFlavor(DishDTO dishDTO) { 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); } }
起/停售 -> 菜品及相关套餐的状态
-
controller
/** * 菜品起售停售 * * @param status * @param id * @return */ @PostMapping("/status/{status}") @ApiOperation("菜品起售停售") public Result<String> startOrStop(@PathVariable Integer status, Long id) { dishService.startOrStop(status, id); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return Result.success(); }
-
service
/** * 菜品起售停售 * * @param status * @param id */ @Transactional public void startOrStop(Integer status, Long id) { Dish dish = Dish.builder() .id(id) .status(status) .build(); dishMapper.update(dish); if (status == StatusConstant.DISABLE) { // 如果是停售操作,还需要将包含当前菜品的套餐也停售 List<Long> dishIds = new ArrayList<>(); dishIds.add(id); // select setmeal_id from setmeal_dish where dish_id in (?,?,?) List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds); if (setmealIds != null && setmealIds.size() > 0) { for (Long setmealId : setmealIds) { Setmeal setmeal = Setmeal.builder() .id(setmealId) .status(StatusConstant.DISABLE) .build(); setmealMapper.update(setmeal); } } } }
2. 店铺状态
直接放在redis缓存中
-
controller
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(); } /** * 获取店铺的营业状态 * @return */ @GetMapping("/status") @ApiOperation("获取店铺的营业状态") public Result<Integer> getStatus(){ Integer status = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中" : "打烊中"); return Result.success(status); }
3. 套餐
和菜品为 多对多 关系, 两者之间有一张 setmeal_dish 表
-
dto
@Data public class SetmealDTO implements Serializable { private Long id; //分类id private Long categoryId; //套餐名称 private String name; //套餐价格 private BigDecimal price; //状态 0:停用 1:启用 private Integer status; //描述信息 private String description; //图片 private String image; //套餐菜品关系 private List<SetmealDish> setmealDishes = new ArrayList<>(); }
插入 -> 菜品处理
-
controller
/** * 新增套餐 * * @param setmealDTO * @return */ @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100 public Result save(@RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDish(setmealDTO); return Result.success(); }
-
service
/** * 新增套餐,同时需要保存套餐和菜品的关联关系 * * @param 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);
删除类似
查询 -> 多对多关系,联表查询
-
controller
/** * 根据id查询套餐,用于修改页面回显数据 * * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询套餐") public Result<SetmealVO> getById(@PathVariable Long id) { SetmealVO setmealVO = setmealService.getByIdWithDish(id); return Result.success(setmealVO); }
-
mapper
<select id="getByIdWithDish" parameterType="long" resultMap="setmealAndDishMap"> select a.*, b.id sd_id, b.setmeal_id, b.dish_id, b.name sd_name, b.price sd_price, b.copies from setmeal a left join setmeal_dish b on a.id = b.setmeal_id where a.id = #{id} </select>
修改 -> 还要删除关联关系,再添加新的
重点在于setmealId
-
service
/** * 修改套餐 * * @param setmealDTO */ @Transactional public void update(SetmealDTO setmealDTO) { Setmeal setmeal = new Setmeal(); BeanUtils.copyProperties(setmealDTO, setmeal); //1、修改套餐表,执行update setmealMapper.update(setmeal); //套餐id Long setmealId = setmealDTO.getId(); //2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete setmealDishMapper.deleteBySetmealId(setmealId); List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes(); setmealDishes.forEach(setmealDish -> { setmealDish.setSetmealId(setmealId); }); //3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert setmealDishMapper.insertBatch(setmealDishes); }
起/停售 -> 先判断菜品起/停售
-
service
/** * 套餐起售、停售 * * @param status * @param id */ public void startOrStop(Integer status, Long id) { //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售" if (status == StatusConstant.ENABLE) { //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ? List<Dish> dishList = dishMapper.getBySetmealId(id); if (dishList != null && dishList.size() > 0) { dishList.forEach(dish -> { if (StatusConstant.DISABLE == dish.getStatus()) { throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED); } }); } } Setmeal setmeal = Setmeal.builder() .id(id) .status(status) .build(); setmealMapper.update(setmeal); }
-
mapper
/** * 根据套餐id查询菜品 * @param setmealId * @return */ @Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}") List<Dish> getBySetmealId(Long setmealId);
ocntroller如果重名,可以在@RestController("")
中加值区别
4. 购物车
存setmeal/dish的条目
-
dto
@Data public class ShoppingCartDTO implements Serializable { private Long dishId; private Long setmealId; private String dishFlavor; }
增加 -> 首次添加?添加的是dish还是setmeal ?
-
service
/** * 添加购物车 * @param shoppingCartDTO */ 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); } }
查看 -> 使用userId
-
service
/** * 查看购物车 * @return */ public List<ShoppingCart> showShoppingCart() { //获取到当前微信用户的id Long userId = BaseContext.getCurrentId(); ShoppingCart shoppingCart = ShoppingCart.builder() .userId(userId) .build(); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); return list; }
删除 -> 是否只剩一个?
-
service
/** * 删除购物车中一个商品 * @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); } } }
5. 地址铺
设置默认 -> 全部设置为非默认,当前设置为默认
-
controller
/** * 设置默认地址 * * @param addressBook * @return */ @PutMapping("/default") @ApiOperation("设置默认地址") public Result setDefault(@RequestBody AddressBook addressBook) { addressBookService.setDefault(addressBook); return Result.success(); }
-
service
/** * 设置默认地址 * * @param addressBook */ @Transactional public void setDefault(AddressBook addressBook) { //1、将当前用户的所有地址修改为非默认地址 update address_book set is_default = ? where user_id = ? addressBook.setIsDefault(0); addressBook.setUserId(BaseContext.getCurrentId()); addressBookMapper.updateIsDefaultByUserId(addressBook); //2、将当前地址改为默认地址 update address_book set is_default = ? where id = ? addressBook.setIsDefault(1); addressBookMapper.update(addressBook); }
6. 订单
orders, order_detail ( order_id ) 一对多关系
订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
支付状态 0未支付 1已支付 2退款
用户下单 -> 异常?订单+订单明细,清空购物车,返回VO
-
dto
@Data public class OrdersSubmitDTO implements Serializable { //地址簿id private Long addressBookId; //付款方式 private int payMethod; //备注 private String remark; //预计送达时间 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime estimatedDeliveryTime; //配送状态 1立即送出 0选择具体时间 private Integer deliveryStatus; //餐具数量 private Integer tablewareNumber; //餐具数量状态 1按餐量提供 0选择具体数量 private Integer tablewareStatus; //打包费 private Integer packAmount; //总金额 private BigDecimal amount; }
-
service
/** * 用户下单 * @param ordersSubmitDTO * @return */ @Transactional public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) { //1. 处理各种业务异常(地址簿为空、购物车数据为空) AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId()); if(addressBook == null){ //抛出业务异常 throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL); } //检查用户的收货地址是否超出配送范围 //checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail()); //查询当前用户的购物车数据 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. 向订单表插入1条数据 Orders orders = new Orders(); 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.setAddress(addressBook.getDetail()); 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()) .orderNumber(orders.getNumber()) .orderAmount(orders.getAmount()) .build(); return orderSubmitVO; }
订单支付 -> 更新订单状态,推送消息
-
dto
@Data public class OrdersPaymentDTO implements Serializable { //订单号 private String orderNumber; //付款方式 private Integer payMethod; }
-
controller
/** * 订单支付 * * @param ordersPaymentDTO * @return */ @PutMapping("/payment") @ApiOperation("订单支付") public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception { log.info("订单支付:{}", ordersPaymentDTO); OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO); log.info("生成预支付交易单:{}", orderPaymentVO); return Result.success(orderPaymentVO); }
-
service
/** * 订单支付 * * @param ordersPaymentDTO * @return */ public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception { JSONObject jsonObject = new JSONObject(); jsonObject.put("code", "ORDERPAID"); OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class); vo.setPackageStr(jsonObject.getString("package")); // 成功 paySuccess(ordersPaymentDTO.getOrderNumber()); return vo; } /** * 支付成功,修改订单状态 * * @param outTradeNo */ public void paySuccess(String outTradeNo) { // 当前登录用户id Long userId = BaseContext.getCurrentId(); // 根据订单号查询当前用户的订单 Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId); // 根据订单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()); map.put("content","订单号:" + outTradeNo); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); }
取消订单 -> 异常?更改状态
-
controller
/** * 用户取消订单 * * @return */ @PutMapping("/cancel/{id}") @ApiOperation("取消订单") public Result cancel(@PathVariable("id") Long id) throws Exception { orderService.userCancelById(id); return Result.success(); }
-
service
/** * 用户取消订单 * * @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)) { //支付状态修改为 退款 orders.setPayStatus(Orders.REFUND); } // 更新订单状态、取消原因、取消时间 orders.setStatus(Orders.CANCELLED); orders.setCancelReason("用户取消"); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
再来一单 -> 订单详情 复制到 购物车
-
controller
/** * 再来一单 * * @param id * @return */ @PostMapping("/repetition/{id}") @ApiOperation("再来一单") public Result repetition(@PathVariable Long id) { orderService.repetition(id); return Result.success(); }
-
service
/** * 再来一单 * * @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); }
催单 -> 推送消息
-
controller
/** * 客户催单 * @param id * @return */ @GetMapping("/reminder/{id}") @ApiOperation("客户催单") public Result reminder(@PathVariable("id") Long id){ orderService.reminder(id); return Result.success(); }
-
service
/** * 客户催单 * @param id */ public void reminder(Long id) { // 根据id查询订单 Orders ordersDB = orderMapper.getById(id); // 校验订单是否存在 if (ordersDB == null) { throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR); } Map map = new HashMap(); map.put("type",2); //1表示来单提醒 2表示客户催单 map.put("orderId",id); map.put("content","订单号:" + ordersDB.getNumber()); //通过websocket向客户端浏览器推送消息 webSocketServer.sendToAllClient(JSON.toJSONString(map)); }
商家拒单 -> 更新订单状态 + 退钱
-
controller
/** * 拒单 * * @return */ @PutMapping("/rejection") @ApiOperation("拒单") public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception { orderService.rejection(ordersRejectionDTO); return Result.success(); }
-
service
/** * 拒单 * * @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(); Orders orders = new Orders(); orders.setId(ordersDB.getId()); if (payStatus == Orders.PAID) { //用户已支付,需要退款 orders.setStatus(Orders.REFUND); } // 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间 orders.setStatus(Orders.CANCELLED); orders.setRejectionReason(ordersRejectionDTO.getRejectionReason()); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
取消订单, 派送订单,完成订单 类似
7. 统计
和时间关联
营业额
-
controller
/** * 营业额统计 * @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)); }
-
service
/** * 统计指定时间区间内的营业额数据 * * @param begin * @param end * @return */ public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) { //当前集合用于存放从begin到end范围内的每天的日期 List<LocalDate> dateList = new ArrayList<>(); dateList.add(begin); 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 orders where order_time > beginTime and order_time < endTime 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, ",")) .turnoverList(StringUtils.join(turnoverList, ",")) .build(); }
-
mapper
<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>
订单完成率
//计算时间区间内的订单总数量 Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get(); //计算时间区间内的有效订单数量 Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get(); Double orderCompletionRate = 0.0; if(totalOrderCount != 0){ //计算订单完成率 orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount; }