美文网首页Spring Boot
SpringBoot项目标准化代码编写

SpringBoot项目标准化代码编写

作者: 一击必中 | 来源:发表于2021-04-15 12:00 被阅读0次

    一、认识项目结构

    1、项目基本结构

    • 基础功能结构
    entitys // 存放实体类
    enums // 存放枚举类
    dto  // 存放入参结构
    vo   // 存放出参结构
    utils // 存放相关工具类
    
    • 核心逻辑功能结构
    -controller  // 基本参数校验
    --service    // 存放服务接口
    ---impl       // 存放服务接口实现类(核心业务逻辑功能开发)
    ----dao      // 持久层,数据增删改查
    -----provider // 动态sql拼接层,编写动态的sql
    

    2、统一消息返回

    • ErrorCodeEnum.java(存放各类错误码)
    public enum ErrorCodeEnum {
        /**
         * 错误码
         */
        ERROR(9999, "系统异常"),
        HTTP_CONNECTION_OVERTIME(9998, "连接超时"),
        FREQUENTLY_REQUEST(9003, "操作频繁"),
        INVALID_RSA_KEY(9002, "超时失效"),
        TOKEN_TIMEOUT(9005, "token失效"),
        INVALID_PARAMS(9001, "非法参数"),
        SIGN_ERROR(9000, "签名错误"),
        INVALID_STATUS(9004, "状态不符"),
    
        OK(200, "请求通过"),
        NO(201, "请求不通过"),
        TIP(202, "提示"),
    
        private Integer code;
    
        private String message;
    
        ErrorCodeEnum(Integer code, String message) {
            this.code = code;
            this.message = message;
        }
    
        public Integer getCode() {
            return code;
        }
    
        public String getMessage() {
            return message;
        }
    }
    
    • ErrorCodeException.java 统一异常结构
    @ToString
    public class ErrorCodeException extends RuntimeException {
    
        private static final long serialVersionUID = -7638041501183925225L;
    
        private Integer code;
    
        public ErrorCodeException(ErrorCodeEnum errorCode, String msg) {
            super(msg);
            this.code = errorCode.getCode();
        }
    
        public ErrorCodeException(ErrorCodeEnum errorCode) {
            super(errorCode.getMessage());
            this.code = errorCode.getCode();
        }
    
        public Integer getCode() {
            return code;
        }
    
        public void setCode(Integer code) {
            this.code = code;
        }
    
    }
    
    • SimpleMessage.java (简易信息返回)
    @Data
    public class SimpleMessage implements Serializable {
        private static final long serialVersionUID = -2957516153008725933L;
        private Integer errorCode;
        private String errorMsg;
    
        public SimpleMessage(ErrorCodeEnum errorCode, String errorMsg) {
            this.errorCode = errorCode.getCode();
            this.errorMsg = errorMsg;
        }
    }
    
    • MessageBean.java (丰富信息返回)
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class MessageBean implements Serializable {
        private static final long serialVersionUID = 7192766535561421181L;
        private String errorMsg;
        private Object data;
        private Integer errorCode;
    }
    
    • AppResponseBodyAdvice.java 处理统一返回
    @Slf4j
    @ControllerAdvice
    public class AppResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
        }
    
        @SuppressWarnings("Duplicates")
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                      Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                      ServerHttpRequest request,
                                      ServerHttpResponse response) {
           // 特殊返回类型处理
            if (body instanceof SimpleMessage || body instanceof MessageBean) {
                return body;
            }
            MessageBean messageBean = new MessageBean();
            messageBean.setErrorCode(ErrorCodeEnum.OK.getCode());
            messageBean.setErrorMsg(ErrorCodeEnum.OK.getMessage());
            messageBean.setData(body);
            return messageBean;
        }
    }
    

    · ManagerExceptionHandler.java (全局异常处理)

    @Slf4j
    @ControllerAdvice
    public class ManagerExceptionHandler {
    
        @ExceptionHandler(value = ErrorCodeException.class)
        @ResponseBody
        public SimpleMessage myErrorHandler(ErrorCodeException e) {
            SimpleMessage message = new SimpleMessage();
            message.setErrorCode(e.getCode());
            message.setErrorMsg(e.getMessage());
            return message;
        }
    
        @ExceptionHandler(value = DuplicateKeyException.class)
        @ResponseBody
        public SimpleMessage duplicateKeyErrorHandler() {
            SimpleMessage message = new SimpleMessage();
            message.setErrorCode(ErrorCodeEnum.NO.getCode());
            message.setErrorMsg("数据重复");
            return message;
        }
    
        @ExceptionHandler(value = MaxUploadSizeExceededException.class)
        @ResponseBody
        public SimpleMessage fileSizeLimitErrorHandler() {
            SimpleMessage message = new SimpleMessage();
            message.setErrorCode(ErrorCodeEnum.NO.getCode());
            message.setErrorMsg("图片过大");
            return message;
        }
    
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        public SimpleMessage errorHandler(Exception e, HttpServletRequest request) {
            SimpleMessage message = new SimpleMessage();
            message.setErrorCode(ErrorCodeEnum.ERROR.getCode());
            message.setErrorMsg(ErrorCodeEnum.ERROR.getMessage());
            log.error("url [{}] params [{}] error", request.getRequestURI(), JSON.toJSONString(request.getParameterMap()), e);
            return message;
        }
    }
    

    二、了解常用规范

    1.实体类(以设备表功能为例)

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class DeviceInfo implements Serializable {
        private static final long serialVersionUID = -4951578247348538266L;
        /**
         * 自增ID
         */
        @NotNull(message = "设备ID不能为空", groups = {Update.class})
        private Integer id;
        /**
         * 设备名称
         */
        @NotBlank(message = "设备名称不能为空", groups = {Insert.class, Update.class})
        private String deviceName;
        /**
         * 设备码
         */
        @NotNull(message = "设备码不能为空", groups = {Insert.class, Update.class})
        private DeviceTypeEnum deviceCode;
        /**
         * 二级分类ID
         */
        @NotNull(message = "二级分类ID", groups = {Insert.class, Update.class})
        private Integer parentId;
        /**
         * 图标地址
         */
        @NotBlank(message = "图标地址不能为空", groups = {Insert.class, Update.class})
        private String iconUrl;
        /**
         * 排序
         */
        @NotNull(message = "排序不能为空", groups = {Insert.class, Update.class})
        private Integer sort;
        /**
         * 创建人
         */
        private String createNo;
        /**
         * 更新人
         */
        private String updateNo;
        /**
         * 创建时间
         */
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime createTime;
        /**
         * 更新时间
         */
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime updateTime;
    
    }
    
    • 讲解点

    1、 Lomok 注解
    2、@Valid 注解 【@NotNull 、@NotBlank、@NotEmpty、@JsonFormat、@DateTimeFormat】
    3、自定义接口【Insert.class、Update.class】
    4、javadoc 注释
    5、@Valid延伸、多级结构校验

    • 注意点

    【强制】1、实体类必须与表结构完全对应一致
    【强制】2、实体类必须实现序列化接口
    【强制】3、增、删、改、查的接口,必须使用公共接口
    【强制】4、java类、各参数值必须使用 javadoc 注释(具体注释情况,需按照alibaba编程规范执行)
    【推荐】5、添加@JsonInclude(JsonInclude.Include.NON_NULL) 和 @Builder

    2.DTO类

    @Data
    @ToString(callSuper = true)
    @EqualsAndHashCode(callSuper = true)
    public class DeviceInfoDTO extends DeviceInfo implements Serializable {
        private static final long serialVersionUID = 7535820681677648701L;
    
        /**
         * 当前页
         */
        @NotNull(message = "当前页不能为空", groups = {PageQuery.class})
        private Integer pageNumber;
        /**
         * 每页的数量
         */
        @NotNull(message = "每页显示数量不能为空", groups = {PageQuery.class})
        private Integer pageSize;
    
    }
    
    • 讲解点

    1、extend 继承的好处
    2、PageQuery.class 接口

    • 注意点

    【强制】DTO 类必须继承至实体类
    【强制】DTO 类型实现后必须实现序列化接口
    【强制】DTO 类的命名,必须最后的DTO 为大写
    【建议】在需要分页功能中,pageNumber和pageSzie必传

    2.VO类

    @Data
    @ToString(callSuper = true)
    @EqualsAndHashCode(callSuper = true)
    public class DeviceInfoVO extends DeviceInfo implements Serializable {
        private static final long serialVersionUID = -5721621832940504628L;
        /**
         * 二级分类
         */
        private String secondaryCategoryName;
        /**
         * 一级分类
         */
        private String primaryCategoryName;
    
    }
    
    • 讲解点

    用法与DTO保持一致

    • 注意点

    【强制】VO 类必须继承至实体类
    【强制】VO 类型实现后必须实现序列化接口
    【强制】VO 类的命名,必须最后的DTO 为大写

    3.枚举类

    public enum DeviceTypeEnum {
    
        /**
         * 智能插座
         */
        SOCKET("智能插座"),
        /**
         * 通断器
         */
        ON_OFF("通断器")
        ;
    
        private final String str;
    
        DeviceTypeEnum(String str) {
            this.str = str;
        }
    
        /**
         * 获取key,value值
         *
         * @return List<CodeValuePair>
         */
        public static List<CodeValuePair> getStatusMap() {
            List<CodeValuePair> list = new ArrayList<>();
            for (DeviceTypeEnum balanceTypeEnum : DeviceTypeEnum.values()) {
                list.add(CodeValuePair.builder().code(balanceTypeEnum.name()).value(balanceTypeEnum.getStr()).build());
            }
            return list;
        }
    
        public String getStr() {
            return str;
        }
    }
    
    • 讲解点

    1、使用类型枚举的好处
    2、使用枚举的注意点(保持枚举更新的一致性)
    3、字符串枚举的好处(简述间隙锁)

    • 注意点

    【强制】涉及到类型的必须使用枚举

    三、增删改查标准化写法

    1、controller 层级

    /**
     * @author tangn
     * @date 2021/1/9 9:46
     */
    @RestController
    @RequestMapping("/device")
    public class DeviceController {
    
        @Resource
        private DeviceService deviceService;
    
    
        /**
         * 创建设备
         *
         * @param deviceInfo 设备信息
         * @param result     校验结果
         * @return SimpleMessage
         */
        @RequestMapping("/createDevice")
        public SimpleMessage createDevice(@Validated({Insert.class}) DeviceInfo deviceInfo,
                                          BindingResult result) throws Exception {
            if (result.hasErrors()) {
                return new SimpleMessage(ErrorCodeEnum.INVALID_PARAMS, result.getAllErrors().get(0).getDefaultMessage());
            }
            return deviceService.createDevice(deviceInfo);
        }
    
        /**
         * 获取设备列表
         *
         * @param deviceInfoDTO 查询参数
         * @param result        校验结果
         * @return List<DeviceInfoVO>
         */
        @RequestMapping("/getDeviceList")
        public Page<DeviceInfoVO> getDeviceList(@Validated({PageQuery.class}) DeviceInfoDTO deviceInfoDTO,
                                                BindingResult result) {
            if (result.hasErrors()) {
                throw new ErrorCodeException(ErrorCodeEnum.INVALID_PARAMS, result.getAllErrors().get(0).getDefaultMessage());
            }
            return new Page<>(deviceService.getDeviceList(deviceInfoDTO));
        }
    
        /**
         * 更新设备信息
         *
         * @param deviceInfo 设备信息
         * @param result     校验结果
         * @return SimpleMessage
         */
        @RequestMapping("/updateDevice")
        public SimpleMessage updateDevice(@Validated({Update.class}) DeviceInfo deviceInfo,
                                          BindingResult result) throws Exception {
            if (result.hasErrors()) {
             return new SimpleMessage(ErrorCodeEnum.INVALID_PARAMS, result.getAllErrors().get(0).getDefaultMessage());
            }
            return deviceService.updateDevice(deviceInfo);
        }
    
        /**
         * 删除设备信息
         *
         * @param deviceId 设备ID
         * @return SimpleMessage
         */
        @RequestMapping("/delDevice")
        public SimpleMessage updateDevice(Integer deviceId) {
            if (Objects.isNull(deviceId)) {
                return new SimpleMessage(ErrorCodeEnum.INVALID_PARAMS);
            }
            return deviceService.delDevice(deviceId);
        }
    
    }
    
    • 讲解点

    1、头部类的 @RequestMapping
    2、@Validated({Update.class}) 根据接口标识类型进行参数校验的规范
    3、return 和 throw 的用法
    4、controller层的使用规范
    5、分页查询的使用方法(搭配DTO、VO 使用)
    6、注释的使用
    7、@Resource 和 @Autowired 的区别(自行理解)(https://blog.csdn.net/magi1201/article/details/82590106)

    • 注意点

    【强制】基本参数校验必须使用@Validated校验方式
    【强制】必须用对return 和 throw ,不允许满足return情况的使用throw
    【强制】注释必须完善,不允许图省事不写参数的意义
    【强制】controller 层级不允许出现 Dao层的注入,只能注入 Service 层
    【强制】无特殊用途,必须使用@Resuorce注解,不能使用@Autowired注解
    【建议】操作类型的返回,使用SimpleMessage

    2、service 层级

    /**
     * @author tangn
     * @date 2021/1/9 9:47
     */
    public interface DeviceService {
    
        /**
         * 创建设备
         *
         * @param deviceInfo 设备信息
         * @return SimpleMessage
         * @throws Exception
         */
        SimpleMessage createDevice(DeviceInfo deviceInfo) throws Exception;
    
        /**
         * 获取设备列表
         *
         * @param deviceInfoDTO 查询参数
         * @return List<DeviceInfoVO>
         */
        List<DeviceInfoVO> getDeviceList(DeviceInfoDTO deviceInfoDTO);
    
        /**
         * 更新设备信息
         *
         * @param deviceInfo 设备信息
         * @return SimpleMessage
         * @throws Exception
         */
        SimpleMessage updateDevice(DeviceInfo deviceInfo) throws Exception;
    
        /**
         * 删除设备
         *
         * @param deviceId 设备ID
         * @return SimpleMessage
         * @throws Exception
         */
        SimpleMessage delDevice(Integer deviceId);
    
    }
    
    • 讲解点

    为什么要通过接口形式进行service层编写?

    2、impl 层级

    /**
     * @author tangn
     * @date 2021/1/9 9:48
     */
    @Service
    @Slf4j
    public class DeviceServiceImpl implements DeviceService {
    
        @Resource
        private CategoryDao categoryDao;
        @Resource
        private DeviceDao deviceDao;
        @Resource
        private StoreDevicesDao storeDevicesDao;
        @Resource
        private AdminService adminService;
    
        /**
         * 创建设备
         *
         * @param deviceInfo 设备信息
         * @return SimpleMessage
         */
        @Override
        public SimpleMessage createDevice(DeviceInfo deviceInfo) throws Exception {
            AccountVO currentAccount = adminService.getCurrentAdmin();
            // 二级分类检测
            if (categoryDao.checkSecondaryCategoryExistById(deviceInfo.getParentId()) == 0) {
                return new SimpleMessage(ErrorCodeEnum.NO, "查询不到二级分类");
            }
            // 设置账户
            deviceInfo.setCreateNo(currentAccount.getPhoneNo());
            // 创建商品
            deviceDao.createDeviceInfo(deviceInfo);
            return new SimpleMessage(ErrorCodeEnum.OK, "创建设备成功");
        }
    
        /**
         * 获取设备列表
         *
         * @param deviceInfoDTO 查询参数
         * @return List<DeviceInfo>
         */
        @Override
        public List<DeviceInfoVO> getDeviceList(DeviceInfoDTO deviceInfoDTO) {
            PageHelper.startPage(deviceInfoDTO.getPageNumber(), deviceInfoDTO.getPageSize());
            return deviceDao.getDeviceList(deviceInfoDTO);
        }
    
        /**
         * 更新设备信息
         *
         * @param deviceInfo 设备信息
         * @return SimpleMessage
         */
        @Override
        public SimpleMessage updateDevice(DeviceInfo deviceInfo) throws Exception {
            AccountVO currentAccount = adminService.getCurrentAdmin();
            // 设备检测
            if (deviceDao.checkDeviceById(deviceInfo.getId()) == 0) {
                return new SimpleMessage(ErrorCodeEnum.NO, "查询不到该设备");
            }
            // 二级分类检测
            if (categoryDao.checkSecondaryCategoryExistById(deviceInfo.getParentId()) == 0) {
                return new SimpleMessage(ErrorCodeEnum.NO, "查询不到二级分类");
            }
            deviceInfo.setUpdateNo(currentAccount.getPhoneNo());
            // 更新设备
            deviceDao.updateDeviceInfo(deviceInfo);
            return new SimpleMessage(ErrorCodeEnum.OK, "更新成功");
        }
    
        /**
         * 删除设备
         *
         * @param deviceId 设备ID
         * @return SimpleMessage
         */
        @Override
        public SimpleMessage delDevice(Integer deviceId) {
            // 设备检测
            if (deviceDao.checkDeviceById(deviceId) == 0) {
                return new SimpleMessage(ErrorCodeEnum.NO, "查询不到该设备");
            }
            // 检查设备是否被门店绑定
            if (storeDevicesDao.checkDeviceBinded(deviceId) > 0) {
                return new SimpleMessage(ErrorCodeEnum.NO, "该设备已经被门店绑定,请先移除");
            }
            deviceDao.delDeviceInfo(deviceId);
            return new SimpleMessage(ErrorCodeEnum.OK, "删除成功");
        }
    }
    
    • 讲解点

    1、@Slf4j 日志打印注解
    2、数据是否存在校验(count)
    3、PageHelper 使用
    4、注释的使用

    • 注意点

    【强制】参数的真实性校验一定要做,不要相信任何传过来的值
    【强制】编写每句代码时,操作每个数据时,一定要确定该值是否为空,禁止出现空指针异常
    【强制】return 和 throw 一定要使用正确(再次强调!!)
    【强制】数据库返回的值,一定要做期望值校验(后面会讲解期望值校验注解)
    【强制】方法中定义的变量要使用非包装类型接受,(例:使用int 不用 Integer )

    3、dao层级

    /**
     * @author tangn
     * @date 2021/1/19 17:00
     */
    @Mapper
    public interface DeviceDao {
    
        /**
         * 根据二级分类ID检查设备是否存在
         *
         * @param parentId 分类ID
         * @return int
         */
        @Select("SELECT COUNT(*) FROM device_info WHERE parent_id = #{parentId}")
        int checkDeviceByParentId(@Param("parentId") Integer parentId);
    
        /**
         * 检查设备信息
         *
         * @param id 设备ID
         * @return int
         */
        @Select("SELECT COUNT(*) FROM device_info WHERE id = #{id}")
        int checkDeviceById(@Param("id") Integer id);
    
        /**
         * 创建设备信息
         *
         * @param deviceInfo 设备信息
         * @return int
         */
        @Insert("INSERT INTO device_info (" +
                "device_name," +
                "device_code," +
                "parent_id," +
                "icon_url," +
                "sort," +
                "create_time," +
                "create_no) VALUES (" +
                "#{dto.deviceName}," +
                "#{dto.deviceCode}," +
                "#{dto.parentId}," +
                "#{dto.iconUrl}," +
                "#{dto.sort}," +
                "now()," +
                "#{dto.createNo})")
        @ReturnCheck(info = "创建设备信息失败")
        int createDeviceInfo(@Param("dto") DeviceInfo deviceInfo);
    
        /**
         * 获取设备信息
         *
         * @param deviceInfoDTO 查询参数
         * @return List<DeviceInfoVO>
         */
        @SelectProvider(type = DeviceDaoProvider.class, method = "getDeviceList")
        List<DeviceInfoVO> getDeviceList(@Param("dto") DeviceInfoDTO deviceInfoDTO);
    
        /**
         * 更新设备信息
         *
         * @param deviceInfo 设备信息
         * @return int
         */
        @Update("UPDATE device_info set " +
                "device_name = #{dto.deviceName}," +
                "device_code = #{dto.deviceCode}," +
                "parent_id = #{dto.parentId}," +
                "icon_url = #{dto.iconUrl}," +
                "sort = #{dto.sort}," +
                "update_no = #{dto.updateNo}," +
                "update_time = now() " +
                "WHERE id = #{dto.id} ")
        @ReturnCheck(info = "更新设备信息失败")
        int updateDeviceInfo(@Param("dto") DeviceInfo deviceInfo);
    
        /**
         * 删除设备信息
         *
         * @param id 设备ID
         * @return int
         */
        @Delete("DELETE FROM device_info WHERE id = #{id}")
        @ReturnCheck(info = "删除设备失败")
        int delDeviceInfo(@Param("id") Integer id);
    
        /**
         * 获取设备信息
         *
         * @param id 设备ID
         * @return DeviceInfo
         */
        @Select("SELECT id,device_name FROM device_info WHERE id = #{id}")
        DeviceInfo getDeviceInfo(@Param("id") Integer id);
    
    }
    
    • 讲解点

    1.使用count(*)校验数据真实性
    2.@ReturnCheck 注解使用
    3.@Select 、@Update 、@Delete 的使用(尤其注意@Update 和 @Delete )
    3.@SelectProvider 使用 type 和 method
    4.利用映射转VO
    5.无特殊需求返回值无须添加 as (mybatis配置)

    • 注意点

    【强制】插入及更新时不能忘记创建时间(人)、更新时间(人)
    【强制】无特殊需求,不允许创建时插入更新时间
    【强制】影响行数必须使用非包装类型接受(使用 int 而非 Integer )
    【建议】更新时,更新依据字段尽可能落在 主键或唯一索引上
    【建议】增加@ReturnCheck 注解 和 @ReturnListCheck 注解减少serivce层判断

    3、provider层级

    /**
     * @author tangn
     * @date 2021/1/20 17:29
     */
    public class DeviceDaoProvider {
    
        /**
         * 获取设备列表
         *
         * @param map 查询参数
         * @return String
         */
        public String getDeviceList(HashMap<String, DeviceInfoDTO> map) {
            StringBuilder sql = new StringBuilder();
            DeviceInfoDTO deviceInfoDTO = map.get("dto");
            sql.append("SELECT " +
                    " a.id," +
                    " a.device_name," +
                    " a.device_code," +
                    " a.parent_id," +
                    " a.icon_url," +
                    " a.sort," +
                    " d.name as 'createNo'," +
                    " e.name as 'updateNo'," +
                    " a.create_time," +
                    " a.update_time, " +
                    " b.category_name AS 'secondaryCategoryName', " +
                    " c.category_name AS 'primaryCategoryName'  " +
                    "FROM " +
                    " `device_info` a " +
                    " JOIN device_secondary_category b ON a.parent_id = b.id " +
                    " JOIN device_primary_category c ON b.parent_id = c.id  " +
                    " LEFT JOIN shiro_account d ON a.create_no = d.phone_no and d.plat_type = 0 " +
                    " LEFT JOIN shiro_account e ON a.update_no = e.phone_no and e.plat_type = 0  " +
                    "WHERE " +
                    " 1 = 1 ");
            // 设备名称
            if (StringUtils.isNotBlank(deviceInfoDTO.getDeviceName())) {
                sql.append(" AND INSTR(a.device_name,#{dto.deviceName}) > 0 ");
            }
            // 二级分类
            if (Objects.nonNull(deviceInfoDTO.getParentId())) {
                sql.append(" AND a.parent_id = #{dto.parentId} ");
            }
            sql.append(" ORDER BY a.create_time DESC ");
            return sql.toString();
        }
    
    }
    
    • 讲解点

    1.DTO 内取值的好处
    2.WHERE 1 = 1 的妙处
    3.使用工具类进行参数值的判断
    4.使用 INSTR 进行模糊查询

    • 规范点

    【强制】使用工具类对参数进行判别
    【建议】使用DTO 作为参数的携带体

    四、来点干的?

    1、事务的使用

        /**
         * 确认发放
         *
         * @param shopOrderDTO 参数
         * @return : com.orangeconvenient.common.entity.MessageBean<java.time.LocalDateTime>
         */
        @Transactional(rollbackFor = Exception.class)
        @Override
        public MessageBean<String> confirmRelease(ShopOrderDTO shopOrderDTO) {
            ShopOrder shopOrder = Optional.ofNullable(storeOrderDao.getByIdAndStore(shopOrderDTO.getId(), shopOrderDTO.getStoreNo()))
                    .orElseThrow(() -> new ErrorCodeException(ErrorCodeEnum.NO, "订单不存在"));
            if (ShopOrderStatusEnum.RECEIVED.equals(shopOrder.getOrderStatus())) {
                return new MessageBean<>(ErrorCodeEnum.EXT_ASSEMBLE_ORDER_CONSUMED, Objects.nonNull(shopOrder.getSuccessTime()) ? LocalDateTimeUtil.formatDateTime(shopOrder.getSuccessTime()) : null, "此订单顾客已取货,请不要重复发放!");
            }
            if (ShopOrderStatusEnum.MEMBER_CANCEL.equals(shopOrder.getOrderStatus()) ||
                    ShopOrderStatusEnum.STORE_CANCEL.equals(shopOrder.getOrderStatus()) ||
                    ShopOrderStatusEnum.RAISE_OVER_CANCEL.equals(shopOrder.getOrderStatus())) {
                return new MessageBean<>(ErrorCodeEnum.NO, "此订单已被取消,请不要发放商品!");
            }
            if (!ShopOrderStatusEnum.PENDING.equals(shopOrder.getOrderStatus())) {
                return new MessageBean<>(ErrorCodeEnum.NO, "订单状态不正确");
            }
            shopOrder.setSuccessTime(LocalDateTime.now());
            // 设置出库商品的数量
            shopOrder.setOutGoodsNum(orderDao.getGoodsNumByOrderNo(shopOrder.getOrderNo(), ShopOrderInfoStatusEnum.PENDING));
            orderDao.updateOrderStatus(shopOrder, ShopOrderStatusEnum.RECEIVED);
            List<ShopOrderInfoVO> shopOrderInfoList = orderDao.getCanRefundOrderInfoList(shopOrder.getId(), ShopOrderInfoStatusEnum.PENDING);
            if (CollectionUtils.isEmpty(shopOrderInfoList)) {
                return new MessageBean<>(ErrorCodeEnum.NO, "没有待自提的订单明细");
            }
            if (orderDao.updateInfoStatusByOrderId(shopOrder.getId(), ShopOrderInfoStatusEnum.RECEIVED, ShopOrderInfoStatusEnum.PENDING, null) != shopOrderInfoList.size()) {
                throw new ErrorCodeException(ErrorCodeEnum.NO, "订单明细状态修改失败");
            }
            // 更新实际出库的订单详情
            storeOrderDao.updateInfoOutStatusByIds(shopOrderInfoList.size(), shopOrderInfoList.stream().map(ShopOrderInfo::getId).collect(Collectors.toList()), true);
            User user = Optional.ofNullable(userDao.queryUserById(shopOrder.getUserId())).orElseThrow(() -> new ErrorCodeException(ErrorCodeEnum.NO, "会员不存在"));
            //初始化评价
            memberCardOrderDao.insertEvaluateRecord(EvaluateRecord.builder()
                    .orderNo(shopOrder.getOrderNo())
                    .unionId(user.getUnionId())
                    .userId(shopOrder.getUserId())
                    .consumeTime(shopOrder.getSuccessTime())
                    .merchantNo(shopOrder.getStoreNo())
                    .totalAmount(shopOrderInfoList.stream().map(ShopOrderInfo::getActualPrice).reduce(BigDecimal.ZERO, BigDecimal::add))
                    .evaluateRecordType(EvaluateRecordTypeEnum.MALL_ORDER).build());
            // 获取门店信息
            Store store = storeDao.getByStoreNo(shopOrder.getStoreNo());
            //初始化售后信息
            ShopOrderAfterSale shopOrderAfterSale = ShopOrderAfterSale.builder()
                    .orderId(shopOrder.getId())
                    .userId(shopOrder.getUserId())
                    .afterSaleOrderNo(Constant.SHOP_ORDER_AFTER_SALE_PREFIX + CheckUtil.fillZero(user.getId().longValue(), 5) + System.nanoTime()).build();
            afterSaleOrderDao.insertAfterSaleOrder(shopOrderAfterSale);
            // 初始化模板消息备注信息
            StringBuilder remark = new StringBuilder(200);
            givenUserIntegral(user, shopOrderInfoList, shopOrder.getStoreNo(), shopOrder.getOrderNo(), shopOrder, remark);
            // 消费返券
            consumeReturnCoupon(shopOrder.getId(), user, remark, shopOrderDTO.getStoreNo());
            vitalityChangeService.doVitality(shopOrder.getUserId(), ShopCouponWeeklyContentTypeEnum.SHOP_CONSUME, null);
            //发送新版评价
            accessTokenComponent.sendCommonEvaluateMessage(user.getOpenId(),
                    user.getUnionId(), shopOrder.getOrderNo(), Objects.nonNull(store) ? store.getBusinessName() : "",
                    LocalDateTime.now(), remark);
            return new MessageBean<>(ErrorCodeEnum.OK, "取货成功,请将商品发放给顾客");
        }
    
    • 讲解点

    1.事务的使用场景
    2.为什么我的事务不生效?
    3.@Builder使用

    • 注意点

    【强制】不要使用编程式事务
    【建议】开启事务后,要考虑在合适的时机抛出事务,让其回滚
    【建议】多层事务嵌套,要考虑回滚情况

    2、for update 使用(悲观锁)

        /**
         * 获取最后一条会员积分记录
         *
         * @param userId 会员id
         * @return 积分
         */
        @Select("select integral from tbl_user_integral where user_id=#{userId} for update")
        @Options(timeout = 3)
        Long getLastIntegralByUserId(@Param("userId") Long userId);
    
    • 讲解点

    当涉及到金额、库存等需要保持一致性的操作时,可采用悲观锁进行相应的查询并执行更新

    • 注意点

    @Options(timeout = 3) 一定要加,且不能锁数据时间过长

    3、update 更新状态

        /**
         * 更新闲鱼订单状态
         * @param outOrderStatus 订单状态
         * @param orderId 订单ID
         * @param oldStatus 旧状态
         * @return int
         */
        @Update("UPDATE xy_receive_order SET " +
                "out_order_status = #{outOrderStatus}," +
                "update_time = now() " +
                "WHERE id = #{orderId} AND " +
                "out_order_status = #{oldStatus} ")
        @ReturnCheck(info = "更新闲鱼订单状态失败")
        int updateOutOrderStatus(@Param("outOrderStatus") Integer outOrderStatus,
                                 @Param("orderId") Long orderId,
                                 @Param("oldStatus") Integer oldStatus);
    
    • 讲解点

    当对状态相关数据进行更新时,需要知道数据的原状态

    • 注意点

    【强制】 更新状态时,必须限定原状态

    4、update 更新数值

        /**
         * 扣款
         *
         * @param id          代金券接收ID
         * @param orderAmount 订单金额
         * @return int
         */
        @Update("UPDATE cash_voucher_receiver  " +
                "SET left_amount = ( left_amount - #{orderAmount} ), " +
                "update_time = now( ) " +
                "WHERE " +
                " id = #{id}  " +
                " AND (left_amount - #{orderAmount}) >= 0 ")
        @ReturnCheck(info = "核销失败")
        int consumeVoucher(@Param("id") Long id, @Param("orderAmount") BigDecimal orderAmount);
    
    • 讲解点
    1. WHERE 前使用数据库增减余额(库存)
      2.WHERE 后使用计算结果>0 进行校验,防止多扣(允许负库存例外)
    • 注意点

    【强制】禁止在service层级计算好结果直接更新到库

    5、批量操作 in 的使用方法

        /**
         * 批量插入用户信息
         *
         * @param userInfoList 用户信息列表
         * @return 影响的行数
         */
        @Insert("<script>" +
                " insert into tbl_user_info " +
                " (user_id, recent_consume_date, store_consume_time, " +
                " shop_consume_time, assemble_consume_time, " +
                " store_consume_date_collect, shop_consume_date_collect, " +
                " assemble_consume_date_collect, create_time) " +
                " values " +
                "  <foreach collection=\"list\" index=\"index\" item=\"info\" open=\"\" separator=\",\" close=\"\"> " +
                "  ( " +
                "      #{info.userId}, " +
                "      #{info.recentConsumeDate}, " +
                "      #{info.storeConsumeTime}, " +
                "      #{info.shopConsumeTime}, " +
                "      #{info.assembleConsumeTime}, " +
                "      #{info.storeConsumeDateCollect}, " +
                "      #{info.shopConsumeDateCollect}, " +
                "      #{info.assembleConsumeDateCollect}, " +
                "      NOW() " +
                "    )" +
                "  </foreach>" +
                "</script>")
        int insertUserInfo(@Param("list") List<UserInfo> userInfoList);
    
    • 讲解点

    1.<script> 标签 替代 xml 文件
    2.<foreach> 标签的使用
    3.使用批量插入的好处
    4.使用批量插入的风险点

    • 注意点

    【强制】在使用in进行动态sql拼接时,一定要考虑应用场景插入的条数,必要时,需要在sevice层做分割。

    相关文章

      网友评论

        本文标题:SpringBoot项目标准化代码编写

        本文链接:https://www.haomeiwen.com/subject/acfdlltx.html