feat: 新增 GET /exchange/attempts/latest,区分实验"已完成"与"从未开始"

attempts/current 只查 IN_PROGRESS/SUBMITTED,实验走完后返回 404,
前端无法区分"已完成"和"从未开始",会静默新建一个空实验,导致页面
回显旧进度而后端动作历史为空。

新增 latest 接口返回最近一次未取消的实验(含 COMPLETED),已取消的不返回。
current 的行为不变。
chore/issuance-comments-zh
chenyuan 5 days ago
parent 2af28bbd51
commit 1e3abc9554

@ -54,6 +54,13 @@ public class ExchangeTrainingAttemptController {
return ok(attempts.current(ExperimentModule.EXCHANGE, subject(users.getCurrentUser())));
}
@GetMapping("/latest")
@Operation(summary = "查询最近一次兑换实验",
description = "含已完成的实验,用于区分「从未开始」与「已完成」;已取消的不返回。")
public ApiResponse<ExperimentAttemptView> latest() {
return ok(attempts.latest(ExperimentModule.EXCHANGE, subject(users.getCurrentUser())));
}
@GetMapping("/{attemptId}")
@Operation(summary = "查询兑换实验详情和已保存动作")
public ApiResponse<ExperimentAttemptView> detail(@PathVariable UUID attemptId) {

@ -50,6 +50,13 @@ public class ExperimentAttemptService {
return toAttemptView(attempt, repository.findActions(attempt.getId()));
}
@Transactional(readOnly = true)
public ExperimentAttemptView latest(ExperimentModule module, ExperimentSubject subject) {
ExperimentAttempt attempt = repository.findLatest(module, subject)
.orElseThrow(() -> notFound("未找到兑换实验记录"));
return toAttemptView(attempt, repository.findActions(attempt.getId()));
}
@Transactional(readOnly = true)
public ExperimentAttemptView detail(UUID attemptId, ExperimentSubject subject) {
ExperimentAttempt attempt = repository.findById(attemptId, subject)

@ -7,6 +7,8 @@ import java.util.UUID;
public interface ExperimentAttemptRepository {
ExperimentAttempt create(ExperimentModule module, ExperimentSubject subject);
Optional<ExperimentAttempt> findCurrent(ExperimentModule module, ExperimentSubject subject);
/** 最近一次未取消的实验,含已完成的;用于区分「从未开始」与「已完成」。 */
Optional<ExperimentAttempt> findLatest(ExperimentModule module, ExperimentSubject subject);
Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject);
Optional<ExperimentAttempt> findByIdForUpdate(UUID id, ExperimentSubject subject);
void save(ExperimentAttempt attempt);

@ -59,6 +59,17 @@ public class JdbcExperimentAttemptRepository implements ExperimentAttemptReposit
return optional(values);
}
@Override
public Optional<ExperimentAttempt> findLatest(ExperimentModule module, ExperimentSubject subject) {
List<ExperimentAttempt> values = jdbc.query("SELECT * FROM training_experiment_attempt WHERE module_code=? "
+ "AND user_id=? AND school_id=? AND class_id=? "
+ "AND organization_id=? "
+ "AND status IN ('IN_PROGRESS','SUBMITTED','COMPLETED') ORDER BY attempt_no DESC LIMIT 1",
attemptMapper(), module.name(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
subject.getOrganizationId());
return optional(values);
}
@Override
public Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject) {
return findById(id, subject, false);

@ -87,6 +87,8 @@ class ExchangeTrainingAttemptControllerTest {
mockMvc.perform(get("/api/v1/exchange/attempts/current").with(jwtForUser()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptId").value(attemptId.toString()));
mockMvc.perform(get("/api/v1/exchange/attempts/latest").with(jwtForUser()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptId").value(attemptId.toString()));
mockMvc.perform(get("/api/v1/exchange/attempts/{id}", attemptId).with(jwtForUser()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptId").value(attemptId.toString()));
mockMvc.perform(post("/api/v1/exchange/attempts/{id}/cancel", attemptId).with(jwtForUser()))

@ -41,6 +41,34 @@ class JdbcExperimentAttemptRepositoryTest {
assertEquals(second.getId(), repository.findCurrent(ExperimentModule.EXCHANGE, subject).get().getId());
}
@Test
void findLatestReturnsCompletedAttemptButCurrentDoesNot() {
ExperimentSubject subject = new ExperimentSubject("user-latest", 10L, 20L, "ORG-1");
ExperimentAttempt first = repository.create(ExperimentModule.EXCHANGE, subject);
first.complete(java.time.Instant.now());
repository.save(first);
assertFalse(repository.findCurrent(ExperimentModule.EXCHANGE, subject).isPresent());
assertEquals(first.getId(), repository.findLatest(ExperimentModule.EXCHANGE, subject).get().getId());
}
@Test
void findLatestSkipsCancelledAttemptsAndPrefersTheNewestOne() {
ExperimentSubject subject = new ExperimentSubject("user-latest-2", 10L, 20L, "ORG-1");
ExperimentAttempt first = repository.create(ExperimentModule.EXCHANGE, subject);
first.complete(java.time.Instant.now());
repository.save(first);
ExperimentAttempt second = repository.create(ExperimentModule.EXCHANGE, subject);
assertEquals(second.getId(), repository.findLatest(ExperimentModule.EXCHANGE, subject).get().getId());
second.cancel();
repository.save(second);
assertEquals(first.getId(), repository.findLatest(ExperimentModule.EXCHANGE, subject).get().getId());
assertFalse(repository.findLatest(ExperimentModule.PAYMENT, subject).isPresent());
}
@Test
void scopesAttemptReadsToUserSchoolClassAndOrganization() {
ExperimentSubject owner = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");

Loading…
Cancel
Save