feat: persist module training attempts and actions

chore/issuance-comments-zh
chenyuan 2 months ago
parent 0f1794b1c7
commit ae8ce2819c

@ -0,0 +1,8 @@
package com.yau.digitalrmb.training.attempt.domain;
public enum ActionStatus {
RUNNING,
COMPLETED,
FAILED,
ROLLED_BACK
}

@ -0,0 +1,8 @@
package com.yau.digitalrmb.training.attempt.domain;
public enum AttemptStatus {
IN_PROGRESS,
SUBMITTED,
COMPLETED,
CANCELLED
}

@ -0,0 +1,94 @@
package com.yau.digitalrmb.training.attempt.domain;
import java.time.Instant;
import java.util.UUID;
public final class ExperimentAction {
private final UUID id;
private final UUID attemptId;
private final String stepCode;
private final String actionCode;
private ActionStatus status;
private String requestFingerprint;
private String outputJson;
private String errorCode;
private String errorMessage;
private final Instant startedAt;
private Instant completedAt;
private Instant updatedAt;
private long version;
public ExperimentAction(UUID id, UUID attemptId, String stepCode, String actionCode, ActionStatus status,
String requestFingerprint, String outputJson, String errorCode, String errorMessage,
Instant startedAt, Instant completedAt, Instant updatedAt, long version) {
this.id = id;
this.attemptId = attemptId;
this.stepCode = stepCode;
this.actionCode = actionCode;
this.status = status;
this.requestFingerprint = requestFingerprint;
this.outputJson = outputJson;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.startedAt = startedAt;
this.completedAt = completedAt;
this.updatedAt = updatedAt;
this.version = version;
}
public static ExperimentAction running(UUID id, UUID attemptId, String stepCode,
String actionCode, String requestFingerprint) {
Instant now = Instant.now();
return new ExperimentAction(id, attemptId, stepCode, actionCode, ActionStatus.RUNNING,
requestFingerprint, null, null, null, now, null, now, 0L);
}
public void complete(ActionStatus terminalStatus, String json, Instant now) {
if (terminalStatus != ActionStatus.COMPLETED && terminalStatus != ActionStatus.ROLLED_BACK) {
throw new IllegalArgumentException("动作终态不合法");
}
status = terminalStatus;
outputJson = json;
errorCode = null;
errorMessage = null;
completedAt = now;
updatedAt = now;
}
public void fail(String code, String message, Instant now) {
status = ActionStatus.FAILED;
errorCode = code;
errorMessage = message;
completedAt = now;
updatedAt = now;
}
public void restart(String fingerprint, Instant now) {
if (status != ActionStatus.FAILED && status != ActionStatus.ROLLED_BACK) {
throw new IllegalStateException("只有失败或已补偿动作可以重试");
}
status = ActionStatus.RUNNING;
requestFingerprint = fingerprint;
outputJson = null;
errorCode = null;
errorMessage = null;
completedAt = null;
updatedAt = now;
}
public void markPersisted() { version++; }
public UUID getId() { return id; }
public UUID getAttemptId() { return attemptId; }
public String getStepCode() { return stepCode; }
public String getActionCode() { return actionCode; }
public ActionStatus getStatus() { return status; }
public String getRequestFingerprint() { return requestFingerprint; }
public String getOutputJson() { return outputJson; }
public String getErrorCode() { return errorCode; }
public String getErrorMessage() { return errorMessage; }
public Instant getStartedAt() { return startedAt; }
public Instant getCompletedAt() { return completedAt; }
public Instant getUpdatedAt() { return updatedAt; }
public long getVersion() { return version; }
}

