diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/contracts/ContractController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/contracts/ContractController.java index ca3b504..cbbd408 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/contracts/ContractController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/contracts/ContractController.java @@ -1,5 +1,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.web.dto.ContractCreateRequest; import cn.craftlabs.platform.api.web.dto.ContractLineRequest; @@ -8,10 +10,16 @@ import cn.craftlabs.platform.api.web.dto.ContractResponse; import cn.craftlabs.platform.api.web.dto.ContractStatusPatchRequest; import cn.craftlabs.platform.api.web.dto.ContractUpdateRequest; import cn.craftlabs.platform.api.web.dto.PageResponse; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; +import java.io.File; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -24,19 +32,19 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; -import java.util.List; - -/** 合同 API:头信息与行挂在同一资源树下(嵌套路由)。 */ @RestController @RequestMapping("/api/v1/contracts") @Validated public class ContractController { private final ContractService contractService; + private final PlatformContractAttachmentMapper attachmentMapper; - public ContractController(ContractService contractService) { + public ContractController(ContractService contractService, PlatformContractAttachmentMapper attachmentMapper) { this.contractService = contractService; + this.attachmentMapper = attachmentMapper; } @GetMapping @@ -97,4 +105,40 @@ public class ContractController { public void deleteLine(@PathVariable("id") long contractId, @PathVariable("lineId") long lineId) { contractService.deleteLine(contractId, lineId); } + + @PostMapping("/{id}/attachments") + public ResponseEntity> uploadAttachment( + @PathVariable Long id, + @RequestParam("file") MultipartFile file) { + try { + String uploadDir = System.getProperty("user.dir") + "/uploads/contracts/" + id + "/"; + File dir = new File(uploadDir); + if (!dir.exists()) dir.mkdirs(); + + String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename(); + File dest = new File(uploadDir + fileName); + file.transferTo(dest); + + PlatformContractAttachment attachment = new PlatformContractAttachment(); + attachment.setContractId(id); + attachment.setFileName(file.getOriginalFilename()); + attachment.setFilePath(dest.getAbsolutePath()); + attachment.setFileSize(file.getSize()); + attachment.setContentType(file.getContentType()); + attachment.setCreatedAt(OffsetDateTime.now()); + attachmentMapper.insert(attachment); + + return ResponseEntity.ok(Map.of("id", attachment.getId(), "fileName", attachment.getFileName())); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/{id}/attachments") + public ResponseEntity> getAttachments(@PathVariable Long id) { + var query = Wrappers.lambdaQuery(PlatformContractAttachment.class) + .eq(PlatformContractAttachment::getContractId, id) + .orderByDesc(PlatformContractAttachment::getCreatedAt); + return ResponseEntity.ok(attachmentMapper.selectList(query)); + } } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/attachment/PlatformContractAttachment.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/attachment/PlatformContractAttachment.java new file mode 100644 index 0000000..70de27b --- /dev/null +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/attachment/PlatformContractAttachment.java @@ -0,0 +1,44 @@ +package cn.craftlabs.platform.api.persistence.attachment; + +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_attachment") +public class PlatformContractAttachment { + @TableId(type = IdType.AUTO) + private Long id; + @TableField("contract_id") + private Long contractId; + @TableField("file_name") + private String fileName; + @TableField("file_path") + private String filePath; + @TableField("file_size") + private Long fileSize; + @TableField("content_type") + private String contentType; + @TableField("uploaded_by") + private String uploadedBy; + @TableField("created_at") + private OffsetDateTime createdAt; + + 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 String getFileName() { return fileName; } + public void setFileName(String fileName) { this.fileName = fileName; } + public String getFilePath() { return filePath; } + public void setFilePath(String filePath) { this.filePath = filePath; } + public Long getFileSize() { return fileSize; } + public void setFileSize(Long fileSize) { this.fileSize = fileSize; } + public String getContentType() { return contentType; } + public void setContentType(String contentType) { this.contentType = contentType; } + public String getUploadedBy() { return uploadedBy; } + public void setUploadedBy(String uploadedBy) { this.uploadedBy = uploadedBy; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/attachment/PlatformContractAttachmentMapper.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/attachment/PlatformContractAttachmentMapper.java new file mode 100644 index 0000000..b21c148 --- /dev/null +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/attachment/PlatformContractAttachmentMapper.java @@ -0,0 +1,7 @@ +package cn.craftlabs.platform.api.persistence.attachment; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PlatformContractAttachmentMapper extends BaseMapper {} diff --git a/services/delivery-platform-api/src/main/resources/db/migration/V10__contract_attachments.sql b/services/delivery-platform-api/src/main/resources/db/migration/V10__contract_attachments.sql new file mode 100644 index 0000000..2d245f3 --- /dev/null +++ b/services/delivery-platform-api/src/main/resources/db/migration/V10__contract_attachments.sql @@ -0,0 +1,13 @@ +-- V10__contract_attachments.sql +CREATE TABLE platform_contract_attachment ( + id BIGSERIAL PRIMARY KEY, + contract_id BIGINT NOT NULL REFERENCES platform_contract(id), + file_name VARCHAR(256) NOT NULL, + file_path VARCHAR(1024) NOT NULL, + file_size BIGINT, + content_type VARCHAR(128), + uploaded_by VARCHAR(256), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_attachment_contract ON platform_contract_attachment(contract_id); diff --git a/web/delivery-platform-ui/src/api/platform.js b/web/delivery-platform-ui/src/api/platform.js index 791dcba..a2ec2f5 100644 --- a/web/delivery-platform-ui/src/api/platform.js +++ b/web/delivery-platform-ui/src/api/platform.js @@ -3,6 +3,18 @@ import axios from "axios"; /** * @param {{ page?: number, size?: number, keyword?: string }} params */ +export function uploadContractAttachment(contractId, file) { + const formData = new FormData(); + formData.append('file', file); + return axios.post(`/api/v1/contracts/${contractId}/attachments`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }); +} + +export function listContractAttachments(contractId) { + return axios.get(`/api/v1/contracts/${contractId}/attachments`); +} + export function listCustomers(params) { return axios.get("/api/v1/customers", { params }); } diff --git a/web/delivery-platform-ui/src/views/ContractDetailView.vue b/web/delivery-platform-ui/src/views/ContractDetailView.vue index 71d05de..028f15a 100644 --- a/web/delivery-platform-ui/src/views/ContractDetailView.vue +++ b/web/delivery-platform-ui/src/views/ContractDetailView.vue @@ -62,6 +62,26 @@ +

附件

+
+ + 上传附件 + + + + + + + + +
+

最近审计

@@ -110,6 +130,8 @@ import { listAuditEvents, listCustomers, listProjects, + uploadContractAttachment, + listContractAttachments, } from "../api/platform"; import { apiErrorMessage } from "../utils/apiErrorMessage"; @@ -131,6 +153,9 @@ const form = reactive({ const auditLoading = ref(false); const auditRows = ref([]); +const attachments = ref([]); +const uploading = ref(false); + const lineDialogVisible = ref(false); const lineEditingId = ref(null); const lineFormRef = ref(null); @@ -150,6 +175,7 @@ const transitionLoading = ref(""); 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 lineRows = computed(() => { const c = contract.value; @@ -196,6 +222,7 @@ onMounted(async () => { auth.restoreAxiosAuth(); await loadNameMaps(); await refreshAll(); + await loadAttachments(); }); watch( @@ -459,6 +486,26 @@ function onTransition(btn) { }) .catch(() => {}); } + +async function loadAttachments() { + try { + const { data } = await listContractAttachments(route.params.id); + attachments.value = data || []; + } catch (e) { /* ignore */ } +} + +async function handleFileChange(uploadFile) { + uploading.value = true; + try { + await uploadContractAttachment(route.params.id, uploadFile.raw); + ElMessage.success('上传成功'); + loadAttachments(); + } catch (e) { + ElMessage.error(apiErrorMessage(e, '上传失败')); + } finally { + uploading.value = false; + } +}