feat(tickets): 实现冻结水票自动释放功能
- 在 GltUserTicketMapper 中新增 releaseFrozenQty 方法用于释放冻结水票 - 新增 GltUserTicketReleaseMapper 处理释放记录的查询和状态更新 - 添加 H2 数据库依赖支持单元测试 - 创建 GltUserTicketAutoReleaseService 接口及其实现类 - 实现自动释放服务的核心逻辑包括加锁查询、数量更新和流水记录 - 新建定时任务 GltUserTicketAutoReleaseTask 定期执行释放操作 - 添加完整的单元测试覆盖正常释放、未到期和冻结不足等场景 - 创建测试专用数据库表结构文件
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -38,6 +38,11 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- spring-boot-web -->
|
<!-- spring-boot-web -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ 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.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,4 +65,28 @@ public interface GltUserTicketMapper extends BaseMapper<GltUserTicket> {
|
|||||||
@Param("userId") Integer userId,
|
@Param("userId") Integer userId,
|
||||||
@Param("tenantId") Integer tenantId);
|
@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放冻结水票(冻结 -> 可用;并累加已释放数量)
|
||||||
|
* <p>
|
||||||
|
* 返回值为受影响行数:1 表示释放成功;0 表示记录不存在/状态不符/冻结不足。
|
||||||
|
*/
|
||||||
|
@Update("""
|
||||||
|
UPDATE glt_user_ticket
|
||||||
|
SET available_qty = COALESCE(available_qty, 0) + #{qty},
|
||||||
|
frozen_qty = COALESCE(frozen_qty, 0) - #{qty},
|
||||||
|
released_qty = COALESCE(released_qty, 0) + #{qty},
|
||||||
|
update_time = #{now}
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND user_id = #{userId}
|
||||||
|
AND tenant_id = #{tenantId}
|
||||||
|
AND status = 0
|
||||||
|
AND deleted = 0
|
||||||
|
AND COALESCE(frozen_qty, 0) >= #{qty}
|
||||||
|
""")
|
||||||
|
int releaseFrozenQty(@Param("id") Integer id,
|
||||||
|
@Param("userId") Integer userId,
|
||||||
|
@Param("tenantId") Integer tenantId,
|
||||||
|
@Param("qty") Integer qty,
|
||||||
|
@Param("now") LocalDateTime now);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ 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.GltUserTicketRelease;
|
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
|
||||||
import com.gxwebsoft.glt.param.GltUserTicketReleaseParam;
|
import com.gxwebsoft.glt.param.GltUserTicketReleaseParam;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,4 +37,37 @@ public interface GltUserTicketReleaseMapper extends BaseMapper<GltUserTicketRele
|
|||||||
*/
|
*/
|
||||||
List<GltUserTicketRelease> selectListRel(@Param("param") GltUserTicketReleaseParam param);
|
List<GltUserTicketRelease> selectListRel(@Param("param") GltUserTicketReleaseParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询待释放且到期的记录(加行锁,防止多实例重复处理)
|
||||||
|
*
|
||||||
|
* status: 0=待释放, 1=已释放, 2=释放失败(数据异常)
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
SELECT *
|
||||||
|
FROM glt_user_ticket_release
|
||||||
|
WHERE status = 0
|
||||||
|
AND deleted = 0
|
||||||
|
AND release_time <= #{now}
|
||||||
|
ORDER BY release_time ASC, id ASC
|
||||||
|
LIMIT #{limit}
|
||||||
|
FOR UPDATE
|
||||||
|
""")
|
||||||
|
List<GltUserTicketRelease> selectDueForUpdate(@Param("now") LocalDateTime now,
|
||||||
|
@Param("limit") int limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新释放记录状态
|
||||||
|
*/
|
||||||
|
@Update("""
|
||||||
|
UPDATE glt_user_ticket_release
|
||||||
|
SET status = #{status},
|
||||||
|
update_time = #{now}
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND deleted = 0
|
||||||
|
AND status = 0
|
||||||
|
""")
|
||||||
|
int updateStatus(@Param("id") Long id,
|
||||||
|
@Param("status") Integer status,
|
||||||
|
@Param("now") LocalDateTime now);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.gxwebsoft.glt.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结水票自动释放(到达 release_time 后执行:frozen -> available)。
|
||||||
|
*/
|
||||||
|
public interface GltUserTicketAutoReleaseService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放到期的冻结水票
|
||||||
|
*
|
||||||
|
* @param now 当前时间(用于测试/可控时钟)
|
||||||
|
* @param batchSize 单次处理上限
|
||||||
|
* @return 成功释放的条数
|
||||||
|
*/
|
||||||
|
int releaseDue(LocalDateTime now, int batchSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放到期的冻结水票(使用系统当前时间)
|
||||||
|
*/
|
||||||
|
default int releaseDue(int batchSize) {
|
||||||
|
return releaseDue(LocalDateTime.now(), batchSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.gxwebsoft.glt.service.impl;
|
||||||
|
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketLog;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketLogMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketReleaseMapper;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结水票自动释放实现:
|
||||||
|
* - 读取到期且待释放的 release 记录(FOR UPDATE 加锁,防止重复处理)
|
||||||
|
* - 释放成功:更新 user_ticket 数量 & 将 release.status 置为 1,并写入流水
|
||||||
|
* - 释放失败:将 release.status 置为 2(数据异常/冻结不足等)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoReleaseService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更类型:冻结释放
|
||||||
|
* <p>
|
||||||
|
* 现有发放类型为 10(见 GltTicketIssueService.CHANGE_TYPE_ISSUE),这里取 11。
|
||||||
|
*/
|
||||||
|
private static final int CHANGE_TYPE_RELEASE = 11;
|
||||||
|
|
||||||
|
/** release.status:待释放 */
|
||||||
|
private static final int RELEASE_STATUS_PENDING = 0;
|
||||||
|
/** release.status:已释放 */
|
||||||
|
private static final int RELEASE_STATUS_DONE = 1;
|
||||||
|
/** release.status:释放失败(数据异常/冻结不足等,需人工处理) */
|
||||||
|
private static final int RELEASE_STATUS_FAILED = 2;
|
||||||
|
|
||||||
|
private final GltUserTicketReleaseMapper releaseMapper;
|
||||||
|
private final GltUserTicketMapper userTicketMapper;
|
||||||
|
private final GltUserTicketLogMapper userTicketLogMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int releaseDue(LocalDateTime now, int batchSize) {
|
||||||
|
int limit = Math.max(batchSize, 1);
|
||||||
|
List<GltUserTicketRelease> dueList = releaseMapper.selectDueForUpdate(now, limit);
|
||||||
|
if (dueList == null || dueList.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int success = 0;
|
||||||
|
for (GltUserTicketRelease rel : dueList) {
|
||||||
|
if (rel.getId() == null || rel.getStatus() == null || rel.getStatus() != RELEASE_STATUS_PENDING) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer qtyObj = rel.getReleaseQty();
|
||||||
|
if (qtyObj == null || qtyObj <= 0) {
|
||||||
|
markFailed(rel.getId(), now, "releaseQty无效");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int qty = qtyObj;
|
||||||
|
|
||||||
|
if (rel.getUserTicketId() == null || rel.getUserId() == null || rel.getTenantId() == null) {
|
||||||
|
markFailed(rel.getId(), now, "缺少userTicketId/userId/tenantId");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long userTicketIdLong = rel.getUserTicketId();
|
||||||
|
if (userTicketIdLong > Integer.MAX_VALUE || userTicketIdLong < 1) {
|
||||||
|
markFailed(rel.getId(), now, "userTicketId超范围");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer userTicketId = (int) userTicketIdLong;
|
||||||
|
|
||||||
|
// 先释放冻结数量(条件更新,确保 frozen_qty >= qty)
|
||||||
|
int updated = userTicketMapper.releaseFrozenQty(
|
||||||
|
userTicketId,
|
||||||
|
rel.getUserId(),
|
||||||
|
rel.getTenantId(),
|
||||||
|
qty,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
if (updated <= 0) {
|
||||||
|
markFailed(rel.getId(), now, "冻结不足/水票不存在/状态不符");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入流水(可用 +qty,冻结 -qty)
|
||||||
|
GltUserTicket ticket = userTicketMapper.selectById(userTicketId);
|
||||||
|
GltUserTicketLog logRow = new GltUserTicketLog();
|
||||||
|
logRow.setUserTicketId(userTicketId);
|
||||||
|
logRow.setChangeType(CHANGE_TYPE_RELEASE);
|
||||||
|
logRow.setChangeAvailable(qty);
|
||||||
|
logRow.setChangeFrozen(-qty);
|
||||||
|
logRow.setChangeUsed(0);
|
||||||
|
if (ticket != null) {
|
||||||
|
logRow.setAvailableAfter(ticket.getAvailableQty());
|
||||||
|
logRow.setFrozenAfter(ticket.getFrozenQty());
|
||||||
|
logRow.setUsedAfter(ticket.getUsedQty());
|
||||||
|
}
|
||||||
|
logRow.setOrderId(null);
|
||||||
|
logRow.setOrderNo(null);
|
||||||
|
logRow.setUserId(rel.getUserId());
|
||||||
|
logRow.setSortNumber(0);
|
||||||
|
logRow.setComments("冻结水票到期释放");
|
||||||
|
logRow.setStatus(0);
|
||||||
|
logRow.setDeleted(0);
|
||||||
|
logRow.setTenantId(rel.getTenantId());
|
||||||
|
logRow.setCreateTime(now);
|
||||||
|
logRow.setUpdateTime(now);
|
||||||
|
userTicketLogMapper.insert(logRow);
|
||||||
|
|
||||||
|
// 标记释放记录已完成(放在最后:若流水失败则回滚)
|
||||||
|
releaseMapper.updateStatus(rel.getId(), RELEASE_STATUS_DONE, now);
|
||||||
|
|
||||||
|
success++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markFailed(Long releaseId, LocalDateTime now, String reason) {
|
||||||
|
releaseMapper.updateStatus(releaseId, RELEASE_STATUS_FAILED, now);
|
||||||
|
log.warn("冻结水票释放标记失败 - releaseId={}, reason={}", releaseId, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.gxwebsoft.glt.task;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结水票自动释放任务:
|
||||||
|
* - 扫描 glt_user_ticket_release 中到期且待释放(status=0)的记录
|
||||||
|
* - 释放成功:frozen -> available,并将 release.status 置为 1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(prefix = "glt.ticket.auto-release", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class GltUserTicketAutoReleaseTask {
|
||||||
|
|
||||||
|
private final GltUserTicketAutoReleaseService autoReleaseService;
|
||||||
|
|
||||||
|
@Value("${glt.ticket.auto-release.batch-size:200}")
|
||||||
|
private int batchSize;
|
||||||
|
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Scheduled(cron = "${glt.ticket.auto-release.cron:0 */1 * * * ?}")
|
||||||
|
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;释放记录自带 tenantId,更新时会校验 tenantId")
|
||||||
|
public void run() {
|
||||||
|
if (!running.compareAndSet(false, true)) {
|
||||||
|
log.warn("冻结水票自动释放任务仍在执行中,本轮跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int released = autoReleaseService.releaseDue(now, Math.max(batchSize, 1));
|
||||||
|
if (released > 0) {
|
||||||
|
log.info("冻结水票自动释放完成 - released={}, now={}", released, now);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.gxwebsoft.glt;
|
||||||
|
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketLogMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketReleaseMapper;
|
||||||
|
import com.gxwebsoft.glt.service.impl.GltUserTicketAutoReleaseServiceImpl;
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.SpringBootConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
@SpringBootConfiguration
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
@MapperScan(basePackageClasses = {
|
||||||
|
GltUserTicketMapper.class,
|
||||||
|
GltUserTicketReleaseMapper.class,
|
||||||
|
GltUserTicketLogMapper.class
|
||||||
|
})
|
||||||
|
@Import(GltUserTicketAutoReleaseServiceImpl.class)
|
||||||
|
public class AutoReleaseTestApplication {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package com.gxwebsoft.glt.service;
|
||||||
|
|
||||||
|
import com.gxwebsoft.glt.AutoReleaseTestApplication;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketLog;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketLogMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketReleaseMapper;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@SpringBootTest(
|
||||||
|
classes = AutoReleaseTestApplication.class,
|
||||||
|
properties = {
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:auto_release;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE",
|
||||||
|
"spring.datasource.driver-class-name=org.h2.Driver",
|
||||||
|
"spring.datasource.username=sa",
|
||||||
|
"spring.datasource.password=",
|
||||||
|
"spring.sql.init.mode=always",
|
||||||
|
"spring.sql.init.schema-locations=classpath:schema-auto-release-h2.sql",
|
||||||
|
"mybatis-plus.configuration.map-underscore-to-camel-case=true",
|
||||||
|
"mybatis-plus.global-config.db-config.logic-delete-value=1",
|
||||||
|
"mybatis-plus.global-config.db-config.logic-not-delete-value=0"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
class GltUserTicketAutoReleaseServiceTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GltUserTicketMapper userTicketMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GltUserTicketReleaseMapper releaseMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GltUserTicketLogMapper logMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GltUserTicketAutoReleaseService autoReleaseService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void clean() {
|
||||||
|
jdbcTemplate.execute("DELETE FROM glt_user_ticket_log");
|
||||||
|
jdbcTemplate.execute("DELETE FROM glt_user_ticket_release");
|
||||||
|
jdbcTemplate.execute("DELETE FROM glt_user_ticket");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void releaseDue_success_updatesTicketReleaseAndWritesLog() {
|
||||||
|
LocalDateTime now = LocalDateTime.of(2026, 2, 6, 12, 0, 0);
|
||||||
|
|
||||||
|
GltUserTicket ticket = new GltUserTicket();
|
||||||
|
ticket.setTemplateId(1);
|
||||||
|
ticket.setGoodsId(1);
|
||||||
|
ticket.setOrderId(1);
|
||||||
|
ticket.setOrderNo("T-001");
|
||||||
|
ticket.setOrderGoodsId(1);
|
||||||
|
ticket.setTotalQty(10);
|
||||||
|
ticket.setAvailableQty(0);
|
||||||
|
ticket.setFrozenQty(10);
|
||||||
|
ticket.setUsedQty(0);
|
||||||
|
ticket.setReleasedQty(0);
|
||||||
|
ticket.setUserId(100);
|
||||||
|
ticket.setSortNumber(0);
|
||||||
|
ticket.setComments("test");
|
||||||
|
ticket.setStatus(0);
|
||||||
|
ticket.setDeleted(0);
|
||||||
|
ticket.setTenantId(10584);
|
||||||
|
ticket.setCreateTime(now);
|
||||||
|
ticket.setUpdateTime(now);
|
||||||
|
assertEquals(1, userTicketMapper.insert(ticket));
|
||||||
|
assertNotNull(ticket.getId());
|
||||||
|
|
||||||
|
GltUserTicketRelease rel = new GltUserTicketRelease();
|
||||||
|
rel.setUserTicketId(ticket.getId().longValue());
|
||||||
|
rel.setUserId(100);
|
||||||
|
rel.setPeriodNo(1);
|
||||||
|
rel.setReleaseQty(3);
|
||||||
|
rel.setReleaseTime(now.minusSeconds(1));
|
||||||
|
rel.setStatus(0);
|
||||||
|
rel.setDeleted(0);
|
||||||
|
rel.setTenantId(10584);
|
||||||
|
rel.setCreateTime(now);
|
||||||
|
rel.setUpdateTime(now);
|
||||||
|
assertEquals(1, releaseMapper.insert(rel));
|
||||||
|
assertNotNull(rel.getId());
|
||||||
|
|
||||||
|
int releasedCount = autoReleaseService.releaseDue(now, 100);
|
||||||
|
assertEquals(1, releasedCount);
|
||||||
|
|
||||||
|
GltUserTicket after = userTicketMapper.selectById(ticket.getId());
|
||||||
|
assertNotNull(after);
|
||||||
|
assertEquals(3, after.getAvailableQty());
|
||||||
|
assertEquals(7, after.getFrozenQty());
|
||||||
|
assertEquals(3, after.getReleasedQty());
|
||||||
|
|
||||||
|
GltUserTicketRelease relAfter = releaseMapper.selectById(rel.getId());
|
||||||
|
assertNotNull(relAfter);
|
||||||
|
assertEquals(1, relAfter.getStatus());
|
||||||
|
|
||||||
|
List<GltUserTicketLog> logs = logMapper.selectList(null);
|
||||||
|
assertEquals(1, logs.size());
|
||||||
|
assertEquals(ticket.getId(), logs.get(0).getUserTicketId());
|
||||||
|
assertEquals(3, logs.get(0).getChangeAvailable());
|
||||||
|
assertEquals(-3, logs.get(0).getChangeFrozen());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void releaseDue_notDue_noop() {
|
||||||
|
LocalDateTime now = LocalDateTime.of(2026, 2, 6, 12, 0, 0);
|
||||||
|
|
||||||
|
GltUserTicket ticket = new GltUserTicket();
|
||||||
|
ticket.setTotalQty(10);
|
||||||
|
ticket.setAvailableQty(0);
|
||||||
|
ticket.setFrozenQty(10);
|
||||||
|
ticket.setUsedQty(0);
|
||||||
|
ticket.setReleasedQty(0);
|
||||||
|
ticket.setUserId(100);
|
||||||
|
ticket.setStatus(0);
|
||||||
|
ticket.setDeleted(0);
|
||||||
|
ticket.setTenantId(10584);
|
||||||
|
ticket.setCreateTime(now);
|
||||||
|
ticket.setUpdateTime(now);
|
||||||
|
userTicketMapper.insert(ticket);
|
||||||
|
|
||||||
|
GltUserTicketRelease rel = new GltUserTicketRelease();
|
||||||
|
rel.setUserTicketId(ticket.getId().longValue());
|
||||||
|
rel.setUserId(100);
|
||||||
|
rel.setPeriodNo(1);
|
||||||
|
rel.setReleaseQty(3);
|
||||||
|
rel.setReleaseTime(now.plusDays(1));
|
||||||
|
rel.setStatus(0);
|
||||||
|
rel.setDeleted(0);
|
||||||
|
rel.setTenantId(10584);
|
||||||
|
rel.setCreateTime(now);
|
||||||
|
rel.setUpdateTime(now);
|
||||||
|
releaseMapper.insert(rel);
|
||||||
|
|
||||||
|
int releasedCount = autoReleaseService.releaseDue(now, 100);
|
||||||
|
assertEquals(0, releasedCount);
|
||||||
|
|
||||||
|
GltUserTicket after = userTicketMapper.selectById(ticket.getId());
|
||||||
|
assertNotNull(after);
|
||||||
|
assertEquals(0, after.getAvailableQty());
|
||||||
|
assertEquals(10, after.getFrozenQty());
|
||||||
|
|
||||||
|
GltUserTicketRelease relAfter = releaseMapper.selectById(rel.getId());
|
||||||
|
assertNotNull(relAfter);
|
||||||
|
assertEquals(0, relAfter.getStatus());
|
||||||
|
assertEquals(0, logMapper.selectCount(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void releaseDue_frozenInsufficient_marksFailed() {
|
||||||
|
LocalDateTime now = LocalDateTime.of(2026, 2, 6, 12, 0, 0);
|
||||||
|
|
||||||
|
GltUserTicket ticket = new GltUserTicket();
|
||||||
|
ticket.setTotalQty(2);
|
||||||
|
ticket.setAvailableQty(0);
|
||||||
|
ticket.setFrozenQty(2);
|
||||||
|
ticket.setUsedQty(0);
|
||||||
|
ticket.setReleasedQty(0);
|
||||||
|
ticket.setUserId(100);
|
||||||
|
ticket.setStatus(0);
|
||||||
|
ticket.setDeleted(0);
|
||||||
|
ticket.setTenantId(10584);
|
||||||
|
ticket.setCreateTime(now);
|
||||||
|
ticket.setUpdateTime(now);
|
||||||
|
userTicketMapper.insert(ticket);
|
||||||
|
|
||||||
|
GltUserTicketRelease rel = new GltUserTicketRelease();
|
||||||
|
rel.setUserTicketId(ticket.getId().longValue());
|
||||||
|
rel.setUserId(100);
|
||||||
|
rel.setPeriodNo(1);
|
||||||
|
rel.setReleaseQty(3);
|
||||||
|
rel.setReleaseTime(now.minusSeconds(1));
|
||||||
|
rel.setStatus(0);
|
||||||
|
rel.setDeleted(0);
|
||||||
|
rel.setTenantId(10584);
|
||||||
|
rel.setCreateTime(now);
|
||||||
|
rel.setUpdateTime(now);
|
||||||
|
releaseMapper.insert(rel);
|
||||||
|
|
||||||
|
int releasedCount = autoReleaseService.releaseDue(now, 100);
|
||||||
|
assertEquals(0, releasedCount);
|
||||||
|
|
||||||
|
GltUserTicket after = userTicketMapper.selectById(ticket.getId());
|
||||||
|
assertNotNull(after);
|
||||||
|
assertEquals(0, after.getAvailableQty());
|
||||||
|
assertEquals(2, after.getFrozenQty());
|
||||||
|
|
||||||
|
GltUserTicketRelease relAfter = releaseMapper.selectById(rel.getId());
|
||||||
|
assertNotNull(relAfter);
|
||||||
|
assertEquals(2, relAfter.getStatus());
|
||||||
|
assertEquals(0, logMapper.selectCount(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
62
src/test/resources/schema-auto-release-h2.sql
Normal file
62
src/test/resources/schema-auto-release-h2.sql
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
DROP TABLE IF EXISTS glt_user_ticket_log;
|
||||||
|
DROP TABLE IF EXISTS glt_user_ticket_release;
|
||||||
|
DROP TABLE IF EXISTS glt_user_ticket;
|
||||||
|
|
||||||
|
CREATE TABLE glt_user_ticket (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
template_id INT,
|
||||||
|
goods_id INT,
|
||||||
|
order_id INT,
|
||||||
|
order_no VARCHAR(64),
|
||||||
|
order_goods_id INT,
|
||||||
|
total_qty INT,
|
||||||
|
available_qty INT,
|
||||||
|
frozen_qty INT,
|
||||||
|
used_qty INT,
|
||||||
|
released_qty INT,
|
||||||
|
user_id INT,
|
||||||
|
sort_number INT,
|
||||||
|
comments VARCHAR(255),
|
||||||
|
status INT,
|
||||||
|
deleted INT DEFAULT 0,
|
||||||
|
tenant_id INT,
|
||||||
|
create_time TIMESTAMP,
|
||||||
|
update_time TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE glt_user_ticket_release (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_ticket_id BIGINT,
|
||||||
|
user_id INT,
|
||||||
|
period_no INT,
|
||||||
|
release_qty INT,
|
||||||
|
release_time TIMESTAMP,
|
||||||
|
status INT,
|
||||||
|
deleted INT DEFAULT 0,
|
||||||
|
tenant_id INT,
|
||||||
|
create_time TIMESTAMP,
|
||||||
|
update_time TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE glt_user_ticket_log (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_ticket_id INT,
|
||||||
|
change_type INT,
|
||||||
|
change_available INT,
|
||||||
|
change_frozen INT,
|
||||||
|
change_used INT,
|
||||||
|
available_after INT,
|
||||||
|
frozen_after INT,
|
||||||
|
used_after INT,
|
||||||
|
order_id INT,
|
||||||
|
order_no VARCHAR(64),
|
||||||
|
user_id INT,
|
||||||
|
sort_number INT,
|
||||||
|
comments VARCHAR(255),
|
||||||
|
status INT,
|
||||||
|
deleted INT DEFAULT 0,
|
||||||
|
tenant_id INT,
|
||||||
|
create_time TIMESTAMP,
|
||||||
|
update_time TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
Reference in New Issue
Block a user