SpringBoot项目标准化代码编写

一、认识项目结构

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层做分割。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容