feat(order): 添加送水订单配送时间和完整下单流程
- 在GltTicketOrder实体中新增sendTime字段用于记录配送时间 - 移除送水订单查询接口的权限验证要求,开放查询功能 - 实现完整的下单流程:验证登录用户、扣减水票、写入核销记录、创建订单 - 新增createWithWriteOff方法处理事务性下单操作,确保数据一致性 - 添加数据库行锁机制防止并发扣减问题 - 优化水票相关接口描述,明确为可用水票总数 - 移除水票日志添加接口的权限验证和操作日志注解
This commit is contained in:
@@ -5,6 +5,7 @@ import com.gxwebsoft.common.core.web.ApiResult;
|
|||||||
import com.gxwebsoft.common.core.web.BaseController;
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
import com.gxwebsoft.common.core.web.BatchParam;
|
import com.gxwebsoft.common.core.web.BatchParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
@@ -29,7 +30,6 @@ public class GltTicketOrderController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private GltTicketOrderService gltTicketOrderService;
|
private GltTicketOrderService gltTicketOrderService;
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:list')")
|
|
||||||
@Operation(summary = "分页查询送水订单")
|
@Operation(summary = "分页查询送水订单")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) {
|
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) {
|
||||||
@@ -37,7 +37,6 @@ public class GltTicketOrderController extends BaseController {
|
|||||||
return success(gltTicketOrderService.pageRel(param));
|
return success(gltTicketOrderService.pageRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:list')")
|
|
||||||
@Operation(summary = "查询全部送水订单")
|
@Operation(summary = "查询全部送水订单")
|
||||||
@GetMapping()
|
@GetMapping()
|
||||||
public ApiResult<List<GltTicketOrder>> list(GltTicketOrderParam param) {
|
public ApiResult<List<GltTicketOrder>> list(GltTicketOrderParam param) {
|
||||||
@@ -45,7 +44,6 @@ public class GltTicketOrderController extends BaseController {
|
|||||||
return success(gltTicketOrderService.listRel(param));
|
return success(gltTicketOrderService.listRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:list')")
|
|
||||||
@Operation(summary = "根据id查询送水订单")
|
@Operation(summary = "根据id查询送水订单")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ApiResult<GltTicketOrder> get(@PathVariable("id") Integer id) {
|
public ApiResult<GltTicketOrder> get(@PathVariable("id") Integer id) {
|
||||||
@@ -53,20 +51,16 @@ public class GltTicketOrderController extends BaseController {
|
|||||||
return success(gltTicketOrderService.getByIdRel(id));
|
return success(gltTicketOrderService.getByIdRel(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:save')")
|
|
||||||
@OperationLog
|
|
||||||
@Operation(summary = "添加送水订单")
|
@Operation(summary = "添加送水订单")
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public ApiResult<?> save(@RequestBody GltTicketOrder gltTicketOrder) {
|
public ApiResult<?> save(@RequestBody GltTicketOrder gltTicketOrder) {
|
||||||
// 记录当前登录用户id
|
// 下单:后端原子完成(扣水票 + 写核销记录 + 生成订单)
|
||||||
// User loginUser = getLoginUser();
|
User loginUser = getLoginUser();
|
||||||
// if (loginUser != null) {
|
if (loginUser == null) {
|
||||||
// gltTicketOrder.setUserId(loginUser.getUserId());
|
return fail("请先登录");
|
||||||
// }
|
|
||||||
if (gltTicketOrderService.save(gltTicketOrder)) {
|
|
||||||
return success("添加成功");
|
|
||||||
}
|
}
|
||||||
return fail("添加失败");
|
gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId());
|
||||||
|
return success("下单成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
|
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
|
||||||
|
|||||||
@@ -38,17 +38,23 @@ public class GltUserTicketController extends BaseController {
|
|||||||
return success(gltUserTicketService.pageRel(param));
|
return success(gltUserTicketService.pageRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "我的水票总数")
|
@Operation(summary = "可用水票总数")
|
||||||
@GetMapping("/my-total")
|
@GetMapping("/my-total")
|
||||||
public ApiResult<?> myTotal() {
|
public ApiResult<?> myTotal() {
|
||||||
Integer userId = getLoginUserId();
|
Integer userId = getLoginUserId();
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return fail("未登录");
|
return fail("未登录");
|
||||||
}
|
}
|
||||||
Integer totalQty = gltUserTicketService.sumTotalQtyByUserId(userId);
|
Integer tenantId = getTenantId();
|
||||||
|
if (tenantId == null) {
|
||||||
|
return fail("租户信息缺失");
|
||||||
|
}
|
||||||
|
Integer availableQty = gltUserTicketService.sumAvailableQtyByUserId(userId, tenantId);
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("userId", userId);
|
data.put("userId", userId);
|
||||||
data.put("totalQty", totalQty);
|
// 兼容旧字段:totalQty 表示“可用水票总数”
|
||||||
|
data.put("totalQty", availableQty);
|
||||||
|
data.put("availableQty", availableQty);
|
||||||
return success(data);
|
return success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ public class GltUserTicketLogController extends BaseController {
|
|||||||
return success(gltUserTicketLogService.getByIdRel(id));
|
return success(gltUserTicketLogService.getByIdRel(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('glt:gltUserTicketLog:save')")
|
|
||||||
@OperationLog
|
|
||||||
@Operation(summary = "添加消费日志")
|
@Operation(summary = "添加消费日志")
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public ApiResult<?> save(@RequestBody GltUserTicketLog gltUserTicketLog) {
|
public ApiResult<?> save(@RequestBody GltUserTicketLog gltUserTicketLog) {
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ public class GltTicketOrder implements Serializable {
|
|||||||
@Schema(description = "购买数量")
|
@Schema(description = "购买数量")
|
||||||
private Integer totalNum;
|
private Integer totalNum;
|
||||||
|
|
||||||
|
@Schema(description = "配送时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private String sendTime;
|
||||||
|
|
||||||
@Schema(description = "用户ID")
|
@Schema(description = "用户ID")
|
||||||
private Integer userId;
|
private Integer userId;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.gxwebsoft.glt.entity.GltUserTicket;
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
import com.gxwebsoft.glt.param.GltUserTicketParam;
|
import com.gxwebsoft.glt.param.GltUserTicketParam;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -35,11 +36,31 @@ public interface GltUserTicketMapper extends BaseMapper<GltUserTicket> {
|
|||||||
List<GltUserTicket> selectListRel(@Param("param") GltUserTicketParam param);
|
List<GltUserTicket> selectListRel(@Param("param") GltUserTicketParam param);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计用户水票总数量(sum(total_qty))
|
* 统计用户可用水票总数(sum(available_qty))
|
||||||
*
|
*
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
* @return 总数量
|
* @param tenantId 租户ID
|
||||||
|
* @return 可用总数
|
||||||
*/
|
*/
|
||||||
Integer sumTotalQtyByUserId(@Param("userId") Integer userId);
|
Integer sumAvailableQtyByUserId(@Param("userId") Integer userId,
|
||||||
|
@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按当前用户锁定水票记录(用于扣减/核销的事务场景)
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM glt_user_ticket
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND user_id = #{userId}
|
||||||
|
AND tenant_id = #{tenantId}
|
||||||
|
AND status = 0
|
||||||
|
AND deleted = 0
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE
|
||||||
|
""")
|
||||||
|
GltUserTicket selectByIdForUpdate(@Param("id") Integer id,
|
||||||
|
@Param("userId") Integer userId,
|
||||||
|
@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,14 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
|
|||||||
*/
|
*/
|
||||||
GltTicketOrder getByIdRel(Integer id);
|
GltTicketOrder getByIdRel(Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下单(事务):校验水票 -> 扣减水票 -> 写核销记录 -> 创建送水订单。
|
||||||
|
*
|
||||||
|
* @param gltTicketOrder 订单请求体
|
||||||
|
* @param userId 当前登录用户ID
|
||||||
|
* @param tenantId 当前租户ID
|
||||||
|
* @return 创建后的订单(含id)
|
||||||
|
*/
|
||||||
|
GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
package com.gxwebsoft.glt.service.impl;
|
package com.gxwebsoft.glt.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketLog;
|
||||||
import com.gxwebsoft.glt.mapper.GltTicketOrderMapper;
|
import com.gxwebsoft.glt.mapper.GltTicketOrderMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
|
||||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketLogService;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +30,17 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
|
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
|
||||||
|
|
||||||
|
public static final int CHANGE_TYPE_WRITE_OFF = 20;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketMapper gltUserTicketMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketService gltUserTicketService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketLogService gltUserTicketLogService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
|
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
|
||||||
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
|
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
|
||||||
@@ -44,4 +65,98 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
|||||||
return param.getOne(baseMapper.selectListRel(param));
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId) {
|
||||||
|
if (gltTicketOrder == null) {
|
||||||
|
throw new BusinessException("订单参数不能为空");
|
||||||
|
}
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException("请先登录");
|
||||||
|
}
|
||||||
|
Integer userTicketId = gltTicketOrder.getUserTicketId();
|
||||||
|
if (userTicketId == null) {
|
||||||
|
throw new BusinessException("userTicketId不能为空");
|
||||||
|
}
|
||||||
|
int totalNum = gltTicketOrder.getTotalNum() == null ? 0 : gltTicketOrder.getTotalNum();
|
||||||
|
if (totalNum <= 0) {
|
||||||
|
throw new BusinessException("totalNum必须大于0");
|
||||||
|
}
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("租户信息缺失");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 校验水票归属当前用户 + 正常状态,并锁定记录,避免并发扣减导致日志不准确
|
||||||
|
GltUserTicket userTicket = gltUserTicketMapper.selectByIdForUpdate(userTicketId, userId, tenantId);
|
||||||
|
if (userTicket == null) {
|
||||||
|
throw new BusinessException("水票不存在、已失效或无权限");
|
||||||
|
}
|
||||||
|
int availableQty = userTicket.getAvailableQty() == null ? 0 : userTicket.getAvailableQty();
|
||||||
|
int usedQty = userTicket.getUsedQty() == null ? 0 : userTicket.getUsedQty();
|
||||||
|
if (availableQty < totalNum) {
|
||||||
|
throw new BusinessException("水票数量不足");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 更新 glt_user_ticket: availableQty -= totalNum, usedQty += totalNum
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int availableAfter = availableQty - totalNum;
|
||||||
|
int usedAfter = usedQty + totalNum;
|
||||||
|
userTicket.setAvailableQty(availableAfter);
|
||||||
|
userTicket.setUsedQty(usedAfter);
|
||||||
|
userTicket.setUpdateTime(now);
|
||||||
|
if (!gltUserTicketService.updateById(userTicket)) {
|
||||||
|
throw new BusinessException("扣减水票失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 插入 glt_ticket_order(storeId/addressId/totalNum/buyerRemarks…)
|
||||||
|
gltTicketOrder.setUserId(userId);
|
||||||
|
// 订单基础字段由后端兜底,避免前端误传/恶意传参
|
||||||
|
gltTicketOrder.setStatus(0);
|
||||||
|
gltTicketOrder.setDeleted(0);
|
||||||
|
gltTicketOrder.setTenantId(tenantId);
|
||||||
|
if (gltTicketOrder.getSortNumber() == null) {
|
||||||
|
gltTicketOrder.setSortNumber(0);
|
||||||
|
}
|
||||||
|
if (gltTicketOrder.getCreateTime() == null) {
|
||||||
|
gltTicketOrder.setCreateTime(now);
|
||||||
|
}
|
||||||
|
gltTicketOrder.setUpdateTime(now);
|
||||||
|
if (!this.save(gltTicketOrder)) {
|
||||||
|
throw new BusinessException("创建订单失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 插入 glt_user_ticket_log(核销记录)
|
||||||
|
GltUserTicketLog log = new GltUserTicketLog();
|
||||||
|
log.setUserTicketId(userTicketId);
|
||||||
|
log.setChangeType(CHANGE_TYPE_WRITE_OFF);
|
||||||
|
log.setChangeAvailable(-totalNum);
|
||||||
|
log.setChangeFrozen(0);
|
||||||
|
log.setChangeUsed(totalNum);
|
||||||
|
log.setAvailableAfter(availableAfter);
|
||||||
|
log.setFrozenAfter(userTicket.getFrozenQty() == null ? 0 : userTicket.getFrozenQty());
|
||||||
|
log.setUsedAfter(usedAfter);
|
||||||
|
log.setOrderId(gltTicketOrder.getId());
|
||||||
|
log.setOrderNo(gltTicketOrder.getId() == null ? null : String.valueOf(gltTicketOrder.getId()));
|
||||||
|
log.setUserId(userId);
|
||||||
|
log.setSortNumber(0);
|
||||||
|
String comments = gltTicketOrder.getComments();
|
||||||
|
if (StrUtil.isBlank(comments)) {
|
||||||
|
comments = gltTicketOrder.getBuyerRemarks();
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(comments)) {
|
||||||
|
comments = "水票下单核销";
|
||||||
|
}
|
||||||
|
log.setComments(comments);
|
||||||
|
log.setStatus(0);
|
||||||
|
log.setDeleted(0);
|
||||||
|
log.setTenantId(tenantId);
|
||||||
|
log.setCreateTime(now);
|
||||||
|
log.setUpdateTime(now);
|
||||||
|
if (!gltUserTicketLogService.save(log)) {
|
||||||
|
throw new BusinessException("写入核销记录失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return gltTicketOrder;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user