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);
- 讲解点
- 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层做分割。