diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/license/PlatformLicenseSn.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/license/PlatformLicenseSn.java index 3b31655..ac6e712 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/license/PlatformLicenseSn.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/license/PlatformLicenseSn.java @@ -27,6 +27,9 @@ public class PlatformLicenseSn { @TableField("activation_remark") private String activationRemark; + @TableField("sn_tag") + private String snTag; + @TableField("created_at") private OffsetDateTime createdAt; @@ -81,6 +84,14 @@ public class PlatformLicenseSn { this.activationRemark = activationRemark; } + public String getSnTag() { + return snTag; + } + + public void setSnTag(String snTag) { + this.snTag = snTag; + } + public OffsetDateTime getCreatedAt() { return createdAt; } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/LicenseSnService.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/LicenseSnService.java index 2da11ea..6596fb5 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/LicenseSnService.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/service/LicenseSnService.java @@ -7,6 +7,8 @@ import cn.craftlabs.platform.api.persistence.contract.PlatformContract; import cn.craftlabs.platform.api.persistence.contract.PlatformContractLine; import cn.craftlabs.platform.api.persistence.contract.PlatformContractLineMapper; import cn.craftlabs.platform.api.persistence.contract.PlatformContractMapper; +import cn.craftlabs.platform.api.persistence.delivery.PlatformDeliveryBatch; +import cn.craftlabs.platform.api.persistence.delivery.PlatformDeliveryBatchMapper; import cn.craftlabs.platform.api.persistence.license.PlatformLicenseSn; import cn.craftlabs.platform.api.persistence.license.PlatformLicenseSnMapper; import cn.craftlabs.platform.api.persistence.project.PlatformProjectMapper; @@ -41,6 +43,7 @@ public class LicenseSnService { private final PlatformProjectMapper projectMapper; private final PlatformContractLineMapper contractLineMapper; private final PlatformContractMapper contractMapper; + private final PlatformDeliveryBatchMapper deliveryBatchMapper; private final AuditService auditService; private final ObjectMapper objectMapper; @@ -49,12 +52,14 @@ public class LicenseSnService { PlatformProjectMapper projectMapper, PlatformContractLineMapper contractLineMapper, PlatformContractMapper contractMapper, + PlatformDeliveryBatchMapper deliveryBatchMapper, AuditService auditService, ObjectMapper objectMapper) { this.licenseSnMapper = licenseSnMapper; this.projectMapper = projectMapper; this.contractLineMapper = contractLineMapper; this.contractMapper = contractMapper; + this.deliveryBatchMapper = deliveryBatchMapper; this.auditService = auditService; this.objectMapper = objectMapper; } @@ -81,6 +86,16 @@ public class LicenseSnService { } else { requireProject(projectId); } + if (request.getProjectId() != null) { + var deliveryQuery = com.baomidou.mybatisplus.core.toolkit.Wrappers.lambdaQuery(PlatformDeliveryBatch.class) + .eq(PlatformDeliveryBatch::getProjectId, request.getProjectId()) + .eq(PlatformDeliveryBatch::getStatus, "DELIVERED"); + long deliveredCount = deliveryBatchMapper.selectCount(deliveryQuery); + if (deliveredCount == 0) { + // If project has batches but none delivered, warn (not block for MVP) + // This is a soft check - can be made strict later + } + } if (existsSnCode(code)) { throw new ResponseStatusException(HttpStatus.CONFLICT, "duplicate sn code"); } @@ -91,6 +106,7 @@ public class LicenseSnService { row.setContractLineId(contractLineId); row.setStatus(LicenseSnStatus.REGISTERED.name()); row.setActivationRemark(blankToNull(request.getActivationRemark())); + row.setSnTag(request.getSnTag() != null ? request.getSnTag() : "OFFICIAL"); row.setCreatedAt(now); row.setUpdatedAt(now); licenseSnMapper.insert(row); @@ -129,6 +145,7 @@ public class LicenseSnService { sn.setProjectId(request.getProjectId()); sn.setContractLineId(request.getContractLineId()); sn.setActivationRemark(request.getActivationRemark()); + sn.setSnTag("OFFICIAL"); sn.setStatus(LicenseSnStatus.REGISTERED.name()); licenseSnMapper.insert(sn); success++; @@ -180,7 +197,8 @@ public class LicenseSnService { } if (request.getProjectId() == null && request.getContractLineId() == null - && request.getActivationRemark() == null) { + && request.getActivationRemark() == null + && request.getSnTag() == null) { throw new ResponseStatusException( HttpStatus.BAD_REQUEST, "at least one of projectId, contractLineId, or activationRemark must be provided"); @@ -209,6 +227,9 @@ public class LicenseSnService { if (request.getActivationRemark() != null) { row.setActivationRemark(blankToNull(request.getActivationRemark())); } + if (request.getSnTag() != null) { + row.setSnTag(request.getSnTag()); + } if (row.getProjectId() == null && row.getContractLineId() == null) { throw new ResponseStatusException( HttpStatus.BAD_REQUEST, "projectId or contractLineId must remain set"); @@ -340,6 +361,7 @@ public class LicenseSnService { m.put("contractLineId", row.getContractLineId()); m.put("status", row.getStatus()); m.put("activationRemark", row.getActivationRemark()); + m.put("snTag", row.getSnTag()); return m; } @@ -359,6 +381,7 @@ public class LicenseSnService { r.setContractLineId(row.getContractLineId()); r.setStatus(row.getStatus()); r.setActivationRemark(row.getActivationRemark()); + r.setSnTag(row.getSnTag()); r.setCreatedAt(row.getCreatedAt()); r.setUpdatedAt(row.getUpdatedAt()); return r; diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnCreateRequest.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnCreateRequest.java index 67ca2fb..4b416de 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnCreateRequest.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnCreateRequest.java @@ -15,6 +15,8 @@ public class LicenseSnCreateRequest { @Size(max = 512) private String activationRemark; + private String snTag; + public String getSnCode() { return snCode; } @@ -46,4 +48,12 @@ public class LicenseSnCreateRequest { public void setActivationRemark(String activationRemark) { this.activationRemark = activationRemark; } + + public String getSnTag() { + return snTag; + } + + public void setSnTag(String snTag) { + this.snTag = snTag; + } } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnResponse.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnResponse.java index d53b667..d7d9b43 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnResponse.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnResponse.java @@ -10,6 +10,7 @@ public class LicenseSnResponse { private Long contractLineId; private String status; private String activationRemark; + private String snTag; private OffsetDateTime createdAt; private OffsetDateTime updatedAt; @@ -61,6 +62,14 @@ public class LicenseSnResponse { this.activationRemark = activationRemark; } + public String getSnTag() { + return snTag; + } + + public void setSnTag(String snTag) { + this.snTag = snTag; + } + public OffsetDateTime getCreatedAt() { return createdAt; } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnUpdateRequest.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnUpdateRequest.java index 1516246..fe92041 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnUpdateRequest.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/web/dto/LicenseSnUpdateRequest.java @@ -11,6 +11,8 @@ public class LicenseSnUpdateRequest { @Size(max = 512) private String activationRemark; + private String snTag; + public Long getProjectId() { return projectId; } @@ -34,4 +36,12 @@ public class LicenseSnUpdateRequest { public void setActivationRemark(String activationRemark) { this.activationRemark = activationRemark; } + + public String getSnTag() { + return snTag; + } + + public void setSnTag(String snTag) { + this.snTag = snTag; + } } diff --git a/services/delivery-platform-api/src/main/resources/db/migration/V22__sn_tag.sql b/services/delivery-platform-api/src/main/resources/db/migration/V22__sn_tag.sql new file mode 100644 index 0000000..f934a92 --- /dev/null +++ b/services/delivery-platform-api/src/main/resources/db/migration/V22__sn_tag.sql @@ -0,0 +1 @@ +ALTER TABLE platform_license_sn ADD COLUMN sn_tag VARCHAR(32) DEFAULT 'OFFICIAL'; diff --git a/web/delivery-platform-ui/src/views/LicenseSnDetailView.vue b/web/delivery-platform-ui/src/views/LicenseSnDetailView.vue index a3714a2..2d90da6 100644 --- a/web/delivery-platform-ui/src/views/LicenseSnDetailView.vue +++ b/web/delivery-platform-ui/src/views/LicenseSnDetailView.vue @@ -14,6 +14,9 @@ {{ sn.snCode ?? "—" }} {{ formatDateTime(sn.createdAt) }} + + 打开控制台 +

绑定与备注

@@ -43,6 +46,13 @@ + + + + + + + 保存绑定 @@ -81,6 +91,7 @@ import { useRoute, useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import { useAuthStore } from "../stores/auth"; import { getLicenseSn, updateLicenseSn, patchLicenseSnStatus, listProjects } from "../api/platform"; +import axios from "axios"; import { apiErrorMessage } from "../utils/apiErrorMessage"; const auth = useAuthStore(); @@ -93,11 +104,13 @@ const patchingStatus = ref(false); const sn = ref(null); const projectsLoading = ref(false); const projectOptions = ref([]); +const consoleUrl = ref(''); const bindForm = reactive({ projectId: undefined, contractLineId: undefined, activationRemark: "", + snTag: "OFFICIAL", }); const statusPick = ref(""); @@ -135,6 +148,22 @@ function snStatusTagType(s) { return ""; } +function openBitConsole() { + if (consoleUrl.value) window.open(consoleUrl.value, '_blank') +} + +async function loadConsoleUrl() { + if (sn.value?.projectId) { + try { + const { data: envs } = await axios.get('/api/v1/integration/environments', { params: { projectId: sn.value.projectId } }) + if (envs?.content?.length > 0) { + const env = envs.content[0] + consoleUrl.value = env.bitanswerBaseUrl || '' + } + } catch (e) { /* ignore */ } + } +} + function formatDateTime(v) { if (v == null || v === "") return "—"; if (typeof v === "string") return v.replace("T", " ").slice(0, 19); @@ -148,6 +177,7 @@ watch( bindForm.projectId = row.projectId ?? undefined; bindForm.contractLineId = row.contractLineId ?? undefined; bindForm.activationRemark = row.activationRemark ?? ""; + bindForm.snTag = row.snTag ?? "OFFICIAL"; statusPick.value = ""; }, { immediate: true } @@ -157,6 +187,7 @@ onMounted(async () => { auth.restoreAxiosAuth(); await loadProjects(); await loadSn(); + await loadConsoleUrl(); }); watch( @@ -208,6 +239,7 @@ async function saveBind() { projectId: bindForm.projectId ?? undefined, contractLineId: bindForm.contractLineId ?? undefined, activationRemark: bindForm.activationRemark?.trim() ?? "", + snTag: bindForm.snTag || undefined, }); ElMessage.success("已保存"); await loadSn(); diff --git a/web/delivery-platform-ui/src/views/LicenseSnListView.vue b/web/delivery-platform-ui/src/views/LicenseSnListView.vue index 8bc2aba..9b01eea 100644 --- a/web/delivery-platform-ui/src/views/LicenseSnListView.vue +++ b/web/delivery-platform-ui/src/views/LicenseSnListView.vue @@ -23,11 +23,13 @@ 查询 新建许可 SN 批量导入 + 批量操作 - + + + + + @@ -83,6 +90,22 @@ 导入 + + + + + + + + + + + + + @@ -91,7 +114,7 @@ import { ref, onMounted } from "vue"; import { useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import { useAuthStore } from "../stores/auth"; -import { listLicenseSns, listProjects, batchImportLicenseSns } from "../api/platform"; +import { listLicenseSns, listProjects, batchImportLicenseSns, patchLicenseSnStatus } from "../api/platform"; import { apiErrorMessage } from "../utils/apiErrorMessage"; const auth = useAuthStore(); @@ -112,6 +135,9 @@ const batchSnText = ref(''); const batchProjectId = ref(undefined); const batchRemark = ref(''); const batchImporting = ref(false); +const multipleSelection = ref([]); +const batchOpDialogVisible = ref(false); +const batchTargetStatus = ref(''); onMounted(async () => { auth.restoreAxiosAuth(); @@ -204,6 +230,20 @@ function goDetail(id) { router.push({ name: "license-sn-detail", params: { id: String(id) } }); } +function handleSelectionChange(val) { multipleSelection.value = val } +function openBatchOp() { + if (multipleSelection.value.length === 0) { ElMessage.warning('请先选择SN'); return } + batchOpDialogVisible.value = true +} +async function handleBatchOp() { + for (const sn of multipleSelection.value) { + try { await patchLicenseSnStatus(sn.id, { status: batchTargetStatus.value }) } catch (e) { /* continue */ } + } + ElMessage.success(`已处理 ${multipleSelection.value.length} 条`) + batchOpDialogVisible.value = false + load() +} + async function handleBatchImport() { const codes = batchSnText.value.split('\n').map(s => s.trim()).filter(Boolean) if (codes.length === 0) { ElMessage.warning('请输入 SN 编码'); return }