feat: add system params persistence and delivery gate enforcement

V25 migration creates platform_system_param table. SystemParamController replaces localStorage MVP with backend persistence. LicenseSnService.create now checks deliveryGateEnabled flag and blocks SN creation when gate is on but no deliveries completed.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-27 08:37:02 +08:00
parent 8c167d4909
commit 5d50d2819b
6 changed files with 138 additions and 22 deletions
@@ -0,0 +1,50 @@
package cn.craftlabs.platform.api.config;
import cn.craftlabs.platform.api.persistence.system.PlatformSystemParam;
import cn.craftlabs.platform.api.persistence.system.PlatformSystemParamMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/system-params")
public class SystemParamController {
private final PlatformSystemParamMapper paramMapper;
public SystemParamController(PlatformSystemParamMapper paramMapper) {
this.paramMapper = paramMapper;
}
@GetMapping
public Map<String, String> list() {
List<PlatformSystemParam> params = paramMapper.selectList(Wrappers.lambdaQuery());
return params.stream().collect(Collectors.toMap(
PlatformSystemParam::getParamKey, PlatformSystemParam::getParamValue));
}
@PutMapping
public ResponseEntity<Void> update(@RequestBody Map<String, String> body) {
for (var entry : body.entrySet()) {
PlatformSystemParam param = paramMapper.selectById(entry.getKey());
if (param == null) {
param = new PlatformSystemParam();
param.setParamKey(entry.getKey());
}
param.setParamValue(entry.getValue());
param.setUpdatedAt(OffsetDateTime.now(ZoneOffset.UTC));
if (param.getParamKey() != null && paramMapper.selectById(param.getParamKey()) != null) {
paramMapper.updateById(param);
} else {
paramMapper.insert(param);
}
}
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,29 @@
package cn.craftlabs.platform.api.persistence.system;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.OffsetDateTime;
@TableName("platform_system_param")
public class PlatformSystemParam {
@TableId
@TableField("param_key")
private String paramKey;
@TableField("param_value")
private String paramValue;
@TableField("updated_at")
private OffsetDateTime updatedAt;
public String getParamKey() { return paramKey; }
public void setParamKey(String paramKey) { this.paramKey = paramKey; }
public String getParamValue() { return paramValue; }
public void setParamValue(String paramValue) { this.paramValue = paramValue; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,8 @@
package cn.craftlabs.platform.api.persistence.system;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PlatformSystemParamMapper extends BaseMapper<PlatformSystemParam> {
}
@@ -9,15 +9,23 @@ import cn.craftlabs.platform.api.persistence.contract.PlatformContractLineMapper
import cn.craftlabs.platform.api.persistence.contract.PlatformContractMapper; import cn.craftlabs.platform.api.persistence.contract.PlatformContractMapper;
import cn.craftlabs.platform.api.persistence.delivery.PlatformDeliveryBatch; import cn.craftlabs.platform.api.persistence.delivery.PlatformDeliveryBatch;
import cn.craftlabs.platform.api.persistence.delivery.PlatformDeliveryBatchMapper; import cn.craftlabs.platform.api.persistence.delivery.PlatformDeliveryBatchMapper;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseActivation;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseActivationMapper;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseKey;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseKeyMapper;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseMapper;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseSn; import cn.craftlabs.platform.api.persistence.license.PlatformLicenseSn;
import cn.craftlabs.platform.api.persistence.license.PlatformLicenseSnMapper; import cn.craftlabs.platform.api.persistence.license.PlatformLicenseSnMapper;
import cn.craftlabs.platform.api.persistence.project.PlatformProject;
import cn.craftlabs.platform.api.persistence.project.PlatformProjectMapper; import cn.craftlabs.platform.api.persistence.project.PlatformProjectMapper;
import cn.craftlabs.platform.api.persistence.system.PlatformSystemParamMapper;
import cn.craftlabs.platform.api.web.dto.LicenseSnCreateRequest; import cn.craftlabs.platform.api.web.dto.LicenseSnCreateRequest;
import cn.craftlabs.platform.api.web.dto.LicenseSnResponse; import cn.craftlabs.platform.api.web.dto.LicenseSnResponse;
import cn.craftlabs.platform.api.web.dto.LicenseSnStatusPatchRequest; import cn.craftlabs.platform.api.web.dto.LicenseSnStatusPatchRequest;
import cn.craftlabs.platform.api.web.dto.LicenseSnUpdateRequest; import cn.craftlabs.platform.api.web.dto.LicenseSnUpdateRequest;
import cn.craftlabs.platform.api.web.dto.PageResponse; import cn.craftlabs.platform.api.web.dto.PageResponse;
import cn.craftlabs.platform.api.web.dto.SnBatchImportRequest; import cn.craftlabs.platform.api.web.dto.SnBatchImportRequest;
import cn.craftlabs.platform.api.web.dto.SnBatchImportRequest;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -44,6 +52,7 @@ public class LicenseSnService {
private final PlatformContractLineMapper contractLineMapper; private final PlatformContractLineMapper contractLineMapper;
private final PlatformContractMapper contractMapper; private final PlatformContractMapper contractMapper;
private final PlatformDeliveryBatchMapper deliveryBatchMapper; private final PlatformDeliveryBatchMapper deliveryBatchMapper;
private final PlatformSystemParamMapper systemParamMapper;
private final AuditService auditService; private final AuditService auditService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -53,6 +62,7 @@ public class LicenseSnService {
PlatformContractLineMapper contractLineMapper, PlatformContractLineMapper contractLineMapper,
PlatformContractMapper contractMapper, PlatformContractMapper contractMapper,
PlatformDeliveryBatchMapper deliveryBatchMapper, PlatformDeliveryBatchMapper deliveryBatchMapper,
PlatformSystemParamMapper systemParamMapper,
AuditService auditService, AuditService auditService,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
this.licenseSnMapper = licenseSnMapper; this.licenseSnMapper = licenseSnMapper;
@@ -60,6 +70,7 @@ public class LicenseSnService {
this.contractLineMapper = contractLineMapper; this.contractLineMapper = contractLineMapper;
this.contractMapper = contractMapper; this.contractMapper = contractMapper;
this.deliveryBatchMapper = deliveryBatchMapper; this.deliveryBatchMapper = deliveryBatchMapper;
this.systemParamMapper = systemParamMapper;
this.auditService = auditService; this.auditService = auditService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }
@@ -87,13 +98,22 @@ public class LicenseSnService {
requireProject(projectId); requireProject(projectId);
} }
if (request.getProjectId() != null) { if (request.getProjectId() != null) {
var deliveryQuery = com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery(PlatformDeliveryBatch.class) var gateParam = systemParamMapper.selectById("deliveryGateEnabled");
boolean gateEnabled = gateParam != null && "true".equals(gateParam.getParamValue());
if (gateEnabled) {
var batchQuery = com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery(PlatformDeliveryBatch.class)
.eq(PlatformDeliveryBatch::getProjectId, request.getProjectId());
long totalBatches = deliveryBatchMapper.selectCount(batchQuery);
if (totalBatches > 0) {
var deliveredQuery = com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery(PlatformDeliveryBatch.class)
.eq(PlatformDeliveryBatch::getProjectId, request.getProjectId()) .eq(PlatformDeliveryBatch::getProjectId, request.getProjectId())
.eq(PlatformDeliveryBatch::getStatus, "DELIVERED"); .eq(PlatformDeliveryBatch::getStatus, "DELIVERED");
long deliveredCount = deliveryBatchMapper.selectCount(deliveryQuery); long deliveredCount = deliveryBatchMapper.selectCount(deliveredQuery);
if (deliveredCount == 0) { if (deliveredCount == 0) {
// If project has batches but none delivered, warn (not block for MVP) throw new ResponseStatusException(HttpStatus.FORBIDDEN,
// This is a soft check - can be made strict later "交付闸门已启用: 该项目下的交付批次尚未标记为已交付");
}
}
} }
} }
if (existsSnCode(code)) { if (existsSnCode(code)) {
@@ -0,0 +1,15 @@
-- V25__system_params.sql
-- 系统参数持久化(M11-F20),替代前端 localStorage MVP
CREATE TABLE IF NOT EXISTS platform_system_param (
param_key VARCHAR(64) PRIMARY KEY,
param_value VARCHAR(1024) NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO platform_system_param (param_key, param_value) VALUES
('orphanSnStrictValidation', 'true'),
('deliveryGateEnabled', 'true'),
('sessionTimeoutMinutes', '60'),
('passwordMinLength', '6')
ON CONFLICT (param_key) DO NOTHING;
@@ -1,31 +1,25 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import axios from 'axios'
const params = ref({ const params = ref({})
orphanSnStrictValidation: true,
deliveryGateEnabled: true,
sessionTimeoutMinutes: 60,
passwordMinLength: 6,
})
const loading = ref(false) const loading = ref(false)
async function loadParams() { async function loadParams() {
try { try {
const stored = localStorage.getItem('systemParams') const { data } = await axios.get('/api/v1/system-params')
if (stored) { params.value = { ...data }
params.value = { ...params.value, ...JSON.parse(stored) }
}
} catch { } catch {
ElMessage.error('加载系统参数失败')
} }
} }
async function saveParams() { async function saveParams() {
loading.value = true loading.value = true
try { try {
localStorage.setItem('systemParams', JSON.stringify(params.value)) await axios.put('/api/v1/system-params', params.value)
ElMessage.success('参数已保存MVP: 存储于浏览器本地)') ElMessage.success('参数已保存')
} catch (e) { } catch (e) {
ElMessage.error('保存失败') ElMessage.error('保存失败')
} finally { } finally {
@@ -42,10 +36,10 @@ onMounted(loadParams)
<el-card shadow="never" style="margin-top:16px;max-width:560px"> <el-card shadow="never" style="margin-top:16px;max-width:560px">
<el-form label-width="200px" label-position="left"> <el-form label-width="200px" label-position="left">
<el-form-item label="孤儿 SN 严格校验"> <el-form-item label="孤儿 SN 严格校验">
<el-switch v-model="params.orphanSnStrictValidation" /> <el-switch v-model="params.orphanSnStrictValidation" active-value="true" inactive-value="false" />
</el-form-item> </el-form-item>
<el-form-item label="交付闸门启用"> <el-form-item label="交付闸门启用">
<el-switch v-model="params.deliveryGateEnabled" /> <el-switch v-model="params.deliveryGateEnabled" active-value="true" inactive-value="false" />
</el-form-item> </el-form-item>
<el-form-item label="会话超时(分钟)"> <el-form-item label="会话超时(分钟)">
<el-input-number v-model="params.sessionTimeoutMinutes" :min="5" :max="1440" /> <el-input-number v-model="params.sessionTimeoutMinutes" :min="5" :max="1440" />