Просмотр исходного кода

合同管理接口代码提交

fuyuchuan 16 часов назад
Родитель
Сommit
8f996ea628

+ 2 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ArchiveController.java

@@ -38,8 +38,8 @@ public class ArchiveController {
      * siteId 站点id
      * bizType 业务类型
      * bizId 业务主键
-     * pageNum 页码
-     * pageSize 页大小
+     * current 页码
+     * size 页大小
      */
     @GetMapping
     public ApiResult<CommonPage<VppFileArchiveResponseVO>> page(@RequestParam(value = "id", required = false) Long id,

+ 73 - 3
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/ContractController.java

@@ -2,13 +2,18 @@ package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.VppContractService;
 import com.usky.vpp.service.VppContractTemplateService;
+import com.usky.vpp.service.vo.VppContractRequestVO;
+import com.usky.vpp.service.vo.VppContractResponseVO;
 import com.usky.vpp.service.vo.VppContractTemplateRequestVO;
 import com.usky.vpp.service.vo.VppContractTemplateResponseVO;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDate;
 
 /**
  * 虚拟电厂 - Contract 接口
@@ -20,6 +25,10 @@ public class ContractController {
 
     @Autowired
     private VppContractTemplateService vppContractTemplateService;
+    @Autowired
+    private VppContractService vppContractService;
+
+    // ==================== 合同模板 ====================
 
     /**
      * 新增合同模板
@@ -43,8 +52,8 @@ public class ContractController {
      * templateName 模板名称 模糊匹配
      * contractType 合同类型
      * isEnabled    是否启用 0-禁用 1-启用
-     * pageNum      页码
-     * pageSize     页大小
+     * current      页码
+     * size         页大小
      */
     @GetMapping("/template")
     public ApiResult<CommonPage<VppContractTemplateResponseVO>> pageTemplate(
@@ -52,7 +61,7 @@ public class ContractController {
             @RequestParam(value = "templateName", required = false) String templateName,
             @RequestParam(value = "contractType", required = false) Integer contractType,
             @RequestParam(value = "isEnabled", required = false) Integer isEnabled,
-            @RequestParam(value = "", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
             @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
         return ApiResult.success(vppContractTemplateService.page(id, templateName, contractType, isEnabled, current, size));
     }
@@ -72,4 +81,65 @@ public class ContractController {
     public void downloadTemplate(@PathVariable("id") Long id, HttpServletResponse response) {
         vppContractTemplateService.download(id, response);
     }
+
+    // ==================== 合同 ====================
+
+    /**
+     * 新增合同
+     */
+    @PostMapping
+    public ApiResult<Boolean> create(@RequestBody VppContractRequestVO vo) {
+        return vppContractService.create(vo) ? ApiResult.success(true) : ApiResult.error("新增合同失败!请重试!");
+    }
+
+    /**
+     * 修改合同
+     */
+    @PutMapping
+    public ApiResult<Boolean> update(@RequestBody VppContractRequestVO vo) {
+        return vppContractService.update(vo) ? ApiResult.success(true) : ApiResult.error("修改合同失败!请重试!");
+    }
+
+    /**
+     * 分页查询合同
+     * id             主键ID 精确匹配
+     * signDateStart  签约日期起始
+     * signDateEnd    签约日期截止
+     * templateId     模板ID
+     * contractType   合同类型
+     * customerId     客户ID
+     * contractStatus 合同状态
+     * current        页码
+     * size           页大小
+     */
+    @GetMapping
+    public ApiResult<CommonPage<VppContractResponseVO>> page(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "startDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
+            @RequestParam(value = "endDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
+            @RequestParam(value = "templateId", required = false) Long templateId,
+            @RequestParam(value = "contractType", required = false) Integer contractType,
+            @RequestParam(value = "customerId", required = false) Long customerId,
+            @RequestParam(value = "contractStatus", required = false) Integer contractStatus,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppContractService.page(id, startDate, endDate,
+                templateId, contractType, customerId, contractStatus, current, size));
+    }
+
+    /**
+     * 删除合同(软删除)
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Boolean> delete(@PathVariable("id") Long id) {
+        return vppContractService.delete(id) ? ApiResult.success(true) : ApiResult.error("删除合同失败!请重试!");
+    }
+
+    /**
+     * 下载合同文件(单个文件流下载,不打包)
+     */
+    @GetMapping("/{id}/download")
+    public void download(@PathVariable("id") Long id, HttpServletResponse response) {
+        vppContractService.download(id, response);
+    }
 }

+ 42 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContract.java

@@ -4,8 +4,12 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
@@ -23,45 +27,83 @@ public class VppContract implements Serializable {
 
     @TableId(value = "id", type = IdType.ASSIGN_ID)
     private Long id;
+
     @TableField("contract_no")
+    @NotBlank(message = "合同编号不能为空!")
     private String contractNo;
+
     @TableField("customer_id")
+    @NotNull(message = "客户ID不能为空!")
     private Long customerId;
+
     @TableField("template_id")
+    @NotNull(message = "合同模板ID不能为空!")
     private Long templateId;
+
+    /**
+     * 1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产
+     */
     @TableField("contract_type")
+    @NotNull(message = "合同类型不能为空!")
     private Integer contractType;
+
     @TableField("contract_name")
+    @NotNull(message = "合同名称不能为空!")
     private String contractName;
+
+    /**
+     * 0草稿 1审核中 2已生效 3已到期 4已终止
+     */
+    @NotNull(message = "合同状态不能为空!")
     @TableField("contract_status")
     private Integer contractStatus;
+
     @TableField("sign_date")
     private LocalDate signDate;
+
     @TableField("effective_date")
+    @JsonFormat(pattern = "yyyy-MM-dd")
     private LocalDate effectiveDate;
+
     @TableField("expire_date")
+    @JsonFormat(pattern = "yyyy-MM-dd")
     private LocalDate expireDate;
+
     @TableField("file_url")
     private String fileUrl;
+
     @TableField("share_ratio")
     private BigDecimal shareRatio;
+
     @TableField("price_json")
     private String priceJson;
+
     @TableField("account_info_json")
     private String accountInfoJson;
+
+    @TableField("remark")
     private String remark;
+
     @TableField("tenant_id")
     private Integer tenantId;
+
     @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime createTime;
+
     @TableField("update_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime updateTime;
+
     @TableField("created_by")
     private String createdBy;
+
     @TableField("updated_by")
     private String updatedBy;
+
     @TableField("delete_flag")
     private Integer deleteFlag;
+
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
 }

+ 1 - 34
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppContractAuditLog.java

@@ -1,34 +1 @@
-package com.usky.vpp.domain;

-

-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 lombok.Data;

-import lombok.EqualsAndHashCode;

-import java.io.Serializable;

-import java.time.LocalDateTime;

-

-/**

- * vpp_contract_audit_log

- */

-@Data

-@EqualsAndHashCode(callSuper = false)

-@TableName("vpp_contract_audit_log")

-public class VppContractAuditLog implements Serializable {

-

-    private static final long serialVersionUID = 1L;

-

-    @TableId(value = "id", type = IdType.ASSIGN_ID)

-    private Long id;

-    @TableField("contract_id")

-    private Long contractId;

-    private Integer action;

-    private String opinion;

-    @TableField("tenant_id")

-    private Integer tenantId;

-    @TableField("create_time")

-    private LocalDateTime createTime;

-    @TableField("created_by")

-    private String createdBy;

-}

+package com.usky.vpp.domain;



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 lombok.Data;

import lombok.EqualsAndHashCode;

import java.io.Serializable;

import java.time.LocalDateTime;



/**

 * vpp_contract_audit_log

 */

@Data

@EqualsAndHashCode(callSuper = false)

@TableName("vpp_contract_audit_log")

public class VppContractAuditLog implements Serializable {



    private static final long serialVersionUID = 1L;



    @TableId(value = "id", type = IdType.ASSIGN_ID)

    private Long id;

    @TableField("contract_id")

    private Long contractId;

    private Integer action;

    private String opinion;

    @TableField("tenant_id")

    private Integer tenantId;

    @TableField("create_time")

    private LocalDateTime createTime;

    @TableField("created_by")

    private String createdBy;

}


+ 40 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppContractService.java

@@ -1,7 +1,11 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
+import com.usky.vpp.service.vo.VppContractRequestVO;
+import com.usky.vpp.service.vo.VppContractResponseVO;
 
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDate;
 import java.util.Map;
 
 /**
@@ -12,4 +16,40 @@ public interface VppContractService {
     default Object stub(String action, Map<String, Object> params) {
         return null;
     }
+
+    /**
+     * 新增合同
+     */
+    Boolean create(VppContractRequestVO vo);
+
+    /**
+     * 修改合同
+     */
+    Boolean update(VppContractRequestVO vo);
+
+    /**
+     * 分页查询合同
+     * @param id             主键ID(精确匹配)
+     * @param signDateStart  签约日期起始
+     * @param signDateEnd    签约日期截止
+     * @param templateId     模板ID
+     * @param contractType   合同类型
+     * @param customerId     客户ID
+     * @param contractStatus 合同状态
+     * @param pageNum        页码
+     * @param pageSize       页大小
+     */
+    CommonPage<VppContractResponseVO> page(Long id, LocalDate signDateStart, LocalDate signDateEnd,
+                                           Long templateId, Integer contractType, Long customerId,
+                                           Integer contractStatus, Integer pageNum, Integer pageSize);
+
+    /**
+     * 删除合同(软删除)
+     */
+    Boolean delete(Long id);
+
+    /**
+     * 下载合同文件
+     */
+    void download(Long id, HttpServletResponse response);
 }

+ 325 - 1
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppContractServiceImpl.java

@@ -1,11 +1,335 @@
 package com.usky.vpp.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.vpp.domain.VppContract;
+import com.usky.vpp.domain.VppContractTemplate;
+import com.usky.vpp.domain.VppCustomer;
+import com.usky.vpp.mapper.VppContractMapper;
+import com.usky.vpp.mapper.VppContractTemplateMapper;
+import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.service.VppContractService;
+import com.usky.vpp.service.vo.VppContractRequestVO;
+import com.usky.vpp.service.vo.VppContractResponseVO;
+import com.usky.vpp.util.VppAuditHelper;
+import com.usky.vpp.util.VppFieldValidator;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
- * VppContractService 默认实现(待按业务完善)
+ * VppContractService 默认实现
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
  */
 @Service
 public class VppContractServiceImpl implements VppContractService {
+
+    @Autowired
+    private VppContractMapper vppContractMapper;
+    @Autowired
+    private VppContractTemplateMapper vppContractTemplateMapper;
+    @Autowired
+    private VppCustomerMapper vppCustomerMapper;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override
+    public Boolean create(VppContractRequestVO vo) {
+        // ========== 根据 templateId 查询模板编码并生成合同编号 ==========
+        VppContractTemplate template = getTemplateById(vo.getTemplateId());
+        String contractNo = VppFieldValidator.generateContractNo(template.getTemplateCode());
+
+        // ========== 合同编号全租户唯一性校验(极小概率冲突) ==========
+        checkContractNoUnique(contractNo, null);
+
+        VppContract contract = new VppContract();
+        contract.setContractNo(contractNo);
+        contract.setCustomerId(vo.getCustomerId());
+        contract.setTemplateId(vo.getTemplateId());
+        contract.setContractType(vo.getContractType());
+        contract.setContractName(vo.getContractName());
+        contract.setContractStatus(vo.getContractStatus());
+        contract.setSignDate(vo.getSignDate());
+        contract.setEffectiveDate(vo.getEffectiveDate());
+        contract.setExpireDate(vo.getExpireDate());
+        contract.setFileUrl(vo.getFileUrl());
+        contract.setShareRatio(vo.getShareRatio());
+        contract.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
+        contract.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
+        contract.setRemark(vo.getRemark());
+
+        VppAuditHelper.fillCreate(contract);
+
+        int insert = vppContractMapper.insert(contract);
+        return insert > 0;
+    }
+
+    @Override
+    public Boolean update(VppContractRequestVO vo) {
+        if (vo.getId() == null) {
+            throw new BusinessException("主键ID不能为空!");
+        }
+
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, vo.getId())
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract existingRecord = vppContractMapper.selectOne(queryWrapper);
+
+        if (existingRecord == null) {
+            throw new BusinessException("合同不存在或已被删除!");
+        }
+
+        // 仅更新非空字段
+        if (vo.getCustomerId() != null) {
+            existingRecord.setCustomerId(vo.getCustomerId());
+        }
+        if (vo.getTemplateId() != null) {
+            existingRecord.setTemplateId(vo.getTemplateId());
+        }
+        if (vo.getContractType() != null) {
+            existingRecord.setContractType(vo.getContractType());
+        }
+        if (StringUtils.isNotBlank(vo.getContractName())) {
+            existingRecord.setContractName(vo.getContractName());
+        }
+        if (vo.getContractStatus() != null) {
+            existingRecord.setContractStatus(vo.getContractStatus());
+        }
+        if (vo.getSignDate() != null) {
+            existingRecord.setSignDate(vo.getSignDate());
+        }
+        if (vo.getEffectiveDate() != null) {
+            existingRecord.setEffectiveDate(vo.getEffectiveDate());
+        }
+        if (vo.getExpireDate() != null) {
+            existingRecord.setExpireDate(vo.getExpireDate());
+        }
+        if (StringUtils.isNotBlank(vo.getFileUrl())) {
+            existingRecord.setFileUrl(vo.getFileUrl());
+        }
+        if (vo.getShareRatio() != null) {
+            existingRecord.setShareRatio(vo.getShareRatio());
+        }
+        if (vo.getPriceJson() != null) {
+            existingRecord.setPriceJson(VppFieldValidator.validateJson(vo.getPriceJson(), "价格定义"));
+        }
+        if (vo.getAccountInfoJson() != null) {
+            existingRecord.setAccountInfoJson(VppFieldValidator.validateJson(vo.getAccountInfoJson(), "账户信息"));
+        }
+        if (vo.getRemark() != null) {
+            existingRecord.setRemark(vo.getRemark());
+        }
+
+        VppAuditHelper.fillUpdate(existingRecord);
+
+        int result = vppContractMapper.updateById(existingRecord);
+        return result > 0;
+    }
+
+    @Override
+    public CommonPage<VppContractResponseVO> page(Long id, LocalDate signDateStart, LocalDate signDateEnd,
+                                                   Long templateId, Integer contractType, Long customerId,
+                                                   Integer contractStatus, Integer pageNum, Integer pageSize) {
+        IPage<VppContract> page = new Page<>(pageNum, pageSize);
+
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getDeleteFlag, 0)
+                .eq(id != null, VppContract::getId, id)
+                .ge(signDateStart != null, VppContract::getSignDate, signDateStart)
+                .le(signDateEnd != null, VppContract::getSignDate, signDateEnd)
+                .eq(templateId != null, VppContract::getTemplateId, templateId)
+                .eq(contractType != null, VppContract::getContractType, contractType)
+                .eq(customerId != null, VppContract::getCustomerId, customerId)
+                .eq(contractStatus != null, VppContract::getContractStatus, contractStatus)
+                .orderByDesc(VppContract::getCreateTime);
+        page = vppContractMapper.selectPage(page, queryWrapper);
+
+        List<VppContract> records = page.getRecords();
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
+        }
+
+        // ========== 批量反查客户名称和模板名称 ==========
+        Set<Long> customerIds = records.stream().map(VppContract::getCustomerId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Set<Long> templateIds = records.stream().map(VppContract::getTemplateId).filter(Objects::nonNull).collect(Collectors.toSet());
+
+        final Map<Long, String> customerNameMap = customerIds.isEmpty() ? Collections.emptyMap()
+                : vppCustomerMapper.selectBatchIds(customerIds).stream()
+                        .collect(Collectors.toMap(VppCustomer::getId, VppCustomer::getCustomerName, (a, b) -> a));
+
+        final Map<Long, String> templateNameMap = templateIds.isEmpty() ? Collections.emptyMap()
+                : vppContractTemplateMapper.selectBatchIds(templateIds).stream()
+                        .collect(Collectors.toMap(VppContractTemplate::getId, VppContractTemplate::getTemplateName, (a, b) -> a));
+
+        // ========== 组装响应VO ==========
+        List<VppContractResponseVO> list = records.stream().map(entity -> {
+            VppContractResponseVO vo = new VppContractResponseVO();
+            vo.setId(entity.getId());
+            vo.setContractNo(entity.getContractNo());
+            vo.setCustomerId(entity.getCustomerId());
+            vo.setCustomerName(customerNameMap.get(entity.getCustomerId()));
+            vo.setTemplateId(entity.getTemplateId());
+            vo.setTemplateName(templateNameMap.get(entity.getTemplateId()));
+            vo.setContractType(entity.getContractType());
+            vo.setContractName(entity.getContractName());
+            vo.setContractStatus(entity.getContractStatus());
+            vo.setSignDate(entity.getSignDate());
+            vo.setEffectiveDate(entity.getEffectiveDate());
+            vo.setExpireDate(entity.getExpireDate());
+            vo.setFileUrl(entity.getFileUrl());
+            vo.setShareRatio(entity.getShareRatio());
+            vo.setPriceJson(entity.getPriceJson());
+            vo.setAccountInfoJson(entity.getAccountInfoJson());
+            vo.setRemark(entity.getRemark());
+            vo.setTenantId(entity.getTenantId());
+            vo.setCreateTime(entity.getCreateTime());
+            vo.setUpdateTime(entity.getUpdateTime());
+            vo.setCreatedBy(entity.getCreatedBy());
+            vo.setUpdatedBy(entity.getUpdatedBy());
+            return vo;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
+    }
+
+    @Override
+    public Boolean delete(Long id) {
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract contract = vppContractMapper.selectOne(queryWrapper);
+
+        if (contract == null) {
+            throw new BusinessException("合同不存在或已被删除!");
+        }
+
+        VppAuditHelper.fillSoftDelete(contract);
+
+        int i = vppContractMapper.updateById(contract);
+        return i > 0;
+    }
+
+    @Override
+    public void download(Long id, HttpServletResponse response) {
+        LambdaQueryWrapper<VppContract> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(VppContract::getId, id)
+                .eq(VppContract::getDeleteFlag, 0);
+        VppContract contract = vppContractMapper.selectOne(queryWrapper);
+
+        if (contract == null) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同不存在或已被删除!");
+            return;
+        }
+
+        String fileUrl = contract.getFileUrl();
+        String fileName = contract.getContractName();
+        if (StringUtils.isBlank(fileUrl)) {
+            writeErrorResponse(response, HttpStatus.BAD_REQUEST.value(), "合同文件URL为空,无法下载!");
+            return;
+        }
+
+        // ========== 设置响应头 ==========
+        String downloadFileName = fileName;
+        int dotIndex = fileUrl.lastIndexOf('.');
+        if (dotIndex > 0 && !fileName.contains(".")) {
+            downloadFileName = fileName + fileUrl.substring(dotIndex);
+        }
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
+                "attachment; filename=" + encodeFileName(downloadFileName));
+
+        // ========== 下载文件并写入响应流 ==========
+        try {
+            byte[] fileBytes = restTemplate.getForObject(fileUrl, byte[].class);
+            if (fileBytes == null || fileBytes.length == 0) {
+                writeErrorResponse(response, HttpStatus.NOT_FOUND.value(), "合同文件内容为空!");
+                return;
+            }
+            ServletOutputStream sos = response.getOutputStream();
+            sos.write(fileBytes);
+            sos.flush();
+        } catch (Exception e) {
+            if (!response.isCommitted()) {
+                writeErrorResponse(response, HttpStatus.INTERNAL_SERVER_ERROR.value(),
+                        "下载合同文件失败: " + e.getMessage());
+            }
+        }
+    }
+
+    // ========== 内部辅助方法 ==========
+
+    /**
+     * 根据模板ID查询模板信息
+     */
+    private VppContractTemplate getTemplateById(Long templateId) {
+        if (templateId == null) {
+            throw new BusinessException("模板ID不能为空!");
+        }
+        LambdaQueryWrapper<VppContractTemplate> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppContractTemplate::getId, templateId)
+                .eq(VppContractTemplate::getDeleteFlag, 0);
+        VppContractTemplate template = vppContractTemplateMapper.selectOne(wrapper);
+        if (template == null) {
+            throw new BusinessException("合同模板不存在或已被删除!");
+        }
+        return template;
+    }
+
+    /**
+     * 合同编号全租户唯一性校验
+     */
+    private void checkContractNoUnique(String contractNo, Long excludeId) {
+        LambdaQueryWrapper<VppContract> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppContract::getContractNo, contractNo)
+                .eq(VppContract::getDeleteFlag, 0)
+                .ne(excludeId != null, VppContract::getId, excludeId);
+        Integer count = vppContractMapper.selectCount(wrapper);
+        if (count != null && count > 0) {
+            throw new BusinessException("合同编号【" + contractNo + "】已存在,请重新提交!");
+        }
+    }
+
+    private void writeErrorResponse(HttpServletResponse response, int status, String message) {
+        response.setStatus(status);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        try {
+            response.getWriter().write("{\"code\":\"" + status + "\",\"msg\":\"" + message + "\"}");
+        } catch (IOException e) {
+            // 忽略写入异常
+        }
+    }
+
+    private String encodeFileName(String fileName) {
+        try {
+            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            return fileName;
+        }
+    }
 }