@ -0,0 +1,92 @@
package com.yau.digitalrmb.training.attempt.domain;
import java.time.Instant;
import java.util.UUID;
public final class ExperimentAttempt {
private final UUID id;
private final ExperimentModule module;
private final ExperimentSubject subject;
private final int attemptNo;
private AttemptStatus status;
private String currentStepCode;
private String currentActionCode;
private String businessId;
private long version;
private final Instant createdAt;
private Instant updatedAt;
private Instant completedAt;
public ExperimentAttempt(UUID id, ExperimentModule module, ExperimentSubject subject, int attemptNo,
AttemptStatus status, String currentStepCode, String currentActionCode,
String businessId, long version, Instant createdAt, Instant updatedAt,
Instant completedAt) {
this.id = id;
this.module = module;
this.subject = subject;
this.attemptNo = attemptNo;
this.status = status;
this.currentStepCode = currentStepCode;
this.currentActionCode = currentActionCode;
this.businessId = businessId;
this.version = version;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.completedAt = completedAt;
}
public static ExperimentAttempt start(ExperimentModule module, ExperimentSubject subject, int attemptNo, Instant now) {
return new ExperimentAttempt(UUID.randomUUID(), module, subject, attemptNo, AttemptStatus.IN_PROGRESS,
"01", null, null, 0L, now, now, null);
}
public void moveTo(String stepCode, String actionCode, Instant now) {
requireActive();
currentStepCode = stepCode;
currentActionCode = actionCode;
updatedAt = now;
}
public void bindBusinessId(String value, Instant now) {
requireActive();
if (businessId != null && value != null && !businessId.equals(value)) {
throw new IllegalStateException("实验尝试已经绑定其他业务记录");
}
businessId = value;
updatedAt = now;
}
public void complete(Instant now) {
requireActive();
status = AttemptStatus.COMPLETED;
completedAt = now;
updatedAt = now;
}
public void cancel() {
requireActive();
status = AttemptStatus.CANCELLED;
updatedAt = Instant.now();
}
public void markPersisted() { version++; }
private void requireActive() {
if (status != AttemptStatus.IN_PROGRESS && status != AttemptStatus.SUBMITTED) {
throw new IllegalStateException("实验尝试已经结束");
}
}
public UUID getId() { return id; }
public ExperimentModule getModule() { return module; }
public ExperimentSubject getSubject() { return subject; }
public int getAttemptNo() { return attemptNo; }
public AttemptStatus getStatus() { return status; }
public String getCurrentStepCode() { return currentStepCode; }
public String getCurrentActionCode() { return currentActionCode; }
public String getBusinessId() { return businessId; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public Instant getCompletedAt() { return completedAt; }
}

@ -0,0 +1,17 @@
package com.yau.digitalrmb.training.attempt.domain;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface ExperimentAttemptRepository {
ExperimentAttempt create(ExperimentModule module, ExperimentSubject subject);
Optional<ExperimentAttempt> findCurrent(ExperimentModule module, ExperimentSubject subject);
Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject);
Optional<ExperimentAttempt> findByIdForUpdate(UUID id, ExperimentSubject subject);
void save(ExperimentAttempt attempt);
void insertAction(ExperimentAction action);
void saveAction(ExperimentAction action);
Optional<ExperimentAction> findActionForUpdate(UUID attemptId, String stepCode, String actionCode);
List<ExperimentAction> findActions(UUID attemptId);
}

@ -0,0 +1,7 @@
package com.yau.digitalrmb.training.attempt.domain;
public enum ExperimentModule {
ISSUANCE,
EXCHANGE,
PAYMENT
}

@ -0,0 +1,21 @@
package com.yau.digitalrmb.training.attempt.domain;
public final class ExperimentSubject {
private final String userId;
private final long schoolId;
private final long classId;
private final String organizationId;
public ExperimentSubject(String userId, long schoolId, long classId, String organizationId) {
if (userId == null || userId.trim().isEmpty()) throw new IllegalArgumentException("用户ID不能为空");
this.userId = userId.trim();
this.schoolId = schoolId;
this.classId = classId;
this.organizationId = organizationId == null ? "" : organizationId.trim();
}
public String getUserId() { return userId; }
public long getSchoolId() { return schoolId; }
public long getClassId() { return classId; }
public String getOrganizationId() { return organizationId; }
}

