feat(module6): implement charity donation workflow

chore/issuance-comments-zh
chenyuan 4 weeks ago
parent 2dd9c817be
commit 847a761cbb

@ -3,6 +3,8 @@ package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationMapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationMapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.SalaryBatchEntity;
@ -23,6 +25,7 @@ public class CorporateInnovationTrainingResetService {
@Resource private CorporateWalletApplicationMapper corporateWalletMapper;
@Resource private RemoteWalletApplicationMapper remoteWalletMapper;
@Resource private SalaryBatchMapper salaryBatchMapper;
@Resource private DonationRecordMapper donationRecordMapper;
@Resource private StuModuleScoreDetailsMapper scoreDetailsMapper;
@Resource private UserStepProgressNewService stepProgressService;
@ -32,6 +35,7 @@ public class CorporateInnovationTrainingResetService {
count += logicalDeleteCorporateWalletApplications(subject);
count += logicalDeleteRemoteWalletApplications(subject);
count += logicalDeleteSalaryBatches(subject);
count += logicalDeleteDonationRecords(subject);
count += logicalDeleteScore(subject.getUserId());
stepProgressService.reset(subject.getUserId(), TRAINING_ID);
return new CorporateInnovationTrainingResetResult(count);
@ -61,6 +65,14 @@ public class CorporateInnovationTrainingResetService {
.set(SalaryBatchEntity::getDeleted, true));
}
private int logicalDeleteDonationRecords(InstitutionKeySubject subject) {
return donationRecordMapper.update(null, new LambdaUpdateWrapper<DonationRecordEntity>()
.eq(DonationRecordEntity::getUserId, subject.getUserId())
.eq(DonationRecordEntity::getSchoolId, subject.getSchoolId())
.eq(DonationRecordEntity::getClassId, subject.getClassId())
.set(DonationRecordEntity::getDeleted, true));
}
private int logicalDeleteScore(String userId) {
return scoreDetailsMapper.update(null, new LambdaUpdateWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, userId)

@ -0,0 +1,44 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@Schema(description = "公益捐赠三步流程页面数据")
public class DonationPageResult {
private final String donorName;
private final String donorWalletId;
private final String recipientName;
private final String recipientWalletId;
private final String amount;
private final String projectName;
private final String memo;
private final String donationTimestamp;
private final String donationId;
private final String donationMessage;
private final String donationDigest;
private final String donorPrivateKey;
private final String donorSignature;
private final String donationOutputJson;
private final String receiptStatus;
private final String publicationTimestamp;
private final String publicationMessage;
private final String publicationDigest;
private final String publicationId;
private final String publicationTitle;
private final String publicationContent;
private final String publicationUrl;
private final String publicationOutputJson;
private final String supervisionTimestamp;
private final String supervisionMessage;
private final String supervisionDigest;
private final String regulatorPrivateKey;
private final String regulatorSignature;
private final String approvalId;
private final String approvalTime;
private final String recordNumber;
private final String supervisionOutputJson;
private final String status;
}

@ -0,0 +1,418 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
@Service
public class DonationService {
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final String DONOR_KEY_TYPE = "企业法人私钥";
private static final String REGULATOR_KEY_TYPE = "监管机构私钥";
private static final String REGULATOR_NAME = "民政部公益监管中心";
private enum Status {
DRAFT, CONCATENATED, DIGESTED, SIGNED, SUBMITTED, RECEIVED,
PUBLICATION_EXTRACTED, PUBLICATION_DIGESTED, PUBLICATION_READY, PUBLISHED,
SUPERVISION_EXTRACTED, SUPERVISION_DIGESTED, SUPERVISION_SIGNED, ARCHIVED
}
@Resource private DonationRecordMapper mapper;
@Resource private CorporateWalletApplicationRepository corporateWalletRepository;
@Resource private RemoteWalletApplicationRepository remoteWalletRepository;
@Resource private CorporateWalletKeyService keyService;
@Resource private InstitutionIdentityCryptography cryptography;
@Resource private SalaryScoreService scoreService;
@Resource private ObjectMapper objectMapper;
@Transactional
public DonationPageResult page(InstitutionKeySubject subject, String operator) {
DonationRecordEntity record = findLatest(subject);
CorporateWalletApplication donor = donorWallet(subject);
RemoteWalletApplication recipient = recipientWallet(subject);
return result(subject, record, donor, recipient, operator);
}
@Transactional
public DonationPageResult concatenate(InstitutionKeySubject subject, String operator,
DonationCreateRequest request) {
CorporateWalletApplication donor = requiredDonorWallet(subject);
RemoteWalletApplication recipient = requiredRecipientWallet(subject);
validateWalletInputs(donor, recipient, request);
DonationRecordEntity record = findLatest(subject);
if (record == null || status(record).ordinal() >= Status.SUBMITTED.ordinal()) {
record = new DonationRecordEntity();
record.setUserId(subject.getUserId());
record.setSchoolId(subject.getSchoolId());
record.setClassId(subject.getClassId());
record.setDeleted(false);
record.setStatus(Status.DRAFT.name());
}
String amount = money(request.getAmount());
String message = String.join("|", "DONATE", request.getDonorWalletId(),
request.getRecipientWalletId(), amount, request.getProjectName().trim(),
request.getMemo().trim(), request.getTimestamp());
record.setDonationId("DONATE_" + request.getTimestamp().substring(0, 8) + "_001");
record.setDonorName(request.getDonorName().trim());
record.setDonorWalletId(request.getDonorWalletId().trim());
record.setRecipientName(request.getRecipientName().trim());
record.setRecipientWalletId(request.getRecipientWalletId().trim());
record.setAmount(request.getAmount().setScale(2));
record.setProjectName(request.getProjectName().trim());
record.setMemo(request.getMemo().trim());
record.setDonationTimestamp(request.getTimestamp());
record.setDonationMessage(message);
record.setDonationDigest(null);
record.setDonorSignature(null);
record.setDonationOutputJson(null);
record.setStatus(Status.CONCATENATED.name());
save(record);
return result(subject, record, donor, recipient, operator);
}
@Transactional
public DonationPageResult digest(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.CONCATENATED, "请先拼接捐赠原文");
if (record.getDonationDigest() == null) {
record.setDonationDigest(cryptography.sm3(record.getDonationMessage()));
record.setStatus(Status.DIGESTED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getDonorName());
}
@Transactional
public DonationPageResult signDonation(InstitutionKeySubject subject, String providedPrivateKey) {
DonationRecordEntity record = required(subject, Status.DIGESTED, "请先生成捐赠摘要");
String privateKey = keyService.getOrCreatePrivateKey(subject, DONOR_KEY_TYPE, record.getDonorName());
validatePrivateKey(subject, privateKey, providedPrivateKey, "企业法人私钥不正确,错误次数+1");
if (record.getDonorSignature() == null) {
record.setDonorSignature(cryptography.sign(privateKey, record.getDonationDigest()));
record.setStatus(Status.SIGNED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getDonorName());
}
@Transactional
public DonationPageResult outputDonation(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.SIGNED, "请先完成企业法人SM2签名");
if (record.getDonationOutputJson() == null) {
Map<String, Object> output = new LinkedHashMap<>();
output.put("donationId", record.getDonationId());
output.put("corpWalletId", record.getDonorWalletId());
output.put("orgWalletId", record.getRecipientWalletId());
output.put("amount", money(record.getAmount()));
output.put("project", record.getProjectName());
output.put("memo", record.getMemo());
output.put("digest", record.getDonationDigest());
output.put("corpSignature", record.getDonorSignature());
output.put("timestamp", record.getDonationTimestamp());
output.put("status", "已提交");
record.setDonationOutputJson(json(output));
record.setStatus(Status.SUBMITTED.name());
mapper.updateById(record);
scoreService.markStepCompleted(subject.getUserId(), 6);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getDonorName());
}
@Transactional
public DonationPageResult receiptDetails(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.SUBMITTED, "请先由捐赠企业提交捐赠报文");
if (status(record).ordinal() < Status.RECEIVED.ordinal()) {
record.setStatus(Status.RECEIVED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName());
}
@Transactional
public DonationPageResult extractPublication(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.RECEIVED, "请先查看捐款明细");
if (record.getPublicationMessage() == null) {
String timestamp = now();
String title = publicationTitle(record, timestamp);
record.setPublicationTimestamp(timestamp);
record.setPublicationTitle(title);
record.setPublicationMessage(String.join("|", "PUBLIC", record.getRecipientWalletId(),
title, record.getDonorName(), money(record.getAmount()),
record.getProjectName(), timestamp));
record.setStatus(Status.PUBLICATION_EXTRACTED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName());
}
@Transactional
public DonationPageResult digestPublication(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.PUBLICATION_EXTRACTED, "请先提取公示拼接原文");
if (record.getPublicationDigest() == null) {
record.setPublicationDigest(cryptography.sm3(record.getPublicationMessage()));
record.setStatus(Status.PUBLICATION_DIGESTED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName());
}
@Transactional
public DonationPageResult publish(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.PUBLICATION_DIGESTED, "请先生成资金公示摘要");
if (record.getPublicationId() == null) {
String month = record.getPublicationTimestamp().substring(0, 6);
record.setPublicationId("PUBLIC_" + record.getPublicationTimestamp().substring(0, 8) + "_001");
record.setPublicationContent("本月收到" + record.getDonorName() + "捐赠" + moneyWithGrouping(record.getAmount())
+ "元,用于" + record.getProjectName());
record.setPublicationUrl("https://www.sunshine.org/donate/" + month);
record.setStatus(Status.PUBLICATION_READY.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName());
}
@Transactional
public DonationPageResult outputPublication(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.PUBLICATION_READY, "请先发布资金公示");
if (record.getPublicationOutputJson() == null) {
Map<String, Object> output = new LinkedHashMap<>();
output.put("publicationId", record.getPublicationId());
output.put("orgId", record.getRecipientWalletId());
output.put("title", record.getPublicationTitle());
output.put("content", record.getPublicationContent());
output.put("url", record.getPublicationUrl());
output.put("digest", record.getPublicationDigest());
output.put("publishTime", record.getPublicationTimestamp());
output.put("status", "PUBLISHED");
record.setPublicationOutputJson(json(output));
record.setStatus(Status.PUBLISHED.name());
mapper.updateById(record);
scoreService.markStepCompleted(subject.getUserId(), 7);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName());
}
@Transactional
public DonationPageResult extractSupervision(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.PUBLISHED, "请先完成受赠机构资金公示");
if (record.getSupervisionMessage() == null) {
String timestamp = now();
record.setSupervisionTimestamp(timestamp);
record.setSupervisionMessage(String.join("|", "SUPERVISE", record.getDonationId(),
REGULATOR_NAME, money(record.getAmount()), record.getProjectName(), timestamp));
record.setStatus(Status.SUPERVISION_EXTRACTED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME);
}
@Transactional
public DonationPageResult digestSupervision(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.SUPERVISION_EXTRACTED, "请先提取监管审核原文");
if (record.getSupervisionDigest() == null) {
record.setSupervisionDigest(cryptography.sm3(record.getSupervisionMessage()));
record.setStatus(Status.SUPERVISION_DIGESTED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME);
}
@Transactional
public DonationPageResult signSupervision(InstitutionKeySubject subject, String providedPrivateKey) {
DonationRecordEntity record = required(subject, Status.SUPERVISION_DIGESTED, "请先生成监管审核摘要");
String privateKey = keyService.getOrCreatePrivateKey(subject, REGULATOR_KEY_TYPE, REGULATOR_NAME);
validatePrivateKey(subject, privateKey, providedPrivateKey, "监管机构私钥不正确,错误次数+1");
if (record.getRegulatorSignature() == null) {
record.setRegulatorSignature(cryptography.sign(privateKey, record.getSupervisionDigest()));
record.setStatus(Status.SUPERVISION_SIGNED.name());
mapper.updateById(record);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME);
}
@Transactional
public DonationPageResult outputSupervision(InstitutionKeySubject subject) {
DonationRecordEntity record = required(subject, Status.SUPERVISION_SIGNED, "请先完成监管机构SM2签名");
if (record.getSupervisionOutputJson() == null) {
String date = record.getSupervisionTimestamp().substring(0, 8);
record.setApprovalId("APPROVE_" + date + "_001");
record.setApprovalTime(record.getSupervisionTimestamp());
record.setRecordNumber("RECORD_" + date + "_001");
Map<String, Object> output = new LinkedHashMap<>();
output.put("approvalId", record.getApprovalId());
output.put("donationId", record.getDonationId());
output.put("supervisor", REGULATOR_NAME);
output.put("approvalResult", "APPROVED");
output.put("supervisorSignature", record.getRegulatorSignature());
output.put("approvalTime", record.getApprovalTime());
output.put("recordNumber", record.getRecordNumber());
output.put("status", "已备案");
record.setSupervisionOutputJson(json(output));
record.setStatus(Status.ARCHIVED.name());
mapper.updateById(record);
scoreService.markStepCompleted(subject.getUserId(), 8);
}
return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME);
}
private DonationPageResult result(InstitutionKeySubject subject, DonationRecordEntity record,
CorporateWalletApplication donor, RemoteWalletApplication recipient,
String operator) {
String donorName = record != null ? record.getDonorName() : donor == null ? null : donor.getCorpName();
String recipientName = record != null ? record.getRecipientName() : recipient == null ? null : recipient.getCorpName();
String effectiveOperator = operator == null || operator.trim().isEmpty() ? "企业法人" : operator;
String donorKey = keyService.getOrCreatePrivateKey(subject, DONOR_KEY_TYPE, effectiveOperator);
String regulatorKey = keyService.getOrCreatePrivateKey(subject, REGULATOR_KEY_TYPE, REGULATOR_NAME);
return DonationPageResult.builder()
.donorName(donorName)
.donorWalletId(record != null ? record.getDonorWalletId() : donor == null ? null : donor.getWalletId())
.recipientName(recipientName)
.recipientWalletId(record != null ? record.getRecipientWalletId() : recipient == null ? null : recipient.getWalletId())
.amount(record == null || record.getAmount() == null ? "100000.00" : money(record.getAmount()))
.projectName(record == null ? "乡村教育支持计划" : record.getProjectName())
.memo(record == null ? "乡村教育支持计划捐款" : record.getMemo())
.donationTimestamp(record == null ? null : record.getDonationTimestamp())
.donationId(record == null ? null : record.getDonationId())
.donationMessage(record == null ? null : record.getDonationMessage())
.donationDigest(record == null ? null : record.getDonationDigest())
.donorPrivateKey(donorKey)
.donorSignature(record == null ? null : record.getDonorSignature())
.donationOutputJson(record == null ? null : record.getDonationOutputJson())
.receiptStatus(record != null && status(record).ordinal() >= Status.RECEIVED.ordinal() ? "已到账" : null)
.publicationTimestamp(record == null ? null : record.getPublicationTimestamp())
.publicationMessage(record == null ? null : record.getPublicationMessage())
.publicationDigest(record == null ? null : record.getPublicationDigest())
.publicationId(record == null ? null : record.getPublicationId())
.publicationTitle(record == null ? null : record.getPublicationTitle())
.publicationContent(record == null ? null : record.getPublicationContent())
.publicationUrl(record == null ? null : record.getPublicationUrl())
.publicationOutputJson(record == null ? null : record.getPublicationOutputJson())
.supervisionTimestamp(record == null ? null : record.getSupervisionTimestamp())
.supervisionMessage(record == null ? null : record.getSupervisionMessage())
.supervisionDigest(record == null ? null : record.getSupervisionDigest())
.regulatorPrivateKey(regulatorKey)
.regulatorSignature(record == null ? null : record.getRegulatorSignature())
.approvalId(record == null ? null : record.getApprovalId())
.approvalTime(record == null ? null : record.getApprovalTime())
.recordNumber(record == null ? null : record.getRecordNumber())
.supervisionOutputJson(record == null ? null : record.getSupervisionOutputJson())
.status(record == null ? null : record.getStatus())
.build();
}
private DonationRecordEntity required(InstitutionKeySubject subject, Status minimum, String message) {
DonationRecordEntity record = findLatest(subject);
if (record == null || status(record).ordinal() < minimum.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, message);
}
return record;
}
private DonationRecordEntity findLatest(InstitutionKeySubject subject) {
return mapper.selectOne(new LambdaQueryWrapper<DonationRecordEntity>()
.eq(DonationRecordEntity::getUserId, subject.getUserId())
.eq(DonationRecordEntity::getSchoolId, subject.getSchoolId())
.eq(DonationRecordEntity::getClassId, subject.getClassId())
.eq(DonationRecordEntity::getDeleted, false)
.orderByDesc(DonationRecordEntity::getCreatedAt)
.orderByDesc(DonationRecordEntity::getId)
.last("LIMIT 1"));
}
private CorporateWalletApplication donorWallet(InstitutionKeySubject subject) {
return corporateWalletRepository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
}
private RemoteWalletApplication recipientWallet(InstitutionKeySubject subject) {
return remoteWalletRepository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
}
private CorporateWalletApplication requiredDonorWallet(InstitutionKeySubject subject) {
CorporateWalletApplication wallet = donorWallet(subject);
if (wallet == null || wallet.getWalletId() == null || wallet.getStatus() != CorporateWalletApplication.Status.ACTIVATED) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成临柜开立对公钱包");
}
return wallet;
}
private RemoteWalletApplication requiredRecipientWallet(InstitutionKeySubject subject) {
RemoteWalletApplication wallet = recipientWallet(subject);
if (wallet == null || wallet.getWalletId() == null || wallet.getStatus() != RemoteWalletApplication.Status.ACTIVATED) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成远程开立受赠机构对公钱包");
}
return wallet;
}
private void validateWalletInputs(CorporateWalletApplication donor, RemoteWalletApplication recipient,
DonationCreateRequest request) {
if (!same(donor.getCorpName(), request.getDonorName())
|| !same(donor.getWalletId(), request.getDonorWalletId())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "捐赠企业或付款钱包与对公钱包开立结果不一致");
}
if (!same(recipient.getCorpName(), request.getRecipientName())
|| !same(recipient.getWalletId(), request.getRecipientWalletId())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "受赠机构或收款钱包与远程开立结果不一致");
}
}
private void validatePrivateKey(InstitutionKeySubject subject, String expected, String actual, String message) {
if (!same(expected, actual)) {
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, message);
}
}
private void save(DonationRecordEntity record) {
if (record.getId() == null) mapper.insert(record); else mapper.updateById(record);
}
private String publicationTitle(DonationRecordEntity record, String timestamp) {
int month = Integer.parseInt(timestamp.substring(4, 6));
return record.getRecipientName() + timestamp.substring(0, 4) + "年" + month + "月捐赠收入公示";
}
private String json(Map<String, Object> value) {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("公益捐赠报文序列化失败", exception);
}
}
private Status status(DonationRecordEntity record) {
return record.getStatus() == null ? Status.DRAFT : Status.valueOf(record.getStatus());
}
private String now() { return LocalDateTime.now().format(TIMESTAMP_FORMAT); }
private String money(BigDecimal amount) { return amount.setScale(2).toPlainString(); }
private String moneyWithGrouping(BigDecimal amount) { return String.format("%,.2f", amount); }
private boolean same(String left, String right) {
return left != null && right != null && left.trim().equalsIgnoreCase(right.trim());
}
}

