feat(order): 添加送水订单配送时间和完整下单流程

- 在GltTicketOrder实体中新增sendTime字段用于记录配送时间
- 移除送水订单查询接口的权限验证要求,开放查询功能
- 实现完整的下单流程:验证登录用户、扣减水票、写入核销记录、创建订单
- 新增createWithWriteOff方法处理事务性下单操作,确保数据一致性
- 添加数据库行锁机制防止并发扣减问题
- 优化水票相关接口描述,明确为可用水票总数
- 移除水票日志添加接口的权限验证和操作日志注解
This commit is contained in:
2026-02-06 00:15:20 +08:00
parent 88afd149c3
commit 48cd2e1f7b
7 changed files with 169 additions and 21 deletions

View File

@@ -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')")

View File

@@ -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);
} }

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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_orderstoreId/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;
}
} }