@ -0,0 +1,168 @@
package com.yau.digitalrmb.training.attempt.infrastructure;
import com.yau.digitalrmb.training.attempt.domain.ActionStatus;
import com.yau.digitalrmb.training.attempt.domain.AttemptStatus;
import com.yau.digitalrmb.training.attempt.domain.ExperimentAction;
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttempt;
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttemptRepository;
import com.yau.digitalrmb.training.attempt.domain.ExperimentModule;
import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public class JdbcExperimentAttemptRepository implements ExperimentAttemptRepository {
private final JdbcTemplate jdbc;
public JdbcExperimentAttemptRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
@Transactional
public ExperimentAttempt create(ExperimentModule module, ExperimentSubject subject) {
Integer latest = jdbc.queryForObject("SELECT COALESCE(MAX(attempt_no),0) FROM training_experiment_attempt "
+ "WHERE module_code=? AND user_id=? AND school_id=? AND class_id=? "
+ "AND organization_id=?",
Integer.class, module.name(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
subject.getOrganizationId());
ExperimentAttempt attempt = ExperimentAttempt.start(module, subject, (latest == null ? 0 : latest) + 1,
Instant.now());
jdbc.update("INSERT INTO training_experiment_attempt (id,module_code,user_id,school_id,class_id,"
+ "organization_id,attempt_no,status,current_step_code,current_action_code,business_id,version,"
+ "created_at,updated_at,completed_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
attempt.getId().toString(), module.name(), subject.getUserId(), subject.getSchoolId(),
subject.getClassId(), subject.getOrganizationId(), attempt.getAttemptNo(), attempt.getStatus().name(),
attempt.getCurrentStepCode(), attempt.getCurrentActionCode(), attempt.getBusinessId(),
attempt.getVersion(), timestamp(attempt.getCreatedAt()), timestamp(attempt.getUpdatedAt()), null);
return attempt;
}
@Override
public Optional<ExperimentAttempt> findCurrent(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') 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);
}
@Override
public Optional<ExperimentAttempt> findByIdForUpdate(UUID id, ExperimentSubject subject) {
return findById(id, subject, true);
}
private Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject, boolean forUpdate) {
List<ExperimentAttempt> values = jdbc.query("SELECT * FROM training_experiment_attempt WHERE id=? "
+ "AND user_id=? AND school_id=? AND class_id=? "
+ "AND organization_id=?"
+ (forUpdate ? " FOR UPDATE" : ""),
attemptMapper(), id.toString(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
subject.getOrganizationId());
return optional(values);
}
@Override
public void save(ExperimentAttempt attempt) {
int updated = jdbc.update("UPDATE training_experiment_attempt SET status=?,current_step_code=?,"
+ "current_action_code=?,business_id=?,version=version+1,updated_at=?,completed_at=? "
+ "WHERE id=? AND version=?",
attempt.getStatus().name(), attempt.getCurrentStepCode(), attempt.getCurrentActionCode(),
attempt.getBusinessId(), timestamp(attempt.getUpdatedAt()), timestamp(attempt.getCompletedAt()),
attempt.getId().toString(), attempt.getVersion());
if (updated != 1) throw new IllegalStateException("实验尝试状态已被其他请求修改");
attempt.markPersisted();
}
@Override
public void insertAction(ExperimentAction action) {
jdbc.update("INSERT INTO training_experiment_action (id,attempt_id,step_code,action_code,status,"
+ "request_fingerprint,output_json,error_code,error_message,started_at,completed_at,updated_at,version) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
action.getId().toString(), action.getAttemptId().toString(), action.getStepCode(), action.getActionCode(),
action.getStatus().name(), action.getRequestFingerprint(), action.getOutputJson(), action.getErrorCode(),
action.getErrorMessage(), timestamp(action.getStartedAt()), timestamp(action.getCompletedAt()),
timestamp(action.getUpdatedAt()), action.getVersion());
}
@Override
public void saveAction(ExperimentAction action) {
int updated = jdbc.update("UPDATE training_experiment_action SET status=?,request_fingerprint=?,output_json=?,"
+ "error_code=?,error_message=?,completed_at=?,updated_at=?,version=version+1 "
+ "WHERE id=? AND version=?",
action.getStatus().name(), action.getRequestFingerprint(), action.getOutputJson(), action.getErrorCode(),
action.getErrorMessage(), timestamp(action.getCompletedAt()), timestamp(action.getUpdatedAt()),
action.getId().toString(), action.getVersion());
if (updated != 1) throw new IllegalStateException("实验动作状态已被其他请求修改");
action.markPersisted();
}
@Override
public Optional<ExperimentAction> findActionForUpdate(UUID attemptId, String stepCode, String actionCode) {
List<ExperimentAction> values = jdbc.query("SELECT * FROM training_experiment_action WHERE attempt_id=? "
+ "AND step_code=? AND action_code=? FOR UPDATE", actionMapper(),
attemptId.toString(), stepCode, actionCode);
return optional(values);
}
@Override
public List<ExperimentAction> findActions(UUID attemptId) {
return jdbc.query("SELECT * FROM training_experiment_action WHERE attempt_id=? "
+ "ORDER BY started_at,id", actionMapper(), attemptId.toString());
}
private RowMapper<ExperimentAttempt> attemptMapper() {
return new RowMapper<ExperimentAttempt>() {
@Override
public ExperimentAttempt mapRow(ResultSet rs, int rowNum) throws SQLException {
ExperimentSubject subject = new ExperimentSubject(rs.getString("user_id"), rs.getLong("school_id"),
rs.getLong("class_id"), rs.getString("organization_id"));
return new ExperimentAttempt(UUID.fromString(rs.getString("id")),
ExperimentModule.valueOf(rs.getString("module_code")), subject, rs.getInt("attempt_no"),
AttemptStatus.valueOf(rs.getString("status")), rs.getString("current_step_code"),
rs.getString("current_action_code"), rs.getString("business_id"), rs.getLong("version"),
instant(rs.getTimestamp("created_at")), instant(rs.getTimestamp("updated_at")),
instant(rs.getTimestamp("completed_at")));
}
};
}
private RowMapper<ExperimentAction> actionMapper() {
return new RowMapper<ExperimentAction>() {
@Override
public ExperimentAction mapRow(ResultSet rs, int rowNum) throws SQLException {
return new ExperimentAction(UUID.fromString(rs.getString("id")),
UUID.fromString(rs.getString("attempt_id")), rs.getString("step_code"),
rs.getString("action_code"), ActionStatus.valueOf(rs.getString("status")),
rs.getString("request_fingerprint"), rs.getString("output_json"),
rs.getString("error_code"), rs.getString("error_message"),
instant(rs.getTimestamp("started_at")), instant(rs.getTimestamp("completed_at")),
instant(rs.getTimestamp("updated_at")), rs.getLong("version"));
}
};
}
private static <T> Optional<T> optional(List<T> values) {
return values.isEmpty() ? Optional.<T>empty() : Optional.of(values.get(0));
}
private static Timestamp timestamp(Instant value) { return value == null ? null : Timestamp.from(value); }
private static Instant instant(Timestamp value) { return value == null ? null : value.toInstant(); }
}

@ -0,0 +1,41 @@
CREATE TABLE IF NOT EXISTS training_experiment_attempt (
id CHAR(36) PRIMARY KEY,
module_code VARCHAR(32) NOT NULL,
user_id VARCHAR(36) NOT NULL,
school_id BIGINT NOT NULL,
class_id BIGINT NOT NULL,
organization_id VARCHAR(64) NOT NULL DEFAULT '',
attempt_no INT NOT NULL,
status VARCHAR(32) NOT NULL,
current_step_code VARCHAR(16) NULL,
current_action_code VARCHAR(64) NULL,
business_id VARCHAR(96) NULL,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP NULL,
CONSTRAINT uk_experiment_attempt_no
UNIQUE (module_code, user_id, school_id, class_id, organization_id, attempt_no),
INDEX idx_experiment_attempt_current
(module_code, user_id, school_id, class_id, status, attempt_no)
);
CREATE TABLE IF NOT EXISTS training_experiment_action (
id CHAR(36) PRIMARY KEY,
attempt_id CHAR(36) NOT NULL,
step_code VARCHAR(16) NOT NULL,
action_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
request_fingerprint CHAR(64) NOT NULL,
output_json TEXT NULL,
error_code VARCHAR(32) NULL,
error_message VARCHAR(512) NULL,
started_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP NULL,
updated_at TIMESTAMP NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_experiment_action UNIQUE (attempt_id, step_code, action_code),
INDEX idx_experiment_action_attempt (attempt_id, started_at),
CONSTRAINT fk_experiment_action_attempt
FOREIGN KEY (attempt_id) REFERENCES training_experiment_attempt(id)
);

@ -822,6 +822,48 @@ CREATE TABLE IF NOT EXISTS payment_step_log (
operator_name VARCHAR(64) NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment_order(id)
);
CREATE TABLE IF NOT EXISTS training_experiment_attempt (
id CHAR(36) PRIMARY KEY,
module_code VARCHAR(32) NOT NULL,
user_id VARCHAR(36) NOT NULL,
school_id BIGINT NOT NULL,
class_id BIGINT NOT NULL,
organization_id VARCHAR(64) NOT NULL DEFAULT '',
attempt_no INT NOT NULL,
status VARCHAR(32) NOT NULL,
current_step_code VARCHAR(16) NULL,
current_action_code VARCHAR(64) NULL,
business_id VARCHAR(96) NULL,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP NULL,
CONSTRAINT uk_experiment_attempt_no
UNIQUE (module_code, user_id, school_id, class_id, organization_id, attempt_no),
INDEX idx_experiment_attempt_current
(module_code, user_id, school_id, class_id, status, attempt_no)
);
CREATE TABLE IF NOT EXISTS training_experiment_action (
id CHAR(36) PRIMARY KEY,
attempt_id CHAR(36) NOT NULL,
step_code VARCHAR(16) NOT NULL,
action_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
request_fingerprint CHAR(64) NOT NULL,
output_json TEXT NULL,
error_code VARCHAR(32) NULL,
error_message VARCHAR(512) NULL,
started_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP NULL,
updated_at TIMESTAMP NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_experiment_action UNIQUE (attempt_id, step_code, action_code),
INDEX idx_experiment_action_attempt (attempt_id, started_at),
CONSTRAINT fk_experiment_action_attempt
FOREIGN KEY (attempt_id) REFERENCES training_experiment_attempt(id)
);
-- 开通数字货币钱包实验模块 - 步骤一
CREATE TABLE IF NOT EXISTS wallet_application (

@ -0,0 +1,72 @@
package com.yau.digitalrmb.training.attempt.infrastructure;
import com.yau.digitalrmb.training.attempt.domain.ActionStatus;
import com.yau.digitalrmb.training.attempt.domain.ExperimentAction;
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttempt;
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttemptRepository;
import com.yau.digitalrmb.training.attempt.domain.ExperimentModule;
import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class JdbcExperimentAttemptRepositoryTest {
@Autowired
private ExperimentAttemptRepository repository;
@Test
void assignsIncreasingAttemptNumbersWithinTheAuthenticatedScope() {
ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");
ExperimentAttempt first = repository.create(ExperimentModule.EXCHANGE, subject);
first.cancel();
repository.save(first);
ExperimentAttempt second = repository.create(ExperimentModule.EXCHANGE, subject);
assertEquals(1, first.getAttemptNo());
assertEquals(2, second.getAttemptNo());
assertEquals(second.getId(), repository.findCurrent(ExperimentModule.EXCHANGE, subject).get().getId());
}
@Test
void scopesAttemptReadsToUserSchoolClassAndOrganization() {
ExperimentSubject owner = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");
ExperimentAttempt attempt = repository.create(ExperimentModule.ISSUANCE, owner);
assertTrue(repository.findById(attempt.getId(), owner).isPresent());
assertFalse(repository.findById(attempt.getId(),
new ExperimentSubject("user-2", 10L, 20L, "ORG-1")).isPresent());
assertFalse(repository.findById(attempt.getId(),
new ExperimentSubject("user-1", 10L, 21L, "ORG-1")).isPresent());
}
@Test
void enforcesOneActionRowForAnAttemptStepAndActionCode() {
ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");
ExperimentAttempt attempt = repository.create(ExperimentModule.PAYMENT, subject);
UUID actionId = UUID.randomUUID();
repository.insertAction(ExperimentAction.running(actionId, attempt.getId(), "02",
"generate-digest", "fingerprint-1"));
ExperimentAction locked = repository.findActionForUpdate(attempt.getId(), "02", "generate-digest")
.orElseThrow(AssertionError::new);
assertEquals(ActionStatus.RUNNING, locked.getStatus());
assertThrows(DuplicateKeyException.class, () -> repository.insertAction(
ExperimentAction.running(UUID.randomUUID(), attempt.getId(), "02",
"generate-digest", "fingerprint-2")));
}
}

@ -795,3 +795,44 @@ CREATE TABLE IF NOT EXISTS payment_step_log (
operator_name VARCHAR(64) NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment_order(id)
);
CREATE TABLE IF NOT EXISTS training_experiment_attempt (
id CHAR(36) PRIMARY KEY,
module_code VARCHAR(32) NOT NULL,
user_id VARCHAR(36) NOT NULL,
school_id BIGINT NOT NULL,
class_id BIGINT NOT NULL,
organization_id VARCHAR(64) NOT NULL DEFAULT '',
attempt_no INT NOT NULL,
status VARCHAR(32) NOT NULL,
current_step_code VARCHAR(16) NULL,
current_action_code VARCHAR(64) NULL,
business_id VARCHAR(96) NULL,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP NULL,
CONSTRAINT uk_experiment_attempt_no
UNIQUE (module_code, user_id, school_id, class_id, organization_id, attempt_no),
INDEX idx_experiment_attempt_current
(module_code, user_id, school_id, class_id, status, attempt_no)
);
CREATE TABLE IF NOT EXISTS training_experiment_action (
id CHAR(36) PRIMARY KEY,
attempt_id CHAR(36) NOT NULL,
step_code VARCHAR(16) NOT NULL,
action_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
request_fingerprint CHAR(64) NOT NULL,
output_json TEXT NULL,
error_code VARCHAR(32) NULL,
error_message VARCHAR(512) NULL,
started_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP NULL,
updated_at TIMESTAMP NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_experiment_action UNIQUE (attempt_id, step_code, action_code),
INDEX idx_experiment_action_attempt (attempt_id, started_at),
CONSTRAINT fk_experiment_action_attempt
FOREIGN KEY (attempt_id) REFERENCES training_experiment_attempt(id)
);

Loading…
Cancel
Save