+ 86 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractRequestVO.java

@@ -0,0 +1,86 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 合同 请求VO
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Data
+public class VppContractRequestVO {
+
+    /**
+     * 主键(更新时必传)
+     */
+    private Long id;
+
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+
+    /**
+     * 模板ID
+     */
+    private Long templateId;
+
+    /**
+     * 合同类型
+     */
+    private Integer contractType;
+
+    /**
+     * 合同名称
+     */
+    private String contractName;
+
+    /**
+     * 合同状态
+     */
+    private Integer contractStatus;
+
+    /**
+     * 签约日期
+     */
+    private LocalDate signDate;
+
+    /**
+     * 生效日期
+     */
+    private LocalDate effectiveDate;
+
+    /**
+     * 到期日期
+     */
+    private LocalDate expireDate;
+
+    /**
+     * 合同文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 分成比例
+     */
+    private BigDecimal shareRatio;
+
+    /**
+     * 价格定义(JSON)
+     */
+    private String priceJson;
+
+    /**
+     * 账户信息(JSON)
+     */
+    private String accountInfoJson;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 124 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/VppContractResponseVO.java

@@ -0,0 +1,124 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 合同 响应VO
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/4
+ */
+@Data
+public class VppContractResponseVO {
+
+    private Long id;
+
+    /**
+     * 合同编号(自动生成)
+     */
+    private String contractNo;
+
+    /**
+     * 客户ID
+     */
+    private Long customerId;
+
+    /**
+     * 客户名称(关联查询)
+     */
+    private String customerName;
+
+    /**
+     * 模板ID
+     */
+    private Long templateId;
+
+    /**
+     * 模板名称(关联查询)
+     */
+    private String templateName;
+
+    /**
+     * 合同类型
+     */
+    private Integer contractType;
+
+    /**
+     * 合同名称
+     */
+    private String contractName;
+
+    /**
+     * 合同状态
+     */
+    private Integer contractStatus;
+
+    /**
+     * 签约日期
+     */
+    private LocalDate signDate;
+
+    /**
+     * 生效日期
+     */
+    private LocalDate effectiveDate;
+
+    /**
+     * 到期日期
+     */
+    private LocalDate expireDate;
+
+    /**
+     * 合同文件URL
+     */
+    private String fileUrl;
+
+    /**
+     * 分成比例
+     */
+    private BigDecimal shareRatio;
+
+    /**
+     * 价格定义(JSON)
+     */
+    private String priceJson;
+
+    /**
+     * 账户信息(JSON)
+     */
+    private String accountInfoJson;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+}

+ 11 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppFieldValidator.java

@@ -79,4 +79,15 @@ public final class VppFieldValidator {
         }
         return json;
     }
+
+    /**
+     * 根据模板编码生成合同编号,格式:{templateCode}-{13位毫秒时间戳}
+     * <p>示例:templateCode=CG → CG-1783154814855</p>
+     *
+     * @param templateCode 模板编码
+     * @return 合同编号
+     */
+    public static String generateContractNo(String templateCode) {
+        return templateCode + "-" + System.currentTimeMillis();
+    }
 }