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.PlatformContractAttachmentMapper;
|
||||
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.ContractLineRequest;
|
||||
import cn.craftlabs.platform.api.web.dto.ContractLineResponse;
|
||||
@@ -41,10 +42,12 @@ public class ContractController {
|
||||
|
||||
private final ContractService contractService;
|
||||
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.attachmentMapper = attachmentMapper;
|
||||
this.contractStatusTransitionService = contractStatusTransitionService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -141,4 +144,16 @@ public class ContractController {
|
||||
.orderByDesc(PlatformContractAttachment::getCreatedAt);
|
||||
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;
|
||||
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
@@ -13,6 +20,14 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
@Service
|
||||
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) {
|
||||
if (from == to) {
|
||||
return;
|
||||
@@ -39,4 +54,56 @@ public class ContractStatusTransitionService {
|
||||
}
|
||||
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() {
|
||||
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,
|
||||
uploadContractAttachment,
|
||||
listContractAttachments,
|
||||
initiateContractChange,
|
||||
completeContractChange,
|
||||
} from "../api/platform";
|
||||
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 isEffective = computed(() => String(contract.value?.status ?? "").toUpperCase() === "EFFECTIVE");
|
||||
const isChanging = computed(() => String(contract.value?.status ?? "").toUpperCase() === "CHANGING");
|
||||
|
||||
const lineRows = computed(() => {
|
||||
const c = contract.value;
|
||||
@@ -475,7 +478,13 @@ function onTransition(btn) {
|
||||
if (id == null) return;
|
||||
transitionLoading.value = btn.status;
|
||||
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("状态已更新");
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user