@ -45,7 +45,7 @@ public class SalaryScoreService {
/**
* 标记步骤完成,更新进度和成绩
* @param userId 用户ID
* @param completedStep 完成的步骤编号(1-7)
* @param completedStep 完成的步骤编号(1-8)
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void markStepCompleted(String userId, int completedStep) {
@ -55,10 +55,10 @@ public class SalaryScoreService {
StuModuleScoreDetailsEntity detail = getOrCreate(userId);
int errorCount = errorCount(detail.getCompletionStatus());
// 模块六共7个步骤:
// 模块六共8个步骤:
// 1-临柜开立, 2-远程开立, 3-数币操作员导入, 4-数币复核员审核,
// 5-交易确认与工资发放, 6-捐赠企业发起, 7-受赠机构接收
int totalSteps = 7;
// 5-交易确认与工资发放, 6-捐赠企业发起, 7-受赠机构接收, 8-第三方监管审核
int totalSteps = 8;
int normalizedStep = Math.max(0, Math.min(totalSteps, completedStep));
// 计算进度百分比:completedStep / totalSteps * 100

@ -0,0 +1,48 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@TableName("donation_record")
public class DonationRecordEntity extends AuditableEntity {
@TableField("user_id") private String userId;
@TableField("school_id") private Long schoolId;
@TableField("class_id") private Long classId;
@TableField("donation_id") private String donationId;
@TableField("donor_name") private String donorName;
@TableField("donor_wallet_id") private String donorWalletId;
@TableField("recipient_name") private String recipientName;
@TableField("recipient_wallet_id") private String recipientWalletId;
private BigDecimal amount;
@TableField("project_name") private String projectName;
private String memo;
@TableField("donation_timestamp") private String donationTimestamp;
@TableField("donation_message") private String donationMessage;
@TableField("donation_digest") private String donationDigest;
@TableField("donor_signature") private String donorSignature;
@TableField("donation_output_json") private String donationOutputJson;
@TableField("publication_timestamp") private String publicationTimestamp;
@TableField("publication_message") private String publicationMessage;
@TableField("publication_digest") private String publicationDigest;
@TableField("publication_id") private String publicationId;
@TableField("publication_title") private String publicationTitle;
@TableField("publication_content") private String publicationContent;
@TableField("publication_url") private String publicationUrl;
@TableField("publication_output_json") private String publicationOutputJson;
@TableField("supervision_timestamp") private String supervisionTimestamp;
@TableField("supervision_message") private String supervisionMessage;
@TableField("supervision_digest") private String supervisionDigest;
@TableField("regulator_signature") private String regulatorSignature;
@TableField("approval_id") private String approvalId;
@TableField("approval_time") private String approvalTime;
@TableField("record_number") private String recordNumber;
@TableField("supervision_output_json") private String supervisionOutputJson;
private String status;
}

@ -0,0 +1,8 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DonationRecordMapper extends BaseMapper<DonationRecordEntity> {
}

@ -0,0 +1,103 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.DonationPageResult;
import com.yau.digitalrmb.corporatewallet.application.DonationService;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationSignRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/donations")
@Tag(name = "模块六-3-企业公益捐赠", description = "捐赠企业发起、受赠机构公示、第三方监管审核三步流程")
public class DonationController {
@Resource private DonationService donationService;
@GetMapping("/page")
@Operation(summary = "查询公益捐赠三步流程页面数据")
public ApiResponse<DonationPageResult> page() {
JwtUser user = AuthContextHolder.get();
return success(donationService.page(InstitutionKeySubject.from(user), user.getUsername()));
}
@PostMapping("/concatenate")
@Operation(summary = "拼接捐赠原文")
public ApiResponse<DonationPageResult> concatenate(@Valid @RequestBody DonationCreateRequest request) {
JwtUser user = AuthContextHolder.get();
return success(donationService.concatenate(InstitutionKeySubject.from(user), user.getUsername(), request));
}
@PostMapping("/digest")
@Operation(summary = "生成捐赠原文SM3摘要")
public ApiResponse<DonationPageResult> digest() { return success(donationService.digest(subject())); }
@PostMapping("/sign")
@Operation(summary = "使用企业法人私钥对捐赠摘要进行SM2签名")
public ApiResponse<DonationPageResult> sign(@Valid @RequestBody DonationSignRequest request) {
return success(donationService.signDonation(subject(), request.getPrivateKey()));
}
@PostMapping("/output")
@Operation(summary = "输出并提交捐赠报文")
public ApiResponse<DonationPageResult> output() { return success(donationService.outputDonation(subject())); }
@PostMapping("/receipt/details")
@Operation(summary = "受赠机构查看到账捐赠明细")
public ApiResponse<DonationPageResult> receiptDetails() { return success(donationService.receiptDetails(subject())); }
@PostMapping("/publication/extract")
@Operation(summary = "提取资金公示拼接原文")
public ApiResponse<DonationPageResult> extractPublication() { return success(donationService.extractPublication(subject())); }
@PostMapping("/publication/digest")
@Operation(summary = "生成资金公示原文SM3摘要")
public ApiResponse<DonationPageResult> digestPublication() { return success(donationService.digestPublication(subject())); }
@PostMapping("/publication/publish")
@Operation(summary = "发布资金公示")
public ApiResponse<DonationPageResult> publish() { return success(donationService.publish(subject())); }
@PostMapping("/publication/output")
@Operation(summary = "输出资金公示报文")
public ApiResponse<DonationPageResult> outputPublication() { return success(donationService.outputPublication(subject())); }
@PostMapping("/supervision/extract")
@Operation(summary = "提取监管审核捐赠原文")
public ApiResponse<DonationPageResult> extractSupervision() { return success(donationService.extractSupervision(subject())); }
@PostMapping("/supervision/digest")
@Operation(summary = "生成监管审核原文SM3摘要")
public ApiResponse<DonationPageResult> digestSupervision() { return success(donationService.digestSupervision(subject())); }
@PostMapping("/supervision/sign")
@Operation(summary = "使用监管机构私钥对审核摘要进行SM2签名")
public ApiResponse<DonationPageResult> signSupervision(@Valid @RequestBody DonationSignRequest request) {
return success(donationService.signSupervision(subject(), request.getPrivateKey()));
}
@PostMapping("/supervision/output")
@Operation(summary = "输出监管审核备案报文")
public ApiResponse<DonationPageResult> outputSupervision() { return success(donationService.outputSupervision(subject())); }
private InstitutionKeySubject subject() {
return InstitutionKeySubject.from(AuthContextHolder.get());
}
private ApiResponse<DonationPageResult> success(DonationPageResult result) {
return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY));
}
}

@ -0,0 +1,25 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
@Data
@Schema(description = "拼接捐赠原文请求")
public class DonationCreateRequest {
@NotBlank @Size(max = 200) private String donorName;
@NotBlank @Size(max = 128) private String donorWalletId;
@NotBlank @Size(max = 200) private String recipientName;
@NotBlank @Size(max = 128) private String recipientWalletId;
@NotNull @DecimalMin(value = "0.01") @Digits(integer = 18, fraction = 2) private BigDecimal amount;
@NotBlank @Size(max = 200) private String projectName;
@NotBlank @Size(max = 500) private String memo;
@NotBlank @Pattern(regexp = "\\d{14}", message = "时间戳格式应为yyyyMMddHHmmss") private String timestamp;
}

@ -0,0 +1,13 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@Schema(description = "公益捐赠SM2签名请求")
public class DonationSignRequest {
@NotBlank(message = "私钥不能为空")
private String privateKey;
}

@ -0,0 +1,44 @@
-- 模块六公益捐赠三步流程增量建表脚本。
-- 已有环境升级时执行;全新环境由 schema.sql 自动创建。
CREATE TABLE IF NOT EXISTS donation_record (
id BIGINT PRIMARY KEY,
user_id VARCHAR(36) NOT NULL,
school_id BIGINT NOT NULL,
class_id BIGINT NOT NULL,
donation_id VARCHAR(64),
donor_name VARCHAR(200),
donor_wallet_id VARCHAR(128),
recipient_name VARCHAR(200),
recipient_wallet_id VARCHAR(128),
amount DECIMAL(20, 2),
project_name VARCHAR(200),
memo VARCHAR(500),
donation_timestamp VARCHAR(20),
donation_message TEXT,
donation_digest VARCHAR(128),
donor_signature TEXT,
donation_output_json TEXT,
publication_timestamp VARCHAR(20),
publication_message TEXT,
publication_digest VARCHAR(128),
publication_id VARCHAR(64),
publication_title VARCHAR(300),
publication_content VARCHAR(1000),
publication_url VARCHAR(500),
publication_output_json TEXT,
supervision_timestamp VARCHAR(20),
supervision_message TEXT,
supervision_digest VARCHAR(128),
regulator_signature TEXT,
approval_id VARCHAR(64),
approval_time VARCHAR(20),
record_number VARCHAR(64),
supervision_output_json TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
created_by VARCHAR(64) NOT NULL,
updated_by VARCHAR(64) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
INDEX idx_donation_subject (user_id, school_id, class_id, deleted, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-公益捐赠记录表';

@ -1269,6 +1269,50 @@ CREATE TABLE IF NOT EXISTS salary_batch (
deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记:false-未删除, true-已删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-工资发放批次表';
-- 模块六:企业使用数字人民币进行公益捐赠 - 发起、公示、监管审核全流程
CREATE TABLE IF NOT EXISTS donation_record (
id BIGINT PRIMARY KEY COMMENT '主键ID(雪花算法生成)',
user_id VARCHAR(36) NOT NULL COMMENT '用户ID',
school_id BIGINT NOT NULL COMMENT '学校ID',
class_id BIGINT NOT NULL COMMENT '班级ID',
donation_id VARCHAR(64) COMMENT '捐赠编号',
donor_name VARCHAR(200) COMMENT '捐赠企业',
donor_wallet_id VARCHAR(128) COMMENT '付款钱包ID',
recipient_name VARCHAR(200) COMMENT '受赠机构',
recipient_wallet_id VARCHAR(128) COMMENT '收款钱包ID',
amount DECIMAL(20, 2) COMMENT '捐赠金额',
project_name VARCHAR(200) COMMENT '项目名称',
memo VARCHAR(500) COMMENT '捐赠备注',
donation_timestamp VARCHAR(20) COMMENT '捐赠时间戳',
donation_message TEXT COMMENT '捐赠拼接原文',
donation_digest VARCHAR(128) COMMENT '捐赠SM3摘要',
donor_signature TEXT COMMENT '企业法人SM2签名',
donation_output_json TEXT COMMENT '捐赠提交报文',
publication_timestamp VARCHAR(20) COMMENT '公示时间戳',
publication_message TEXT COMMENT '公示拼接原文',
publication_digest VARCHAR(128) COMMENT '公示SM3摘要',
publication_id VARCHAR(64) COMMENT '公示编号',
publication_title VARCHAR(300) COMMENT '公示标题',
publication_content VARCHAR(1000) COMMENT '公示内容',
publication_url VARCHAR(500) COMMENT '公示网址',
publication_output_json TEXT COMMENT '资金公示报文',
supervision_timestamp VARCHAR(20) COMMENT '监管审核时间戳',
supervision_message TEXT COMMENT '监管审核原文',
supervision_digest VARCHAR(128) COMMENT '监管审核SM3摘要',
regulator_signature TEXT COMMENT '监管机构SM2签名',
approval_id VARCHAR(64) COMMENT '审批编号',
approval_time VARCHAR(20) COMMENT '审批时间',
record_number VARCHAR(64) COMMENT '备案编号',
supervision_output_json TEXT COMMENT '监管审核备案报文',
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '流程状态',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
created_by VARCHAR(64) NOT NULL,
updated_by VARCHAR(64) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
INDEX idx_donation_subject (user_id, school_id, class_id, deleted, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-公益捐赠记录表';
-- 步骤四商业银行端:生成合约信息与开通确认
CREATE TABLE IF NOT EXISTS smart_contract_generation (
id BIGINT PRIMARY KEY,

@ -1,6 +1,8 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationMapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationMapper;
@ -33,6 +35,7 @@ class CorporateInnovationTrainingResetServiceTest {
@Mock private CorporateWalletApplicationMapper corporateWalletMapper;
@Mock private RemoteWalletApplicationMapper remoteWalletMapper;
@Mock private SalaryBatchMapper salaryBatchMapper;
@Mock private DonationRecordMapper donationRecordMapper;
@Mock private StuModuleScoreDetailsMapper scoreDetailsMapper;
@Mock private UserStepProgressNewService stepProgressService;
@InjectMocks private CorporateInnovationTrainingResetService service;
@ -43,6 +46,7 @@ class CorporateInnovationTrainingResetServiceTest {
TableInfoHelper.initTableInfo(assistant, CorporateWalletApplicationEntity.class);
TableInfoHelper.initTableInfo(assistant, RemoteWalletApplicationEntity.class);
TableInfoHelper.initTableInfo(assistant, SalaryBatchEntity.class);
TableInfoHelper.initTableInfo(assistant, DonationRecordEntity.class);
TableInfoHelper.initTableInfo(assistant, StuModuleScoreDetailsEntity.class);
}
@ -51,12 +55,13 @@ class CorporateInnovationTrainingResetServiceTest {
when(corporateWalletMapper.update(any(), any())).thenReturn(2);
when(remoteWalletMapper.update(any(), any())).thenReturn(3);
when(salaryBatchMapper.update(any(), any())).thenReturn(4);
when(donationRecordMapper.update(any(), any())).thenReturn(5);
when(scoreDetailsMapper.update(any(), any())).thenReturn(1);
CorporateInnovationTrainingResetResult result = service.reset(SUBJECT);
assertThat(result.isReset()).isTrue();
assertThat(result.getDeletedRecordCount()).isEqualTo(10);
assertThat(result.getDeletedRecordCount()).isEqualTo(15);
verify(stepProgressService).reset("reset-user", "innovation-scene");
}
}

@ -0,0 +1,119 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DonationServiceTest {
private static final InstitutionKeySubject SUBJECT = new InstitutionKeySubject("u-1", 1L, 2L);
@Mock private DonationRecordMapper mapper;
@Mock private CorporateWalletApplicationRepository corporateWalletRepository;
@Mock private RemoteWalletApplicationRepository remoteWalletRepository;
@Mock private CorporateWalletKeyService keyService;
@Mock private InstitutionIdentityCryptography cryptography;
@Mock private SalaryScoreService scoreService;
@Spy private ObjectMapper objectMapper = new ObjectMapper();
@InjectMocks private DonationService service;
private final AtomicReference<DonationRecordEntity> latest = new AtomicReference<>();
@BeforeAll
static void initializeMybatisLambdaMetadata() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
DonationRecordEntity.class);
}
@BeforeEach
void setUp() {
CorporateWalletApplication donor = CorporateWalletApplication.create("u-1", 1L, 2L);
donor.fillForm("创新科技公司", "CREDIT-1", "张三", "10000000.00", "技术服务", "20260916090000");
donor.activateWallet("CORP_DONOR", "20260916091000");
RemoteWalletApplication recipient = RemoteWalletApplication.create("u-1", 1L, 2L);
recipient.fillForm("阳光公益基金", "CREDIT-2", "李四", "13900139000", "20260916092000");
recipient.activateWallet("CORP_RECIPIENT", "20260916093000");
when(corporateWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(donor));
when(remoteWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(recipient));
when(mapper.selectOne(any())).thenAnswer(invocation -> latest.get());
when(mapper.insert(any(DonationRecordEntity.class))).thenAnswer(invocation -> {
DonationRecordEntity record = invocation.getArgument(0);
record.setId(100L);
latest.set(record);
return 1;
});
when(mapper.updateById(any(DonationRecordEntity.class))).thenAnswer(invocation -> {
latest.set(invocation.getArgument(0));
return 1;
});
when(keyService.getOrCreatePrivateKey(any(), anyString(), anyString())).thenAnswer(invocation ->
"监管机构私钥".equals(invocation.getArgument(1)) ? "REGULATOR_KEY" : "DONOR_KEY");
when(cryptography.sm3(anyString())).thenAnswer(invocation -> "HASH_" + invocation.getArgument(0));
when(cryptography.sign(anyString(), anyString())).thenAnswer(invocation ->
"SIGN_" + invocation.getArgument(1));
}
@Test
void completesPrototypeDonationPublicationAndSupervisionFlow() {
DonationCreateRequest request = new DonationCreateRequest();
request.setDonorName("创新科技公司");
request.setDonorWalletId("CORP_DONOR");
request.setRecipientName("阳光公益基金");
request.setRecipientWalletId("CORP_RECIPIENT");
request.setAmount(new BigDecimal("100000.00"));
request.setProjectName("乡村教育支持计划");
request.setMemo("乡村教育支持计划捐款");
request.setTimestamp("20260916100000");
service.concatenate(SUBJECT, "张三", request);
service.digest(SUBJECT);
service.signDonation(SUBJECT, "DONOR_KEY");
service.outputDonation(SUBJECT);
service.receiptDetails(SUBJECT);
service.extractPublication(SUBJECT);
service.digestPublication(SUBJECT);
service.publish(SUBJECT);
service.outputPublication(SUBJECT);
service.extractSupervision(SUBJECT);
service.digestSupervision(SUBJECT);
service.signSupervision(SUBJECT, "REGULATOR_KEY");
DonationPageResult result = service.outputSupervision(SUBJECT);
assertThat(result.getStatus()).isEqualTo("ARCHIVED");
assertThat(result.getDonationOutputJson()).contains("DONATE_20260916_001", "已提交");
assertThat(result.getPublicationOutputJson()).contains("PUBLISHED", "阳光公益基金");
assertThat(result.getSupervisionOutputJson()).contains("APPROVED", "已备案");
verify(scoreService).markStepCompleted("u-1", 6);
verify(scoreService).markStepCompleted("u-1", 7);
verify(scoreService).markStepCompleted("u-1", 8);
}
}

@ -883,6 +883,24 @@ CREATE TABLE IF NOT EXISTS payment_order (
FOREIGN KEY (payee_wallet_id) REFERENCES digital_wallet(wallet_id)
);
CREATE TABLE IF NOT EXISTS donation_record (
id BIGINT PRIMARY KEY,
user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL,
donation_id VARCHAR(64), donor_name VARCHAR(200), donor_wallet_id VARCHAR(128),
recipient_name VARCHAR(200), recipient_wallet_id VARCHAR(128), amount DECIMAL(20,2),
project_name VARCHAR(200), memo VARCHAR(500), donation_timestamp VARCHAR(20),
donation_message CLOB, donation_digest VARCHAR(128), donor_signature CLOB, donation_output_json CLOB,
publication_timestamp VARCHAR(20), publication_message CLOB, publication_digest VARCHAR(128),
publication_id VARCHAR(64), publication_title VARCHAR(300), publication_content VARCHAR(1000),
publication_url VARCHAR(500), publication_output_json CLOB,
supervision_timestamp VARCHAR(20), supervision_message CLOB, supervision_digest VARCHAR(128),
regulator_signature CLOB, approval_id VARCHAR(64), approval_time VARCHAR(20),
record_number VARCHAR(64), supervision_output_json CLOB, status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL,
created_by VARCHAR(64) NOT NULL, updated_by VARCHAR(64) NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS training_task_step_progress (
user_id VARCHAR(36) NOT NULL,
module_code VARCHAR(64) NOT NULL,

Loading…
Cancel
Save