mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat(m2): add contract change versioning with CHANGING state
This commit is contained in:
+16
-1
@@ -3,6 +3,7 @@ package cn.craftlabs.platform.api.contracts;
|
|||||||
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachment;
|
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachment;
|
||||||
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachmentMapper;
|
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachmentMapper;
|
||||||
import cn.craftlabs.platform.api.service.ContractService;
|
import cn.craftlabs.platform.api.service.ContractService;
|
||||||
|
import cn.craftlabs.platform.api.service.ContractStatusTransitionService;
|
||||||
import cn.craftlabs.platform.api.web.dto.ContractCreateRequest;
|
import cn.craftlabs.platform.api.web.dto.ContractCreateRequest;
|
||||||
import cn.craftlabs.platform.api.web.dto.ContractLineRequest;
|
import cn.craftlabs.platform.api.web.dto.ContractLineRequest;
|
||||||
import cn.craftlabs.platform.api.web.dto.ContractLineResponse;
|
import cn.craftlabs.platform.api.web.dto.ContractLineResponse;
|
||||||
@@ -41,10 +42,12 @@ public class ContractController {
|
|||||||
|
|
||||||
private final ContractService contractService;
|
private final ContractService contractService;
|
||||||
private final PlatformContractAttachmentMapper attachmentMapper;
|
private final PlatformContractAttachmentMapper attachmentMapper;
|
||||||
|
private final ContractStatusTransitionService contractStatusTransitionService;
|
||||||
|
|
||||||
public ContractController(ContractService contractService, PlatformContractAttachmentMapper attachmentMapper) {
|
public ContractController(ContractService contractService, PlatformContractAttachmentMapper attachmentMapper, ContractStatusTransitionService contractStatusTransitionService) {
|
||||||
this.contractService = contractService;
|
this.contractService = contractService;
|
||||||
this.attachmentMapper = attachmentMapper;
|
this.attachmentMapper = attachmentMapper;
|
||||||
|
this.contractStatusTransitionService = contractStatusTransitionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -141,4 +144,16 @@ public class ContractController {
|
|||||||
.orderByDesc(PlatformContractAttachment::getCreatedAt);
|
.orderByDesc(PlatformContractAttachment::getCreatedAt);
|
||||||
return ResponseEntity.ok(attachmentMapper.selectList(query));
|
return ResponseEntity.ok(attachmentMapper.selectList(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/changes")
|
||||||
|
public ResponseEntity<Void> initiateChange(@PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||||
|
contractStatusTransitionService.initiateChange(id, body.getOrDefault("reason", ""));
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/changes/complete")
|
||||||
|
public ResponseEntity<Void> completeChange(@PathVariable Long id) {
|
||||||
|
contractStatusTransitionService.completeChange(id);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
package cn.craftlabs.platform.api.persistence.contract;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@TableName("platform_contract_change")
|
||||||
|
public class PlatformContractChange {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@TableField("contract_id")
|
||||||
|
private Long contractId;
|
||||||
|
|
||||||
|
private Integer version;
|
||||||
|
|
||||||
|
@TableField("change_type")
|
||||||
|
private String changeType;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
@TableField("change_summary")
|
||||||
|
private String changeSummary;
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@TableField("created_by")
|
||||||
|
private String createdBy;
|
||||||
|
|
||||||
|
@TableField("created_at")
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@TableField("completed_at")
|
||||||
|
private OffsetDateTime completedAt;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getContractId() {
|
||||||
|
return contractId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContractId(Long contractId) {
|
||||||
|
this.contractId = contractId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(Integer version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChangeType() {
|
||||||
|
return changeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChangeType(String changeType) {
|
||||||
|
this.changeType = changeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReason(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChangeSummary() {
|
||||||
|
return changeSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChangeSummary(String changeSummary) {
|
||||||
|
this.changeSummary = changeSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCreatedBy() {
|
||||||
|
return createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedBy(String createdBy) {
|
||||||
|
this.createdBy = createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getCompletedAt() {
|
||||||
|
return completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCompletedAt(OffsetDateTime completedAt) {
|
||||||
|
this.completedAt = completedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package cn.craftlabs.platform.api.persistence.contract;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PlatformContractChangeMapper extends BaseMapper<PlatformContractChange> {
|
||||||
|
}
|
||||||
+67
@@ -1,8 +1,15 @@
|
|||||||
package cn.craftlabs.platform.api.service;
|
package cn.craftlabs.platform.api.service;
|
||||||
|
|
||||||
import cn.craftlabs.platform.api.domain.ContractStatus;
|
import cn.craftlabs.platform.api.domain.ContractStatus;
|
||||||
|
import cn.craftlabs.platform.api.persistence.contract.PlatformContract;
|
||||||
|
import cn.craftlabs.platform.api.persistence.contract.PlatformContractChange;
|
||||||
|
import cn.craftlabs.platform.api.persistence.contract.PlatformContractChangeMapper;
|
||||||
|
import cn.craftlabs.platform.api.persistence.contract.PlatformContractMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,6 +20,14 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
@Service
|
@Service
|
||||||
public class ContractStatusTransitionService {
|
public class ContractStatusTransitionService {
|
||||||
|
|
||||||
|
private final PlatformContractMapper contractMapper;
|
||||||
|
private final PlatformContractChangeMapper changeMapper;
|
||||||
|
|
||||||
|
public ContractStatusTransitionService(PlatformContractMapper contractMapper, PlatformContractChangeMapper changeMapper) {
|
||||||
|
this.contractMapper = contractMapper;
|
||||||
|
this.changeMapper = changeMapper;
|
||||||
|
}
|
||||||
|
|
||||||
public void requireTransition(ContractStatus from, ContractStatus to) {
|
public void requireTransition(ContractStatus from, ContractStatus to) {
|
||||||
if (from == to) {
|
if (from == to) {
|
||||||
return;
|
return;
|
||||||
@@ -39,4 +54,56 @@ public class ContractStatusTransitionService {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void initiateChange(Long contractId, String reason) {
|
||||||
|
PlatformContract c = contractMapper.selectById(contractId);
|
||||||
|
if (c == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "contract not found");
|
||||||
|
}
|
||||||
|
requireTransition(ContractStatus.valueOf(c.getStatus()), ContractStatus.CHANGING);
|
||||||
|
|
||||||
|
var query = Wrappers.lambdaQuery(PlatformContractChange.class)
|
||||||
|
.eq(PlatformContractChange::getContractId, contractId)
|
||||||
|
.orderByDesc(PlatformContractChange::getVersion);
|
||||||
|
PlatformContractChange latest = changeMapper.selectOne(query);
|
||||||
|
int nextVersion = latest != null ? latest.getVersion() + 1 : 1;
|
||||||
|
|
||||||
|
PlatformContractChange change = new PlatformContractChange();
|
||||||
|
change.setContractId(contractId);
|
||||||
|
change.setVersion(nextVersion);
|
||||||
|
change.setChangeType("AMENDMENT");
|
||||||
|
change.setReason(reason);
|
||||||
|
change.setStatus("DRAFT");
|
||||||
|
change.setCreatedAt(OffsetDateTime.now());
|
||||||
|
changeMapper.insert(change);
|
||||||
|
|
||||||
|
c.setStatus(ContractStatus.CHANGING.name());
|
||||||
|
c.setUpdatedAt(OffsetDateTime.now());
|
||||||
|
contractMapper.updateById(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void completeChange(Long contractId) {
|
||||||
|
PlatformContract c = contractMapper.selectById(contractId);
|
||||||
|
if (c == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "contract not found");
|
||||||
|
}
|
||||||
|
requireTransition(ContractStatus.valueOf(c.getStatus()), ContractStatus.EFFECTIVE);
|
||||||
|
|
||||||
|
var query = Wrappers.lambdaQuery(PlatformContractChange.class)
|
||||||
|
.eq(PlatformContractChange::getContractId, contractId)
|
||||||
|
.eq(PlatformContractChange::getStatus, "DRAFT")
|
||||||
|
.orderByDesc(PlatformContractChange::getVersion);
|
||||||
|
PlatformContractChange change = changeMapper.selectOne(query);
|
||||||
|
if (change != null) {
|
||||||
|
change.setStatus("COMPLETED");
|
||||||
|
change.setCompletedAt(OffsetDateTime.now());
|
||||||
|
changeMapper.updateById(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setStatus(ContractStatus.EFFECTIVE.name());
|
||||||
|
c.setUpdatedAt(OffsetDateTime.now());
|
||||||
|
contractMapper.updateById(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
-- V11__contract_changes.sql
|
||||||
|
CREATE TABLE platform_contract_change (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
contract_id BIGINT NOT NULL REFERENCES platform_contract(id),
|
||||||
|
version INT NOT NULL DEFAULT 1,
|
||||||
|
change_type VARCHAR(64) NOT NULL DEFAULT 'AMENDMENT',
|
||||||
|
reason TEXT,
|
||||||
|
change_summary TEXT,
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
|
||||||
|
created_by VARCHAR(256),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_contract_change_contract ON platform_contract_change(contract_id);
|
||||||
@@ -379,3 +379,11 @@ export function getCallbackStats(params) {
|
|||||||
export function getProjectHealth() {
|
export function getProjectHealth() {
|
||||||
return axios.get('/api/v1/reports/project-health');
|
return axios.get('/api/v1/reports/project-health');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// —— I11 合同变更版本 ——————————————————————————
|
||||||
|
export function initiateContractChange(id, body) {
|
||||||
|
return axios.post(`/api/v1/contracts/${id}/changes`, body);
|
||||||
|
}
|
||||||
|
export function completeContractChange(id) {
|
||||||
|
return axios.post(`/api/v1/contracts/${id}/changes/complete`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ import {
|
|||||||
listProjects,
|
listProjects,
|
||||||
uploadContractAttachment,
|
uploadContractAttachment,
|
||||||
listContractAttachments,
|
listContractAttachments,
|
||||||
|
initiateContractChange,
|
||||||
|
completeContractChange,
|
||||||
} from "../api/platform";
|
} from "../api/platform";
|
||||||
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||||
|
|
||||||
@@ -176,6 +178,7 @@ const contractId = computed(() => route.params.id);
|
|||||||
|
|
||||||
const isDraft = computed(() => String(contract.value?.status ?? "").toUpperCase() === "DRAFT");
|
const isDraft = computed(() => String(contract.value?.status ?? "").toUpperCase() === "DRAFT");
|
||||||
const isEffective = computed(() => String(contract.value?.status ?? "").toUpperCase() === "EFFECTIVE");
|
const isEffective = computed(() => String(contract.value?.status ?? "").toUpperCase() === "EFFECTIVE");
|
||||||
|
const isChanging = computed(() => String(contract.value?.status ?? "").toUpperCase() === "CHANGING");
|
||||||
|
|
||||||
const lineRows = computed(() => {
|
const lineRows = computed(() => {
|
||||||
const c = contract.value;
|
const c = contract.value;
|
||||||
@@ -475,7 +478,13 @@ function onTransition(btn) {
|
|||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
transitionLoading.value = btn.status;
|
transitionLoading.value = btn.status;
|
||||||
try {
|
try {
|
||||||
await patchContractStatus(id, { status: btn.status });
|
if (btn.status === "CHANGING") {
|
||||||
|
await initiateContractChange(id, { reason: "" });
|
||||||
|
} else if (btn.status === "EFFECTIVE" && isChanging.value) {
|
||||||
|
await completeContractChange(id);
|
||||||
|
} else {
|
||||||
|
await patchContractStatus(id, { status: btn.status });
|
||||||
|
}
|
||||||
ElMessage.success("状态已更新");
|
ElMessage.success("状态已更新");
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user