mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat(m2): add contract attachment upload and listing
This commit is contained in:
+48
-4
@@ -1,5 +1,7 @@
|
|||||||
package cn.craftlabs.platform.api.contracts;
|
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.ContractService;
|
||||||
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;
|
||||||
@@ -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.ContractStatusPatchRequest;
|
||||||
import cn.craftlabs.platform.api.web.dto.ContractUpdateRequest;
|
import cn.craftlabs.platform.api.web.dto.ContractUpdateRequest;
|
||||||
import cn.craftlabs.platform.api.web.dto.PageResponse;
|
import cn.craftlabs.platform.api.web.dto.PageResponse;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.Max;
|
import jakarta.validation.constraints.Max;
|
||||||
import jakarta.validation.constraints.Min;
|
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.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** 合同 API:头信息与行挂在同一资源树下(嵌套路由)。 */
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/contracts")
|
@RequestMapping("/api/v1/contracts")
|
||||||
@Validated
|
@Validated
|
||||||
public class ContractController {
|
public class ContractController {
|
||||||
|
|
||||||
private final ContractService contractService;
|
private final ContractService contractService;
|
||||||
|
private final PlatformContractAttachmentMapper attachmentMapper;
|
||||||
|
|
||||||
public ContractController(ContractService contractService) {
|
public ContractController(ContractService contractService, PlatformContractAttachmentMapper attachmentMapper) {
|
||||||
this.contractService = contractService;
|
this.contractService = contractService;
|
||||||
|
this.attachmentMapper = attachmentMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -97,4 +105,40 @@ public class ContractController {
|
|||||||
public void deleteLine(@PathVariable("id") long contractId, @PathVariable("lineId") long lineId) {
|
public void deleteLine(@PathVariable("id") long contractId, @PathVariable("lineId") long lineId) {
|
||||||
contractService.deleteLine(contractId, lineId);
|
contractService.deleteLine(contractId, lineId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/attachments")
|
||||||
|
public ResponseEntity<Map<String, Object>> 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<List<PlatformContractAttachment>> getAttachments(@PathVariable Long id) {
|
||||||
|
var query = Wrappers.lambdaQuery(PlatformContractAttachment.class)
|
||||||
|
.eq(PlatformContractAttachment::getContractId, id)
|
||||||
|
.orderByDesc(PlatformContractAttachment::getCreatedAt);
|
||||||
|
return ResponseEntity.ok(attachmentMapper.selectList(query));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
@@ -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; }
|
||||||
|
}
|
||||||
+7
@@ -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<PlatformContractAttachment> {}
|
||||||
+13
@@ -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);
|
||||||
@@ -3,6 +3,18 @@ import axios from "axios";
|
|||||||
/**
|
/**
|
||||||
* @param {{ page?: number, size?: number, keyword?: string }} params
|
* @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) {
|
export function listCustomers(params) {
|
||||||
return axios.get("/api/v1/customers", { params });
|
return axios.get("/api/v1/customers", { params });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,26 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<h3 class="section-title">附件</h3>
|
||||||
|
<div>
|
||||||
|
<el-upload
|
||||||
|
:action="''"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:accept="'.pdf,.doc,.docx,.xls,.xlsx,.zip'"
|
||||||
|
>
|
||||||
|
<el-button type="primary" v-if="isDraft || isEffective">上传附件</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-table :data="attachments" stripe size="small" style="margin-top:8px">
|
||||||
|
<el-table-column prop="fileName" label="文件名" min-width="200" />
|
||||||
|
<el-table-column prop="fileSize" label="大小" width="100">
|
||||||
|
<template #default="{ row }">{{ (row.fileSize / 1024).toFixed(1) }} KB</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdAt" label="上传时间" width="170" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="section-title">最近审计</h3>
|
<h3 class="section-title">最近审计</h3>
|
||||||
<el-table v-loading="auditLoading" :data="auditRows" border stripe size="small" style="width: 100%">
|
<el-table v-loading="auditLoading" :data="auditRows" border stripe size="small" style="width: 100%">
|
||||||
<el-table-column label="时间" width="180">
|
<el-table-column label="时间" width="180">
|
||||||
@@ -110,6 +130,8 @@ import {
|
|||||||
listAuditEvents,
|
listAuditEvents,
|
||||||
listCustomers,
|
listCustomers,
|
||||||
listProjects,
|
listProjects,
|
||||||
|
uploadContractAttachment,
|
||||||
|
listContractAttachments,
|
||||||
} from "../api/platform";
|
} from "../api/platform";
|
||||||
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||||
|
|
||||||
@@ -131,6 +153,9 @@ const form = reactive({
|
|||||||
const auditLoading = ref(false);
|
const auditLoading = ref(false);
|
||||||
const auditRows = ref([]);
|
const auditRows = ref([]);
|
||||||
|
|
||||||
|
const attachments = ref([]);
|
||||||
|
const uploading = ref(false);
|
||||||
|
|
||||||
const lineDialogVisible = ref(false);
|
const lineDialogVisible = ref(false);
|
||||||
const lineEditingId = ref(null);
|
const lineEditingId = ref(null);
|
||||||
const lineFormRef = ref(null);
|
const lineFormRef = ref(null);
|
||||||
@@ -150,6 +175,7 @@ const transitionLoading = ref("");
|
|||||||
const contractId = computed(() => route.params.id);
|
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 lineRows = computed(() => {
|
const lineRows = computed(() => {
|
||||||
const c = contract.value;
|
const c = contract.value;
|
||||||
@@ -196,6 +222,7 @@ onMounted(async () => {
|
|||||||
auth.restoreAxiosAuth();
|
auth.restoreAxiosAuth();
|
||||||
await loadNameMaps();
|
await loadNameMaps();
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
|
await loadAttachments();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -459,6 +486,26 @@ function onTransition(btn) {
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user