完善招投标导入测试与文档

This commit is contained in:
wkc
2026-04-22 16:20:37 +08:00
parent 0c5fa6b2c8
commit 5a9b79d4ee
42 changed files with 1814 additions and 707 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -80,7 +80,7 @@
mvn clean compile
# 启动主应用Jar
cd ruoyi-admin/target && java -jar ruoyi-admin.jar
sh bin/restart_java_backend.sh
# 打包全部模块
mvn clean package

View File

@@ -215,7 +215,7 @@ follow_logs() {
start_action() {
running_pids=$(collect_pids)
if [ -n "${running_pids:-}" ]; then
log_error "检测到已有后端进程在运行: $running_pids,请先执行 stop 或 restart"
log_error "检测到已有后端进程在运行: ${running_pids},请先执行 stop 或 restart"
exit 1
fi

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
@@ -33,12 +34,12 @@ import java.util.ArrayList;
import java.util.List;
/**
* 采购交易信息Controller
* 招投标信息维护Controller
*
* @author ruoyi
* @date 2026-02-06
*/
@Tag(name = "采购交易信息管理")
@Tag(name = "招投标信息维护")
@RestController
@RequestMapping("/ccdi/purchaseTransaction")
public class CcdiPurchaseTransactionController extends BaseController {
@@ -50,9 +51,9 @@ public class CcdiPurchaseTransactionController extends BaseController {
private ICcdiPurchaseTransactionImportService transactionImportService;
/**
* 查询采购交易列表
* 查询招投标信息列表
*/
@Operation(summary = "查询采购交易列表")
@Operation(summary = "查询招投标信息列表")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:list')")
@GetMapping("/list")
public TableDataInfo list(CcdiPurchaseTransactionQueryDTO queryDTO) {
@@ -64,9 +65,9 @@ public class CcdiPurchaseTransactionController extends BaseController {
}
/**
* 获取采购交易详细信息
* 获取招投标信息详细信息
*/
@Operation(summary = "获取采购交易详细信息")
@Operation(summary = "获取招投标信息详细信息")
@Parameter(name = "purchaseId", description = "采购事项ID", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:query')")
@GetMapping(value = "/{purchaseId}")
@@ -75,66 +76,81 @@ public class CcdiPurchaseTransactionController extends BaseController {
}
/**
* 新增采购交易
* 新增招投标信息
*/
@Operation(summary = "新增采购交易")
@Operation(summary = "新增招投标信息")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:add')")
@Log(title = "采购交易信息", businessType = BusinessType.INSERT)
@Log(title = "招投标信息维护", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody CcdiPurchaseTransactionAddDTO addDTO) {
return toAjax(transactionService.insertTransaction(addDTO));
}
/**
* 修改采购交易
* 修改招投标信息
*/
@Operation(summary = "修改采购交易")
@Operation(summary = "修改招投标信息")
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:edit')")
@Log(title = "采购交易信息", businessType = BusinessType.UPDATE)
@Log(title = "招投标信息维护", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody CcdiPurchaseTransactionEditDTO editDTO) {
return toAjax(transactionService.updateTransaction(editDTO));
}
/**
* 删除采购交易
* 删除招投标信息
*/
@Operation(summary = "删除采购交易")
@Operation(summary = "删除招投标信息")
@Parameter(name = "purchaseIds", description = "采购事项ID数组", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:remove')")
@Log(title = "采购交易信息", businessType = BusinessType.DELETE)
@Log(title = "招投标信息维护", businessType = BusinessType.DELETE)
@DeleteMapping("/{purchaseIds}")
public AjaxResult remove(@PathVariable String[] purchaseIds) {
return toAjax(transactionService.deleteTransactionByIds(purchaseIds));
}
/**
* 下载带字典下拉框的导入模板
* 使用@DictDropdown注解自动添加下拉框
* 下载双Sheet导入模板
*/
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiPurchaseTransactionExcel.class, "采购交易信息");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiPurchaseTransactionExcel.class,
"招投标主信息",
CcdiPurchaseTransactionSupplierExcel.class,
"供应商明细",
"招投标信息维护导入模板"
);
}
/**
* 异步导入采购交易
* 异步导入招投标信息
*/
@Operation(summary = "异步导入采购交易")
@Operation(summary = "异步导入招投标信息")
@Parameter(name = "file", description = "导入文件", required = true)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:import')")
@Log(title = "采购交易信息", businessType = BusinessType.IMPORT)
@Log(title = "招投标信息维护", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiPurchaseTransactionExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiPurchaseTransactionExcel.class);
List<CcdiPurchaseTransactionExcel> mainList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiPurchaseTransactionExcel.class,
"招投标主信息"
);
List<CcdiPurchaseTransactionSupplierExcel> supplierList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiPurchaseTransactionSupplierExcel.class,
"供应商明细"
);
if (list == null || list.isEmpty()) {
if ((mainList == null || mainList.isEmpty()) && (supplierList == null || supplierList.isEmpty())) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = transactionService.importTransaction(list);
String taskId = transactionService.importTransaction(mainList, supplierList);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();

View File

@@ -0,0 +1,64 @@
package com.ruoyi.info.collection.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 招投标供应商明细对象 ccdi_purchase_transaction_supplier
*/
@Data
public class CcdiPurchaseTransactionSupplier implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** 采购事项ID */
private String purchaseId;
/** 供应商名称 */
private String supplierName;
/** 供应商统一信用代码 */
private String supplierUscc;
/** 供应商联系人 */
private String contactPerson;
/** 供应商联系电话 */
private String contactPhone;
/** 供应商银行账户 */
private String supplierBankAccount;
/** 是否中标1-是0-否 */
private Integer isBidWinner;
/** 排序 */
private Integer sortOrder;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/** 创建人 */
@TableField(fill = FieldFill.INSERT)
private String createdBy;
/** 更新人 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedBy;
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
@@ -9,15 +10,16 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 采购交易信息新增DTO
* 招投标信息新增DTO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息新增")
@Schema(description = "招投标信息新增")
public class CcdiPurchaseTransactionAddDTO implements Serializable {
@Serial
@@ -88,30 +90,10 @@ public class CcdiPurchaseTransactionAddDTO implements Serializable {
@Schema(description = "采购方式")
private String purchaseMethod;
/** 中标供应商名称 */
@Size(max = 200, message = "中标供应商名称长度不能超过200个字符")
@Schema(description = "中标供应商名称")
private String supplierName;
/** 供应商联系人 */
@Size(max = 50, message = "供应商联系人长度不能超过50个字符")
@Schema(description = "供应商联系人")
private String contactPerson;
/** 供应商联系电话 */
@Pattern(regexp = "^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$", message = "供应商联系电话格式不正确")
@Schema(description = "供应商联系电话")
private String contactPhone;
/** 供应商统一信用代码 */
@Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "供应商统一信用代码格式不正确")
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
/** 供应商银行账户 */
@Size(max = 50, message = "供应商银行账户长度不能超过50个字符")
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
/** 供应商明细 */
@Valid
@Schema(description = "供应商明细列表")
private List<CcdiPurchaseTransactionSupplierDTO> supplierList;
/** 采购申请日期(或立项日期) */
@NotNull(message = "采购申请日期不能为空")

View File

@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
@@ -9,15 +10,16 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 采购交易信息编辑DTO
* 招投标信息编辑DTO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息编辑")
@Schema(description = "招投标信息编辑")
public class CcdiPurchaseTransactionEditDTO implements Serializable {
@Serial
@@ -88,30 +90,10 @@ public class CcdiPurchaseTransactionEditDTO implements Serializable {
@Schema(description = "采购方式")
private String purchaseMethod;
/** 中标供应商名称 */
@Size(max = 200, message = "中标供应商名称长度不能超过200个字符")
@Schema(description = "中标供应商名称")
private String supplierName;
/** 供应商联系人 */
@Size(max = 50, message = "供应商联系人长度不能超过50个字符")
@Schema(description = "供应商联系人")
private String contactPerson;
/** 供应商联系电话 */
@Pattern(regexp = "^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$", message = "供应商联系电话格式不正确")
@Schema(description = "供应商联系电话")
private String contactPhone;
/** 供应商统一信用代码 */
@Pattern(regexp = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$", message = "供应商统一信用代码格式不正确")
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
/** 供应商银行账户 */
@Size(max = 50, message = "供应商银行账户长度不能超过50个字符")
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
/** 供应商明细 */
@Valid
@Schema(description = "供应商明细列表")
private List<CcdiPurchaseTransactionSupplierDTO> supplierList;
/** 采购申请日期(或立项日期) */
@NotNull(message = "采购申请日期不能为空")

View File

@@ -9,13 +9,13 @@ import java.io.Serializable;
import java.util.Date;
/**
* 采购交易信息查询DTO
* 招投标信息查询DTO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息查询条件")
@Schema(description = "招投标信息查询条件")
public class CcdiPurchaseTransactionQueryDTO implements Serializable {
@Serial

View File

@@ -0,0 +1,54 @@
package com.ruoyi.info.collection.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招投标供应商明细DTO
*/
@Data
@Schema(description = "招投标供应商明细")
public class CcdiPurchaseTransactionSupplierDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotBlank(message = "供应商名称不能为空")
@Size(max = 200, message = "供应商名称长度不能超过200个字符")
@Schema(description = "供应商名称")
private String supplierName;
@Pattern(
regexp = "^$|^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$",
message = "供应商统一信用代码格式不正确"
)
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
@Size(max = 50, message = "供应商联系人长度不能超过50个字符")
@Schema(description = "供应商联系人")
private String contactPerson;
@Pattern(
regexp = "^$|^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$",
message = "供应商联系电话格式不正确"
)
@Schema(description = "供应商联系电话")
private String contactPhone;
@Size(max = 50, message = "供应商银行账户长度不能超过50个字符")
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
@Schema(description = "是否中标1-是0-否")
private Integer isBidWinner;
@Schema(description = "排序")
private Integer sortOrder;
}

View File

@@ -11,7 +11,7 @@ import java.math.BigDecimal;
import java.util.Date;
/**
* 采购交易信息Excel导入导出对象
* 招投标主信息Excel导入导出对象
*
* @author ruoyi
* @date 2026-02-06
@@ -88,107 +88,82 @@ public class CcdiPurchaseTransactionExcel implements Serializable {
@Required
private String purchaseMethod;
/** 中标供应商名称 */
@ExcelProperty(value = "中标供应商名称", index = 12)
@ColumnWidth(25)
private String supplierName;
/** 供应商联系人 */
@ExcelProperty(value = "供应商联系人", index = 13)
@ColumnWidth(15)
private String contactPerson;
/** 供应商联系电话 */
@ExcelProperty(value = "供应商联系电话", index = 14)
@ColumnWidth(18)
private String contactPhone;
/** 供应商统一信用代码 */
@ExcelProperty(value = "供应商统一信用代码", index = 15)
@ColumnWidth(25)
private String supplierUscc;
/** 供应商银行账户 */
@ExcelProperty(value = "供应商银行账户", index = 16)
@ColumnWidth(20)
private String supplierBankAccount;
/** 采购申请日期(或立项日期) */
@ExcelProperty(value = "采购申请日期", index = 17)
@ExcelProperty(value = "采购申请日期", index = 12)
@ColumnWidth(18)
@Required
private Date applyDate;
/** 采购计划批准日期 */
@ExcelProperty(value = "采购计划批准日期", index = 18)
@ExcelProperty(value = "采购计划批准日期", index = 13)
@ColumnWidth(18)
private Date planApproveDate;
/** 采购公告发布日期 */
@ExcelProperty(value = "采购公告发布日期", index = 19)
@ExcelProperty(value = "采购公告发布日期", index = 14)
@ColumnWidth(18)
private Date announceDate;
/** 开标日期 */
@ExcelProperty(value = "开标日期", index = 20)
@ExcelProperty(value = "开标日期", index = 15)
@ColumnWidth(18)
private Date bidOpenDate;
/** 合同签订日期 */
@ExcelProperty(value = "合同签订日期", index = 21)
@ExcelProperty(value = "合同签订日期", index = 16)
@ColumnWidth(18)
private Date contractSignDate;
/** 预计交货日期 */
@ExcelProperty(value = "预计交货日期", index = 22)
@ExcelProperty(value = "预计交货日期", index = 17)
@ColumnWidth(18)
private Date expectedDeliveryDate;
/** 实际交货日期 */
@ExcelProperty(value = "实际交货日期", index = 23)
@ExcelProperty(value = "实际交货日期", index = 18)
@ColumnWidth(18)
private Date actualDeliveryDate;
/** 验收日期 */
@ExcelProperty(value = "验收日期", index = 24)
@ExcelProperty(value = "验收日期", index = 19)
@ColumnWidth(18)
private Date acceptanceDate;
/** 结算日期 */
@ExcelProperty(value = "结算日期", index = 25)
@ExcelProperty(value = "结算日期", index = 20)
@ColumnWidth(18)
private Date settlementDate;
/** 申请人工号 */
@ExcelProperty(value = "申请人工号", index = 26)
@ExcelProperty(value = "申请人工号", index = 21)
@ColumnWidth(15)
@Required
private String applicantId;
/** 申请人姓名 */
@ExcelProperty(value = "申请人姓名", index = 27)
@ExcelProperty(value = "申请人姓名", index = 22)
@ColumnWidth(15)
@Required
private String applicantName;
/** 申请部门 */
@ExcelProperty(value = "申请部门", index = 28)
@ExcelProperty(value = "申请部门", index = 23)
@ColumnWidth(18)
@Required
private String applyDepartment;
/** 采购负责人工号 */
@ExcelProperty(value = "采购负责人工号", index = 29)
@ExcelProperty(value = "采购负责人工号", index = 24)
@ColumnWidth(15)
private String purchaseLeaderId;
/** 采购负责人姓名 */
@ExcelProperty(value = "采购负责人姓名", index = 30)
@ExcelProperty(value = "采购负责人姓名", index = 25)
@ColumnWidth(15)
private String purchaseLeaderName;
/** 采购部门 */
@ExcelProperty(value = "采购部门", index = 31)
@ExcelProperty(value = "采购部门", index = 26)
@ColumnWidth(18)
private String purchaseDepartment;
}

View File

@@ -0,0 +1,55 @@
package com.ruoyi.info.collection.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招投标供应商明细Excel对象
*/
@Data
public class CcdiPurchaseTransactionSupplierExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "采购事项ID", index = 0)
@ColumnWidth(20)
@Required
private String purchaseId;
@ExcelProperty(value = "供应商名称", index = 1)
@ColumnWidth(25)
@Required
private String supplierName;
@ExcelProperty(value = "供应商统一信用代码", index = 2)
@ColumnWidth(25)
private String supplierUscc;
@ExcelProperty(value = "供应商联系人", index = 3)
@ColumnWidth(15)
private String contactPerson;
@ExcelProperty(value = "供应商联系电话", index = 4)
@ColumnWidth(18)
private String contactPhone;
@ExcelProperty(value = "供应商银行账户", index = 5)
@ColumnWidth(20)
private String supplierBankAccount;
@ExcelProperty(value = "是否中标", index = 6)
@ColumnWidth(12)
@DictDropdown(dictType = "ccdi_yes_no_flag")
private String isBidWinner;
@ExcelProperty(value = "排序", index = 7)
@ColumnWidth(10)
private Integer sortOrder;
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 招投标供应商明细VO
*/
@Data
@Schema(description = "招投标供应商明细")
public class CcdiPurchaseTransactionSupplierVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
private Long id;
@Schema(description = "采购事项ID")
private String purchaseId;
@Schema(description = "供应商名称")
private String supplierName;
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
@Schema(description = "供应商联系人")
private String contactPerson;
@Schema(description = "供应商联系电话")
private String contactPhone;
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
@Schema(description = "是否中标1-是0-否")
private Integer isBidWinner;
@Schema(description = "排序")
private Integer sortOrder;
}

View File

@@ -8,15 +8,16 @@ import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 采购交易信息VO
* 招投标信息VO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息")
@Schema(description = "招投标信息")
public class CcdiPurchaseTransactionVO implements Serializable {
@Serial
@@ -90,6 +91,14 @@ public class CcdiPurchaseTransactionVO implements Serializable {
@Schema(description = "供应商银行账户")
private String supplierBankAccount;
/** 参与供应商数 */
@Schema(description = "参与供应商数")
private Integer supplierCount;
/** 供应商明细 */
@Schema(description = "供应商明细列表")
private List<CcdiPurchaseTransactionSupplierVO> supplierList;
/** 采购申请日期(或立项日期) */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "采购申请日期")

View File

@@ -6,13 +6,13 @@ import lombok.Data;
import java.math.BigDecimal;
/**
* 采购交易信息导入失败记录VO
* 招投标信息导入失败记录VO
*
* @author ruoyi
* @date 2026-02-06
*/
@Data
@Schema(description = "采购交易信息导入失败记录")
@Schema(description = "招投标信息导入失败记录")
public class PurchaseTransactionImportFailureVO {
/** 采购事项ID */

View File

@@ -35,6 +35,14 @@ public interface CcdiPurchaseTransactionMapper extends BaseMapper<CcdiPurchaseTr
*/
CcdiPurchaseTransactionVO selectTransactionById(@Param("purchaseId") String purchaseId);
/**
* 删除指定采购事项ID对应的供应商明细
*
* @param purchaseIds 采购事项ID列表
* @return 删除行数
*/
int deleteSuppliersByPurchaseIds(@Param("purchaseIds") List<String> purchaseIds);
/**
* 批量插入采购交易数据
*

View File

@@ -0,0 +1,12 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier;
import org.apache.ibatis.annotations.Mapper;
/**
* 招投标供应商明细Mapper
*/
@Mapper
public interface CcdiPurchaseTransactionSupplierMapper extends BaseMapper<CcdiPurchaseTransactionSupplier> {
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.PurchaseTransactionImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
@@ -17,11 +18,17 @@ public interface ICcdiPurchaseTransactionImportService {
/**
* 异步导入采购交易数据
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param userName 当前用户名
* @param mainExcelList 主信息Excel数据列表
* @param supplierExcelList 供应商明细Excel数据列表
* @param taskId 任务ID
* @param userName 当前用户名
*/
void importTransactionAsync(List<CcdiPurchaseTransactionExcel> excelList, String taskId, String userName);
void importTransactionAsync(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList,
String taskId,
String userName
);
/**
* 查询导入状态

View File

@@ -5,6 +5,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import java.util.List;
@@ -77,8 +78,12 @@ public interface ICcdiPurchaseTransactionService {
/**
* 导入采购交易数据(异步)
*
* @param excelList Excel实体列表
* @param mainExcelList 主信息Excel实体列表
* @param supplierExcelList 供应商明细Excel实体列表
* @return 任务ID
*/
String importTransaction(List<CcdiPurchaseTransactionExcel> excelList);
String importTransaction(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
);
}

View File

@@ -2,12 +2,15 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransaction;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.PurchaseTransactionImportFailureVO;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionMapper;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionSupplierMapper;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.StringUtils;
@@ -41,79 +44,119 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
@Resource
private CcdiPurchaseTransactionMapper transactionMapper;
@Resource
private CcdiPurchaseTransactionSupplierMapper supplierMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
@Async
@Transactional
public void importTransactionAsync(List<CcdiPurchaseTransactionExcel> excelList, String taskId, String userName) {
public void importTransactionAsync(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList,
String taskId,
String userName
) {
long startTime = System.currentTimeMillis();
List<CcdiPurchaseTransactionExcel> safeMainList = mainExcelList == null ? List.of() : mainExcelList;
List<CcdiPurchaseTransactionSupplierExcel> safeSupplierList = supplierExcelList == null ? List.of() : supplierExcelList;
int totalCount = countImportUnits(safeMainList, safeSupplierList);
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "采购交易信息", excelList.size(), userName);
ImportLogUtils.logImportStart(log, taskId, "招投标信息维护", totalCount, userName);
List<CcdiPurchaseTransaction> newRecords = new ArrayList<>();
List<CcdiPurchaseTransaction> newTransactions = new ArrayList<>();
List<CcdiPurchaseTransactionSupplier> newSuppliers = new ArrayList<>();
List<PurchaseTransactionImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的采购事项ID
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的采购事项ID", excelList.size());
Set<String> existingIds = getExistingPurchaseIds(excelList);
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的采购事项ID", safeMainList.size());
Set<String> existingIds = getExistingPurchaseIds(safeMainList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "采购事项ID", existingIds.size());
// 用于跟踪Excel文件内已处理的采购事项ID
Set<String> processedIds = new HashSet<>();
Map<String, List<CcdiPurchaseTransactionExcel>> mainGroupMap = safeMainList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getPurchaseId()))
.collect(Collectors.groupingBy(
CcdiPurchaseTransactionExcel::getPurchaseId,
LinkedHashMap::new,
Collectors.toList()
));
Map<String, List<CcdiPurchaseTransactionSupplierExcel>> supplierGroupMap = safeSupplierList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getPurchaseId()))
.collect(Collectors.groupingBy(
CcdiPurchaseTransactionSupplierExcel::getPurchaseId,
LinkedHashMap::new,
Collectors.toList()
));
LinkedHashSet<String> purchaseIds = new LinkedHashSet<>();
purchaseIds.addAll(mainGroupMap.keySet());
purchaseIds.addAll(supplierGroupMap.keySet());
// 分类数据
for (int i = 0; i < excelList.size(); i++) {
CcdiPurchaseTransactionExcel excel = excelList.get(i);
for (CcdiPurchaseTransactionSupplierExcel supplierExcel : safeSupplierList) {
if (StringUtils.isEmpty(supplierExcel.getPurchaseId())) {
failures.add(buildFailure(null, null, "供应商明细Sheet中的采购事项ID不能为空"));
}
}
int index = 0;
for (String purchaseId : purchaseIds) {
index++;
List<CcdiPurchaseTransactionExcel> mainRows = mainGroupMap.getOrDefault(purchaseId, List.of());
List<CcdiPurchaseTransactionSupplierExcel> supplierRows = supplierGroupMap.getOrDefault(purchaseId, List.of());
try {
// 转换为AddDTO进行验证
CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证数据
validateTransactionData(addDTO, existingIds);
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(excel, transaction);
if (existingIds.contains(excel.getPurchaseId())) {
// 采购事项ID已存在直接报错
throw new RuntimeException(String.format("采购事项ID[%s]已存在,请勿重复导入", excel.getPurchaseId()));
} else if (processedIds.contains(excel.getPurchaseId())) {
// Excel文件内部重复
throw new RuntimeException(String.format("采购事项ID[%s]在导入文件中重复,已跳过此条记录", excel.getPurchaseId()));
} else {
transaction.setCreatedBy(userName);
transaction.setUpdatedBy(userName);
newRecords.add(transaction);
processedIds.add(excel.getPurchaseId()); // 标记为已处理
if (existingIds.contains(purchaseId)) {
throw new RuntimeException(String.format("采购事项ID[%s]已存在,请勿重复导入", purchaseId));
}
if (mainRows.isEmpty()) {
throw new RuntimeException(String.format("采购事项ID[%s]缺少招投标主信息", purchaseId));
}
if (mainRows.size() > 1) {
throw new RuntimeException(String.format("采购事项ID[%s]在招投标主信息Sheet中重复", purchaseId));
}
CcdiPurchaseTransactionExcel mainExcel = mainRows.getFirst();
CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO();
BeanUtils.copyProperties(mainExcel, addDTO);
validateTransactionData(addDTO);
List<CcdiPurchaseTransactionSupplier> suppliers = buildSupplierEntities(purchaseId, supplierRows, userName);
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(mainExcel, transaction);
fillWinnerSummary(transaction, suppliers);
transaction.setCreatedBy(userName);
transaction.setUpdatedBy(userName);
newTransactions.add(transaction);
newSuppliers.addAll(suppliers);
// 记录进度
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
newRecords.size(), failures.size());
ImportLogUtils.logProgress(log, taskId, index, Math.max(totalCount, purchaseIds.size()),
newTransactions.size(), failures.size());
} catch (Exception e) {
PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
CcdiPurchaseTransactionExcel mainExcel = mainRows.isEmpty() ? null : mainRows.getFirst();
failures.add(buildFailure(mainExcel, purchaseId, e.getMessage()));
// 记录验证失败日志
String keyData = String.format("采购事项ID=%s, 采购类别=%s, 标的物=%s",
excel.getPurchaseId(), excel.getPurchaseCategory(), excel.getSubjectName());
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
purchaseId,
mainExcel == null ? "" : mainExcel.getPurchaseCategory(),
mainExcel == null ? "" : mainExcel.getSubjectName());
ImportLogUtils.logValidationError(log, taskId, index, e.getMessage(), keyData);
}
}
// 批量插入新数据
if (!newRecords.isEmpty()) {
if (!newTransactions.isEmpty()) {
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
(newRecords.size() + 499) / 500, 500);
saveBatch(newRecords, 500);
(newTransactions.size() + 499) / 500, 500);
saveBatch(newTransactions, 500);
}
if (!newSuppliers.isEmpty()) {
saveSupplierBatch(newSuppliers, 500);
}
// 保存失败记录到Redis
@@ -128,8 +171,8 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size());
result.setTotalCount(totalCount);
result.setSuccessCount(newTransactions.size());
result.setFailureCount(failures.size());
// 更新最终状态
@@ -138,8 +181,8 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
// 记录导入完成
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "采购交易信息",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
ImportLogUtils.logImportComplete(log, taskId, "招投标信息维护",
totalCount, result.getSuccessCount(), result.getFailureCount(), duration);
}
/**
@@ -243,13 +286,22 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
}
}
private void saveSupplierBatch(List<CcdiPurchaseTransactionSupplier> list, int batchSize) {
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiPurchaseTransactionSupplier> subList = list.subList(i, end);
for (CcdiPurchaseTransactionSupplier supplier : subList) {
supplierMapper.insert(supplier);
}
}
}
/**
* 验证采购交易数据
*
* @param addDTO 新增DTO
* @param existingIds 已存在的采购事项ID集合
* @param addDTO 新增DTO
*/
private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, Set<String> existingIds) {
private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getPurchaseId())) {
throw new RuntimeException("采购事项ID不能为空");
@@ -310,4 +362,161 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
throw new RuntimeException("结算金额必须大于0");
}
}
private List<CcdiPurchaseTransactionSupplier> buildSupplierEntities(
String purchaseId,
List<CcdiPurchaseTransactionSupplierExcel> supplierRows,
String userName
) {
List<CcdiPurchaseTransactionSupplierExcel> normalizedRows = supplierRows == null
? List.of()
: supplierRows.stream()
.filter(Objects::nonNull)
.filter(this::hasAnySupplierValue)
.toList();
long winnerCount = normalizedRows.stream()
.filter(item -> parseIsBidWinner(item.getIsBidWinner()) == 1)
.count();
if (winnerCount > 1) {
throw new RuntimeException(String.format("采购事项ID[%s]存在多条中标供应商", purchaseId));
}
Set<String> duplicateSupplierKeys = new LinkedHashSet<>();
List<CcdiPurchaseTransactionSupplier> result = new ArrayList<>();
for (int i = 0; i < normalizedRows.size(); i++) {
CcdiPurchaseTransactionSupplierExcel supplierRow = normalizedRows.get(i);
validateSupplierRow(supplierRow);
String duplicateKey = StringUtils.trimToEmpty(supplierRow.getSupplierName()) + "|"
+ StringUtils.trimToEmpty(supplierRow.getSupplierUscc());
if (!duplicateSupplierKeys.add(duplicateKey)) {
throw new RuntimeException(String.format("采购事项ID[%s]存在重复供应商", purchaseId));
}
CcdiPurchaseTransactionSupplier supplier = new CcdiPurchaseTransactionSupplier();
supplier.setPurchaseId(purchaseId);
supplier.setSupplierName(StringUtils.trim(supplierRow.getSupplierName()));
supplier.setSupplierUscc(StringUtils.trimToNull(supplierRow.getSupplierUscc()));
supplier.setContactPerson(StringUtils.trimToNull(supplierRow.getContactPerson()));
supplier.setContactPhone(StringUtils.trimToNull(supplierRow.getContactPhone()));
supplier.setSupplierBankAccount(StringUtils.trimToNull(supplierRow.getSupplierBankAccount()));
supplier.setIsBidWinner(parseIsBidWinner(supplierRow.getIsBidWinner()));
supplier.setSortOrder(supplierRow.getSortOrder() == null ? i + 1 : supplierRow.getSortOrder());
supplier.setCreatedBy(userName);
supplier.setUpdatedBy(userName);
result.add(supplier);
}
return result;
}
private void validateSupplierRow(CcdiPurchaseTransactionSupplierExcel supplierRow) {
if (StringUtils.isEmpty(supplierRow.getSupplierName())) {
throw new RuntimeException("供应商名称不能为空");
}
if (StringUtils.length(supplierRow.getSupplierName()) > 200) {
throw new RuntimeException("供应商名称长度不能超过200个字符");
}
if (StringUtils.length(supplierRow.getContactPerson()) > 50) {
throw new RuntimeException("供应商联系人长度不能超过50个字符");
}
if (StringUtils.length(supplierRow.getSupplierBankAccount()) > 50) {
throw new RuntimeException("供应商银行账户长度不能超过50个字符");
}
if (StringUtils.isNotEmpty(supplierRow.getContactPhone())
&& !supplierRow.getContactPhone().matches("^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$")) {
throw new RuntimeException("供应商联系电话格式不正确");
}
if (StringUtils.isNotEmpty(supplierRow.getSupplierUscc())
&& !supplierRow.getSupplierUscc().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) {
throw new RuntimeException("供应商统一信用代码格式不正确");
}
parseIsBidWinner(supplierRow.getIsBidWinner());
}
private boolean hasAnySupplierValue(CcdiPurchaseTransactionSupplierExcel supplierRow) {
return StringUtils.isNotEmpty(supplierRow.getPurchaseId())
|| StringUtils.isNotEmpty(supplierRow.getSupplierName())
|| StringUtils.isNotEmpty(supplierRow.getSupplierUscc())
|| StringUtils.isNotEmpty(supplierRow.getContactPerson())
|| StringUtils.isNotEmpty(supplierRow.getContactPhone())
|| StringUtils.isNotEmpty(supplierRow.getSupplierBankAccount())
|| StringUtils.isNotEmpty(supplierRow.getIsBidWinner())
|| supplierRow.getSortOrder() != null;
}
private int parseIsBidWinner(String rawValue) {
if (StringUtils.isEmpty(rawValue)) {
return 0;
}
String normalized = StringUtils.trim(rawValue);
if ("1".equals(normalized) || "".equals(normalized) || "Y".equalsIgnoreCase(normalized)
|| "TRUE".equalsIgnoreCase(normalized)) {
return 1;
}
if ("0".equals(normalized) || "".equals(normalized) || "N".equalsIgnoreCase(normalized)
|| "FALSE".equalsIgnoreCase(normalized)) {
return 0;
}
throw new RuntimeException("是否中标仅支持填写“是/否”或“1/0”");
}
private void fillWinnerSummary(
CcdiPurchaseTransaction transaction,
List<CcdiPurchaseTransactionSupplier> supplierList
) {
CcdiPurchaseTransactionSupplier winner = supplierList.stream()
.filter(item -> Objects.equals(item.getIsBidWinner(), 1))
.findFirst()
.orElse(null);
if (winner == null) {
transaction.setSupplierName(null);
transaction.setSupplierUscc(null);
transaction.setContactPerson(null);
transaction.setContactPhone(null);
transaction.setSupplierBankAccount(null);
return;
}
transaction.setSupplierName(winner.getSupplierName());
transaction.setSupplierUscc(winner.getSupplierUscc());
transaction.setContactPerson(winner.getContactPerson());
transaction.setContactPhone(winner.getContactPhone());
transaction.setSupplierBankAccount(winner.getSupplierBankAccount());
}
private PurchaseTransactionImportFailureVO buildFailure(
CcdiPurchaseTransactionExcel mainExcel,
String purchaseId,
String errorMessage
) {
PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO();
if (mainExcel != null) {
BeanUtils.copyProperties(mainExcel, failure);
}
if (StringUtils.isNotEmpty(purchaseId)) {
failure.setPurchaseId(purchaseId);
}
failure.setErrorMessage(errorMessage);
return failure;
}
private int countImportUnits(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
) {
LinkedHashSet<String> purchaseIds = new LinkedHashSet<>();
purchaseIds.addAll(
mainExcelList.stream()
.map(CcdiPurchaseTransactionExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
purchaseIds.addAll(
supplierExcelList.stream()
.map(CcdiPurchaseTransactionSupplierExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
return purchaseIds.size();
}
}

View File

@@ -2,24 +2,34 @@ package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransaction;
import com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionQueryDTO;
import com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionSupplierDTO;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionExcel;
import com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO;
import com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionMapper;
import com.ruoyi.info.collection.mapper.CcdiPurchaseTransactionSupplierMapper;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionImportService;
import com.ruoyi.info.collection.service.ICcdiPurchaseTransactionService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -39,6 +49,9 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Resource
private ICcdiPurchaseTransactionImportService transactionImportService;
@Resource
private CcdiPurchaseTransactionSupplierMapper supplierMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@@ -93,7 +106,14 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
*/
@Override
public CcdiPurchaseTransactionVO selectTransactionById(String purchaseId) {
return transactionMapper.selectTransactionById(purchaseId);
CcdiPurchaseTransactionVO detail = transactionMapper.selectTransactionById(purchaseId);
if (detail == null) {
return null;
}
List<CcdiPurchaseTransactionSupplierVO> supplierList = selectSupplierListByPurchaseId(purchaseId);
detail.setSupplierList(supplierList);
detail.setSupplierCount(supplierList.size());
return detail;
}
/**
@@ -110,9 +130,12 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
throw new RuntimeException("该采购事项ID已存在");
}
List<CcdiPurchaseTransactionSupplier> supplierList = buildSupplierEntities(addDTO.getPurchaseId(), addDTO.getSupplierList());
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(addDTO, transaction);
fillWinnerSummary(transaction, supplierList);
int result = transactionMapper.insert(transaction);
saveSuppliers(supplierList);
return result;
}
@@ -126,9 +149,13 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Override
@Transactional
public int updateTransaction(CcdiPurchaseTransactionEditDTO editDTO) {
List<CcdiPurchaseTransactionSupplier> supplierList = buildSupplierEntities(editDTO.getPurchaseId(), editDTO.getSupplierList());
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
BeanUtils.copyProperties(editDTO, transaction);
fillWinnerSummary(transaction, supplierList);
int result = transactionMapper.updateById(transaction);
transactionMapper.deleteSuppliersByPurchaseIds(List.of(editDTO.getPurchaseId()));
saveSuppliers(supplierList);
return result;
}
@@ -142,19 +169,24 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
@Override
@Transactional
public int deleteTransactionByIds(String[] purchaseIds) {
transactionMapper.deleteSuppliersByPurchaseIds(List.of(purchaseIds));
return transactionMapper.deleteBatchIds(java.util.List.of(purchaseIds));
}
/**
* 导入采购交易数据(异步)
*
* @param excelList Excel实体列表
* @param mainExcelList 主信息Excel实体列表
* @param supplierExcelList 供应商明细Excel实体列表
* @return 任务ID
*/
@Override
@Transactional
public String importTransaction(java.util.List<CcdiPurchaseTransactionExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
public String importTransaction(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
) {
if ((mainExcelList == null || mainExcelList.isEmpty()) && (supplierExcelList == null || supplierExcelList.isEmpty())) {
throw new RuntimeException("至少需要一条数据");
}
@@ -170,7 +202,7 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("totalCount", countImportUnits(mainExcelList, supplierExcelList));
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
@@ -181,8 +213,134 @@ public class CcdiPurchaseTransactionServiceImpl implements ICcdiPurchaseTransact
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
// 调用异步导入服务
transactionImportService.importTransactionAsync(excelList, taskId, userName);
transactionImportService.importTransactionAsync(mainExcelList, supplierExcelList, taskId, userName);
return taskId;
}
private int countImportUnits(
List<CcdiPurchaseTransactionExcel> mainExcelList,
List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList
) {
LinkedHashSet<String> purchaseIds = new LinkedHashSet<>();
if (mainExcelList != null) {
purchaseIds.addAll(
mainExcelList.stream()
.map(CcdiPurchaseTransactionExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
}
if (supplierExcelList != null) {
purchaseIds.addAll(
supplierExcelList.stream()
.map(CcdiPurchaseTransactionSupplierExcel::getPurchaseId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toCollection(LinkedHashSet::new))
);
}
return purchaseIds.isEmpty() ? 0 : purchaseIds.size();
}
private List<CcdiPurchaseTransactionSupplier> buildSupplierEntities(
String purchaseId,
List<CcdiPurchaseTransactionSupplierDTO> supplierDTOList
) {
List<CcdiPurchaseTransactionSupplierDTO> normalizedList = normalizeSupplierList(supplierDTOList);
validateSupplierList(normalizedList);
List<CcdiPurchaseTransactionSupplier> supplierList = new ArrayList<>();
for (int i = 0; i < normalizedList.size(); i++) {
CcdiPurchaseTransactionSupplierDTO dto = normalizedList.get(i);
CcdiPurchaseTransactionSupplier supplier = new CcdiPurchaseTransactionSupplier();
BeanUtils.copyProperties(dto, supplier);
supplier.setPurchaseId(purchaseId);
supplier.setIsBidWinner(Objects.equals(dto.getIsBidWinner(), 1) ? 1 : 0);
supplier.setSortOrder(dto.getSortOrder() == null ? i + 1 : dto.getSortOrder());
supplierList.add(supplier);
}
return supplierList;
}
private List<CcdiPurchaseTransactionSupplierDTO> normalizeSupplierList(
List<CcdiPurchaseTransactionSupplierDTO> supplierDTOList
) {
if (supplierDTOList == null) {
return List.of();
}
return supplierDTOList.stream()
.filter(Objects::nonNull)
.filter(this::hasAnySupplierValue)
.toList();
}
private boolean hasAnySupplierValue(CcdiPurchaseTransactionSupplierDTO supplierDTO) {
return StringUtils.isNotEmpty(supplierDTO.getSupplierName())
|| StringUtils.isNotEmpty(supplierDTO.getSupplierUscc())
|| StringUtils.isNotEmpty(supplierDTO.getContactPerson())
|| StringUtils.isNotEmpty(supplierDTO.getContactPhone())
|| StringUtils.isNotEmpty(supplierDTO.getSupplierBankAccount())
|| supplierDTO.getIsBidWinner() != null
|| supplierDTO.getSortOrder() != null;
}
private void validateSupplierList(List<CcdiPurchaseTransactionSupplierDTO> supplierList) {
long winnerCount = supplierList.stream()
.filter(item -> Objects.equals(item.getIsBidWinner(), 1))
.count();
if (winnerCount > 1) {
throw new RuntimeException("同一招投标事项仅允许维护一条中标供应商");
}
LinkedHashSet<String> duplicateKeys = new LinkedHashSet<>();
for (CcdiPurchaseTransactionSupplierDTO supplier : supplierList) {
String duplicateKey = StringUtils.trimToEmpty(supplier.getSupplierName()) + "|"
+ StringUtils.trimToEmpty(supplier.getSupplierUscc());
if (!duplicateKeys.add(duplicateKey)) {
throw new RuntimeException("同一招投标事项存在重复供应商,请检查供应商名称和统一信用代码");
}
}
}
private void fillWinnerSummary(
CcdiPurchaseTransaction transaction,
List<CcdiPurchaseTransactionSupplier> supplierList
) {
CcdiPurchaseTransactionSupplier winnerSupplier = supplierList.stream()
.filter(item -> Objects.equals(item.getIsBidWinner(), 1))
.findFirst()
.orElse(null);
if (winnerSupplier == null) {
transaction.setSupplierName(null);
transaction.setSupplierUscc(null);
transaction.setContactPerson(null);
transaction.setContactPhone(null);
transaction.setSupplierBankAccount(null);
return;
}
transaction.setSupplierName(winnerSupplier.getSupplierName());
transaction.setSupplierUscc(winnerSupplier.getSupplierUscc());
transaction.setContactPerson(winnerSupplier.getContactPerson());
transaction.setContactPhone(winnerSupplier.getContactPhone());
transaction.setSupplierBankAccount(winnerSupplier.getSupplierBankAccount());
}
private void saveSuppliers(List<CcdiPurchaseTransactionSupplier> supplierList) {
for (CcdiPurchaseTransactionSupplier supplier : supplierList) {
supplierMapper.insert(supplier);
}
}
private List<CcdiPurchaseTransactionSupplierVO> selectSupplierListByPurchaseId(String purchaseId) {
return supplierMapper.selectList(
new LambdaQueryWrapper<CcdiPurchaseTransactionSupplier>()
.eq(CcdiPurchaseTransactionSupplier::getPurchaseId, purchaseId)
.orderByAsc(CcdiPurchaseTransactionSupplier::getSortOrder)
.orderByAsc(CcdiPurchaseTransactionSupplier::getId)
).stream().map(entity -> {
CcdiPurchaseTransactionSupplierVO vo = new CcdiPurchaseTransactionSupplierVO();
BeanUtils.copyProperties(entity, vo);
return vo;
}).collect(Collectors.toList());
}
}

View File

@@ -1,6 +1,8 @@
package com.ruoyi.info.collection.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.info.collection.handler.DictDropdownWriteHandler;
@@ -98,6 +100,23 @@ public class EasyExcelUtil {
}
}
/**
* 导入Excel按指定Sheet名称
*
* @param inputStream 输入流
* @param clazz 实体类
* @param sheetName 工作表名称
* @param <T> 泛型
* @return 数据列表
*/
public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
try {
return EasyExcel.read(inputStream).head(clazz).sheet(sheetName).doReadSync();
} catch (Exception e) {
throw new RuntimeException("导入Excel失败", e);
}
}
/**
* 下载导入模板
*
@@ -210,6 +229,45 @@ public class EasyExcelUtil {
}
}
/**
* 下载双Sheet导入模板带字典下拉框
*
* @param response 响应对象
* @param firstClazz 第一张Sheet实体类
* @param firstSheetName 第一张Sheet名称
* @param secondClazz 第二张Sheet实体类
* @param secondSheetName 第二张Sheet名称
* @param fileName 文件名称
* @param <T1> 第一张Sheet泛型
* @param <T2> 第二张Sheet泛型
*/
public static <T1, T2> void importTemplateWithDictDropdown(
HttpServletResponse response,
Class<T1> firstClazz,
String firstSheetName,
Class<T2> secondClazz,
String secondSheetName,
String fileName
) {
setResponseHeader(response, fileName);
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream()).build()) {
writer.write(List.of(), buildTemplateSheet(0, firstClazz, firstSheetName));
writer.write(List.of(), buildTemplateSheet(1, secondClazz, secondSheetName));
} catch (IOException e) {
throw new RuntimeException("下载双Sheet导入模板失败", e);
}
}
private static <T> WriteSheet buildTemplateSheet(int sheetNo, Class<T> clazz, String sheetName) {
return EasyExcel.writerSheet(sheetNo, sheetName)
.head(clazz)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new TextFormatWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))
.build();
}
/**
* 导出Excel带字典下拉框
* 导出的数据包含实际值,但模板中有下拉框供后续编辑使用

View File

@@ -23,6 +23,7 @@
<result property="contactPhone" column="contact_phone"/>
<result property="supplierUscc" column="supplier_uscc"/>
<result property="supplierBankAccount" column="supplier_bank_account"/>
<result property="supplierCount" column="supplier_count"/>
<result property="applyDate" column="apply_date"/>
<result property="planApproveDate" column="plan_approve_date"/>
<result property="announceDate" column="announce_date"/>
@@ -47,49 +48,61 @@
<!-- 分页查询采购交易列表 -->
<select id="selectTransactionPage" resultMap="CcdiPurchaseTransactionVOResult">
SELECT
purchase_id, purchase_category, project_name, subject_name, subject_desc,
purchase_qty, budget_amount, bid_amount, actual_amount, contract_amount, settlement_amount,
purchase_method, supplier_name, contact_person, contact_phone, supplier_uscc, supplier_bank_account,
apply_date, plan_approve_date, announce_date, bid_open_date, contract_sign_date,
expected_delivery_date, actual_delivery_date, acceptance_date, settlement_date,
applicant_id, applicant_name, apply_department, purchase_leader_id, purchase_leader_name, purchase_department,
created_by, create_time, updated_by, update_time
FROM ccdi_purchase_transaction
t.purchase_id, t.purchase_category, t.project_name, t.subject_name, t.subject_desc,
t.purchase_qty, t.budget_amount, t.bid_amount, t.actual_amount, t.contract_amount, t.settlement_amount,
t.purchase_method, t.supplier_name, t.contact_person, t.contact_phone, t.supplier_uscc, t.supplier_bank_account,
IFNULL(supplier_stats.supplier_count, 0) AS supplier_count,
t.apply_date, t.plan_approve_date, t.announce_date, t.bid_open_date, t.contract_sign_date,
t.expected_delivery_date, t.actual_delivery_date, t.acceptance_date, t.settlement_date,
t.applicant_id, t.applicant_name, t.apply_department, t.purchase_leader_id, t.purchase_leader_name, t.purchase_department,
t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_purchase_transaction t
LEFT JOIN (
SELECT purchase_id, COUNT(*) AS supplier_count
FROM ccdi_purchase_transaction_supplier
GROUP BY purchase_id
) supplier_stats ON supplier_stats.purchase_id = t.purchase_id
<where>
<if test="query.projectName != null and query.projectName != ''">
AND project_name LIKE CONCAT('%', #{query.projectName}, '%')
AND t.project_name LIKE CONCAT('%', #{query.projectName}, '%')
</if>
<if test="query.subjectName != null and query.subjectName != ''">
AND subject_name LIKE CONCAT('%', #{query.subjectName}, '%')
AND t.subject_name LIKE CONCAT('%', #{query.subjectName}, '%')
</if>
<if test="query.applicantName != null and query.applicantName != ''">
AND applicant_name LIKE CONCAT('%', #{query.applicantName}, '%')
AND t.applicant_name LIKE CONCAT('%', #{query.applicantName}, '%')
</if>
<if test="query.applicantId != null and query.applicantId != ''">
AND applicant_id = #{query.applicantId}
AND t.applicant_id = #{query.applicantId}
</if>
<if test="query.applyDateStart != null">
AND apply_date &gt;= #{query.applyDateStart}
AND t.apply_date &gt;= #{query.applyDateStart}
</if>
<if test="query.applyDateEnd != null">
AND apply_date &lt;= #{query.applyDateEnd}
AND t.apply_date &lt;= #{query.applyDateEnd}
</if>
</where>
ORDER BY create_time DESC
ORDER BY t.create_time DESC
</select>
<!-- 查询采购交易详情 -->
<select id="selectTransactionById" resultMap="CcdiPurchaseTransactionVOResult">
SELECT
purchase_id, purchase_category, project_name, subject_name, subject_desc,
purchase_qty, budget_amount, bid_amount, actual_amount, contract_amount, settlement_amount,
purchase_method, supplier_name, contact_person, contact_phone, supplier_uscc, supplier_bank_account,
apply_date, plan_approve_date, announce_date, bid_open_date, contract_sign_date,
expected_delivery_date, actual_delivery_date, acceptance_date, settlement_date,
applicant_id, applicant_name, apply_department, purchase_leader_id, purchase_leader_name, purchase_department,
created_by, create_time, updated_by, update_time
FROM ccdi_purchase_transaction
WHERE purchase_id = #{purchaseId}
t.purchase_id, t.purchase_category, t.project_name, t.subject_name, t.subject_desc,
t.purchase_qty, t.budget_amount, t.bid_amount, t.actual_amount, t.contract_amount, t.settlement_amount,
t.purchase_method, t.supplier_name, t.contact_person, t.contact_phone, t.supplier_uscc, t.supplier_bank_account,
IFNULL(supplier_stats.supplier_count, 0) AS supplier_count,
t.apply_date, t.plan_approve_date, t.announce_date, t.bid_open_date, t.contract_sign_date,
t.expected_delivery_date, t.actual_delivery_date, t.acceptance_date, t.settlement_date,
t.applicant_id, t.applicant_name, t.apply_department, t.purchase_leader_id, t.purchase_leader_name, t.purchase_department,
t.created_by, t.create_time, t.updated_by, t.update_time
FROM ccdi_purchase_transaction t
LEFT JOIN (
SELECT purchase_id, COUNT(*) AS supplier_count
FROM ccdi_purchase_transaction_supplier
GROUP BY purchase_id
) supplier_stats ON supplier_stats.purchase_id = t.purchase_id
WHERE t.purchase_id = #{purchaseId}
</select>
<!-- 批量插入采购交易数据 -->
@@ -137,4 +150,12 @@
</foreach>
</update>
<delete id="deleteSuppliersByPurchaseIds">
DELETE FROM ccdi_purchase_transaction_supplier
WHERE purchase_id IN
<foreach collection="purchaseIds" item="purchaseId" open="(" separator="," close=")">
#{purchaseId}
</foreach>
</delete>
</mapper>

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
/**
@@ -80,4 +81,6 @@ public class CcdiProjectExtendedPurchaseDetailVO {
private String updatedBy;
private String updateTime;
private List<CcdiProjectExtendedPurchaseSupplierVO> supplierList;
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 专项核查采购供应商明细
*/
@Data
public class CcdiProjectExtendedPurchaseSupplierVO {
private Long id;
private String purchaseId;
private String supplierName;
private String supplierUscc;
private String contactPerson;
private String contactPhone;
private String supplierBankAccount;
private Integer isBidWinner;
private Integer sortOrder;
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseSupplierVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
@@ -96,6 +97,18 @@ public interface CcdiProjectSpecialCheckMapper {
@Param("purchaseId") String purchaseId
);
/**
* 查询专项核查采购供应商明细
*
* @param projectId 项目ID
* @param purchaseId 采购事项ID
* @return 供应商明细
*/
List<CcdiProjectExtendedPurchaseSupplierVO> selectExtendedPurchaseSuppliers(
@Param("projectId") Long projectId,
@Param("purchaseId") String purchaseId
);
/**
* 查询专项核查招聘拓展列表
*

View File

@@ -103,6 +103,9 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
if (detail == null) {
throw new ServiceException("当前记录不属于该项目专项核查范围");
}
detail.setSupplierList(defaultList(
specialCheckMapper.selectExtendedPurchaseSuppliers(queryDTO.getProjectId(), queryDTO.getPurchaseId())
));
return detail;
}

View File

@@ -574,6 +574,33 @@
where p.purchase_id = #{purchaseId}
</select>
<select id="selectExtendedPurchaseSuppliers"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseSupplierVO">
select
s.id,
s.purchase_id,
s.supplier_name,
s.supplier_uscc,
s.contact_person,
s.contact_phone,
s.supplier_bank_account,
s.is_bid_winner,
s.sort_order
from ccdi_purchase_transaction_supplier s
inner join ccdi_purchase_transaction p on p.purchase_id = s.purchase_id
inner join (
select distinct scope.staff_name
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
) scoped_staff
on scoped_staff.staff_name = p.applicant_name
where s.purchase_id = #{purchaseId}
order by s.sort_order asc, s.id asc
</select>
<select id="selectExtendedRecruitmentPage" resultMap="ExtendedRecruitmentListItemResultMap">
<bind name="projectId" value="query.projectId"/>
select distinct r.recruit_id,

View File

@@ -0,0 +1,35 @@
# 招投标信息维护后端实施计划
## 目标
- 将现有 `purchaseTransaction` 后端链路改造为“招投标主信息 + 供应商明细子表”结构。
- 保留原有 URL、权限前缀和内部类名统一用户可见文案为“招投标信息维护”。
- 支持详情查询返回全部供应商明细,列表返回中标供应商摘要和参与供应商数。
- 支持双 Sheet 导入模板与按 `purchaseId` 聚合校验的异步导入。
## 实施内容
- 数据层
- 新增 `ccdi_purchase_transaction_supplier` 明细表初始化 SQL 与增量迁移脚本。
- 迁移脚本回填历史中标供应商数据,并将菜单名称更新为“招投标信息维护”。
- 领域模型
- 新增供应商 entity、DTO、VO、Excel 模型。
- 主 DTO/VO 增加 `supplierList`,主 VO 增加 `supplierCount`
- 主 Excel 模板改为仅承载招投标主信息,供应商明细独立建模。
- 接口与服务
- 列表 SQL 增加供应商数聚合。
- 详情查询补充供应商明细列表。
- 新增/修改时由 `supplierList` 自动回填主表中标供应商摘要字段。
- 删除主记录时级联删除供应商明细。
- 导入链路改为“双 Sheet 读取 + 按事项聚合校验 + 主从同落库”。
- 项目专项核查
- 项目采购详情 VO、Mapper、Service 增加供应商明细查询能力,保持项目详情与信息维护详情口径一致。
## 验证
- `mvn -pl ccdi-info-collection,ccdi-project -am -DskipTests compile`
- `./bin/restart_java_backend.sh restart`
- 浏览器验证列表接口、详情接口与项目详情供应商明细展示。
## 产出文件
- `sql/ccdi_purchase_transaction.sql`
- `sql/ccdi_purchase_transaction_menu.sql`
- `sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql`
- `ccdi-info-collection``ccdi-project` 相关后端代码

View File

@@ -0,0 +1,24 @@
# 招投标信息维护弹窗宽度与供应商序号前端实施计划
## 保存路径确认
- 前端实施计划保存到 `docs/plans/frontend/`
## 目标
- 新增、编辑弹窗宽度调整为页面宽度的 `80%`
- 供应商明细不再录入排序,改为页面按当前行顺序自动生成序号与提交顺序
## 实施范围
- 文件:`ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
## 实施步骤
1. 将新增/编辑共用弹窗宽度从固定像素改为 `80%`
2. 将供应商明细表中的“排序”输入列改为只读“序号”展示列
3. 调整供应商新增与提交逻辑,提交时按当前行顺序自动补齐 `sortOrder`
4. 清理空白供应商行判断,避免隐藏排序字段后把空白行误判为有效数据
5. 使用真实页面验证新增/编辑弹窗显示与供应商录入行为
## 验证点
- 新增/编辑弹窗宽度明显放大至页面宽度的 `80%`
- 供应商明细不再出现排序输入框
- 新增供应商后可正常录入供应商名称、统一信用代码、联系人、联系电话、银行账户
- 编辑已有数据时供应商顺序按当前表格行顺序提交

View File

@@ -0,0 +1,33 @@
# 招投标信息维护前端实施计划
## 目标
- 将信息维护下“采购交易管理”页面改造为“招投标信息维护”。
- 列表页展示中标供应商和参与供应商数,新增/编辑弹窗支持维护全部供应商明细。
- 详情弹窗与项目详情采购弹窗统一展示供应商明细表。
- 导入入口文案和模板命名改为招投标信息维护语义。
## 实施内容
- 页面改造
- 重写 `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- 将原单供应商表单替换为可增删行的供应商明细表格。
- 使用单选方式标记唯一中标供应商,并在提交时写回 `supplierList.isBidWinner`
- 列表页新增“参与供应商数”列,保留“中标供应商”摘要列。
- 详情展示
- 页面详情弹窗改为供应商明细表展示。
- 项目专项核查采购详情弹窗同步改为供应商明细表展示。
- 导入交互
- 导入弹窗文案更新为“招投标信息导入”。
- 模板下载文件名与提示改为双 Sheet 模板。
## 验证
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod`
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run dev -- --port 8080`
- 使用 Playwright 打开真实页面:
- 验证菜单显示“招投标信息维护”
- 验证列表显示“中标供应商”“参与供应商数”
- 验证新增弹窗可添加供应商明细
- 验证详情弹窗展示供应商明细表
## 产出文件
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ExtendedPurchaseDetailDialog.vue`

View File

@@ -0,0 +1,36 @@
# 招聘信息新增编辑弹窗宽度调整前端实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将招聘信息管理页“新增招聘信息”“修改招聘信息”共用弹窗的宽度从固定 `900px` 调整为页面宽度的 `80%`,提升表单内容展示空间。
**Architecture:** 本次仅调整 `ccdiStaffRecruitment` 页面新增/编辑弹窗的 `el-dialog` 宽度配置,不新增字段、不改动详情弹窗、不调整提交逻辑和接口交互。
**Tech Stack:** Vue 2, Element UI, JavaScript, Markdown
---
## 文件结构与职责
- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`
调整新增/编辑招聘信息弹窗宽度配置。
## 实施步骤
- [x] 定位招聘信息管理页新增/编辑弹窗的实际模板位置
- [x] 将新增/编辑弹窗宽度从固定像素改为 `80%`
- [x] 确认详情弹窗与其他弹窗保持原样,不扩大影响范围
- [x] 运行前端构建校验模板改动未引入语法问题
- [x] 使用浏览器实际打开新增/编辑弹窗,确认宽度按页面宽度 `80%` 展示
## 验证
```bash
source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod
```
## 完成标准
- 招聘信息管理页新增/编辑弹窗宽度调整为页面宽度的 `80%`
- 招聘信息详情弹窗和页面其他交互保持不变
- 页面构建成功,且浏览器实测可正常打开新增/编辑弹窗

View File

@@ -0,0 +1,24 @@
# 招投标信息维护弹窗宽度与供应商序号实施记录
## 保存路径确认
- 实施记录保存到 `docs/reports/implementation/`
## 本次修改
- 将招投标信息新增/编辑弹窗宽度由固定 `1200px` 调整为 `80%`
- 将供应商明细中的“排序”录入列改为只读“序号”展示列
- 供应商新增行不再预填可编辑排序值
- 提交时按供应商当前行顺序自动回填 `sortOrder`
- 空白供应商行判定不再依赖 `sortOrder`
## 影响范围
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
## 验证方式
- 启动前端开发服务进行页面热更新验证
- 使用 Playwright 打开真实页面 `http://localhost:8080/maintain/purchaseTransaction`
- 验证编辑弹窗宽度、供应商明细列展示与新增供应商录入行为
## 结果
- 弹窗宽度调整生效
- 供应商排序输入已移除
- 供应商录入功能正常

View File

@@ -0,0 +1,47 @@
# 招投标信息维护实施记录
## 本次改动
- 将信息维护下“采购交易管理”改为“招投标信息维护”,保留原有 `purchaseTransaction` 技术标识。
- 新增供应商明细子表 `ccdi_purchase_transaction_supplier`,支持维护全部参标供应商,并使用 `is_bid_winner` 标记中标方。
- 主表继续保留中标供应商摘要字段,新增/修改/导入时从供应商明细自动回填。
- 列表查询新增供应商数聚合;详情查询和项目专项核查采购详情新增供应商明细返回。
- 导入模板改为“招投标主信息 + 供应商明细”双 Sheet并按 `purchaseId` 聚合校验。
- 前端页面改造为多供应商明细表单,详情弹窗与项目详情弹窗改为供应商明细表展示。
## 关键文件
- 后端
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java`
- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiPurchaseTransactionMapper.xml`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml`
- 前端
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ExtendedPurchaseDetailDialog.vue`
- SQL
- `sql/ccdi_purchase_transaction.sql`
- `sql/ccdi_purchase_transaction_menu.sql`
- `sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql`
## 验证结果
- 后端编译
- 命令:`mvn -pl ccdi-info-collection,ccdi-project -am -DskipTests compile`
- 结果:通过。
- 前端构建
- 命令:`source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod`
- 结果:通过,仅有既有产物体积 warning。
- 数据迁移
- 命令:`bin/mysql_utf8_exec.sh sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql`
- 结果:执行成功,本地联调库菜单名已更新为“招投标信息维护”。
- Playwright 实页验证
- 验证页面:`http://localhost:8080/maintain/purchaseTransaction`
- 结果:
- 菜单与面包屑显示“招投标信息维护”
- 列表显示“中标供应商”“参与供应商数”
- 新增弹窗显示供应商明细表,并可新增供应商行
- 详情弹窗显示供应商明细表与中标标识
- 联调过程中发现列表 SQL 因 join 后字段未加别名导致 `purchase_id is ambiguous`,已修复并复验通过。
## 测试进程清理
- 已关闭 Playwright 浏览器会话。
- 已停止前端 `npm run dev` 进程。
- 已停止测试期间启动的后端进程。

View File

@@ -0,0 +1,28 @@
# ruoyi-ui 添加 .nvmrc 实施记录
## 文档信息
- 保存路径:`docs/reports/implementation/2026-04-22-ruoyi-ui-nvmrc-implementation.md`
- 实施日期2026-04-22
- 关联范围:`ruoyi-ui` 前端工程本地 Node 版本约束
## 本次修改内容
1. 确认 `docs/` 内近期前端实施计划与实施记录均使用 `nvm use 14.21.3` 作为 `ruoyi-ui` 的实际 Node 版本。
2.`ruoyi-ui` 目录新增 `.nvmrc`,写入 `14.21.3`,统一本地开发、构建与测试时的 Node 版本入口。
3. 保持 `package.json` 现有 `engines` 配置不变,本次仅补充 `nvm` 版本声明文件,不扩展其他构建配置。
## 影响范围
- 前端:`ruoyi-ui/.nvmrc`
- 文档:本实施记录
## 验证情况
1. 版本依据核对:
- 已核对 `docs/plans/frontend/``docs/reports/implementation/` 中近期前端构建命令,当前统一使用 `nvm use 14.21.3`
2. 本地命令校验:
- 执行命令:`source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use`
- 预期结果:自动读取 `.nvmrc` 并切换到 `v14.21.3`
3. 说明:
- 本次为 Node 版本声明文件补充,不涉及业务代码、页面交互或接口行为变更,未执行额外前端构建与页面测试。

View File

@@ -0,0 +1,30 @@
# 招聘信息新增编辑弹窗宽度调整实施记录
## 文档信息
- 保存路径:`docs/reports/implementation/2026-04-22-staff-recruitment-dialog-width-implementation.md`
- 实施日期2026-04-22
- 关联范围:招聘信息管理前端页面
## 本次修改内容
1. 定位到招聘信息管理页新增/编辑共用同一个 `el-dialog`,原宽度固定为 `900px`
2. 将新增/编辑弹窗宽度改为 `80%`,使弹窗按页面可视宽度自适应放大。
3. 保持招聘信息详情弹窗宽度和原有表单提交流程不变,避免影响非本次需求范围的交互。
4. 新增本次前端实施计划与实施记录,沉淀改动背景、影响范围与验证结论。
## 影响范围
- 前端:`ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue` 新增/编辑招聘信息弹窗。
- 文档:新增前端实施计划与实施记录。
## 验证情况
1. 前端构建校验:
- 执行命令:`source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod`
- 结果:构建成功;存在项目原有的 bundle size warnings本次改动未引入新的构建错误。
2. Playwright 浏览器实测:
- 执行方式:通过 `nvm use 25.9.0` 启动 `playwright-cli`,打开 `http://127.0.0.1:1025/prototype/staff-recruitment?preview=1&mode=add``http://127.0.0.1:1025/prototype/staff-recruitment?preview=1&mode=edit`,在真实浏览器中读取可见 `el-dialog` 的实际宽度。
- 结果:新增弹窗标题为“添加招聘信息”,页面宽度 `1280px`、弹窗宽度 `1024px`、宽度占比 `0.8`;编辑弹窗标题为“修改招聘信息”,页面宽度 `1280px`、弹窗宽度 `1024px`、宽度占比 `0.8`,与需求一致。
3. 测试进程清理:
- 已关闭本次测试过程中临时启动的 Playwright 浏览器与前端 dev server。

1
ruoyi-ui/.nvmrc Normal file
View File

@@ -0,0 +1 @@
14.21.3

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog title="采购记录详情" :visible.sync="visibleProxy" width="1000px" append-to-body>
<el-dialog title="招投标记录详情" :visible.sync="visibleProxy" width="1100px" append-to-body>
<div class="detail-container">
<el-divider content-position="left">基本信息</el-divider>
<el-descriptions :column="2" border>
@@ -21,14 +21,27 @@
<el-descriptions-item label="结算金额(元)">{{ detail.settlementAmount || "-" }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">供应商信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="中标供应商名称">{{ detail.supplierName || "-" }}</el-descriptions-item>
<el-descriptions-item label="统一信用代码">{{ detail.supplierUscc || "-" }}</el-descriptions-item>
<el-descriptions-item label="供应商联系人">{{ detail.contactPerson || "-" }}</el-descriptions-item>
<el-descriptions-item label="供应商联系电话">{{ detail.contactPhone || "-" }}</el-descriptions-item>
<el-descriptions-item label="供应商银行账户" :span="2">{{ detail.supplierBankAccount || "-" }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">供应商明细</el-divider>
<el-empty
v-if="!supplierList.length"
:image-size="72"
description="未录入供应商信息"
/>
<el-table v-else :data="supplierList" border size="small">
<el-table-column label="排序" prop="sortOrder" width="90" align="center" />
<el-table-column label="中标结果" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.isBidWinner === 1 ? 'danger' : 'info'" size="mini">
{{ scope.row.isBidWinner === 1 ? "中标" : "参标" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="供应商名称" prop="supplierName" min-width="220" :show-overflow-tooltip="true" />
<el-table-column label="统一信用代码" prop="supplierUscc" min-width="180" :show-overflow-tooltip="true" />
<el-table-column label="供应商联系人" prop="contactPerson" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="供应商联系电话" prop="contactPhone" min-width="150" :show-overflow-tooltip="true" />
<el-table-column label="供应商银行账户" prop="supplierBankAccount" min-width="180" :show-overflow-tooltip="true" />
</el-table>
<el-divider content-position="left">日期信息</el-divider>
<el-descriptions :column="3" border>
@@ -77,12 +90,12 @@ export default {
props: {
visible: {
type: Boolean,
default: false,
default: false
},
detail: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
},
computed: {
visibleProxy: {
@@ -91,8 +104,11 @@ export default {
},
set(value) {
this.$emit("update:visible", value);
},
}
},
},
supplierList() {
return Array.isArray(this.detail && this.detail.supplierList) ? this.detail.supplierList : [];
}
}
};
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -225,7 +225,7 @@
/>
<!-- 添加或修改对话框 -->
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-divider content-position="left">招聘岗位信息</el-divider>
<el-row :gutter="16">

View File

@@ -1,4 +1,4 @@
-- 员工采购交易信息表
-- 招投标主信息表
CREATE TABLE `ccdi_purchase_transaction` (
`purchase_id` VARCHAR(32) NOT NULL COMMENT '采购事项ID',
`purchase_category` VARCHAR(50) NOT NULL COMMENT '采购类别',
@@ -41,4 +41,24 @@ CREATE TABLE `ccdi_purchase_transaction` (
KEY `idx_apply_date` (`apply_date`),
KEY `idx_supplier_uscc` (`supplier_uscc`),
KEY `idx_category_method` (`purchase_category`, `purchase_method`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工采购交易信息表';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='招投标主信息表';
-- 招投标供应商明细表
CREATE TABLE `ccdi_purchase_transaction_supplier` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`purchase_id` VARCHAR(32) NOT NULL COMMENT '采购事项ID',
`supplier_name` VARCHAR(200) NOT NULL COMMENT '供应商名称',
`supplier_uscc` VARCHAR(18) DEFAULT NULL COMMENT '供应商统一信用代码',
`contact_person` VARCHAR(50) DEFAULT NULL COMMENT '供应商联系人',
`contact_phone` VARCHAR(20) DEFAULT NULL COMMENT '供应商联系电话',
`supplier_bank_account` VARCHAR(50) DEFAULT NULL COMMENT '供应商银行账户',
`is_bid_winner` TINYINT NOT NULL DEFAULT 0 COMMENT '是否中标1-是0-否',
`sort_order` INT NOT NULL DEFAULT 1 COMMENT '排序',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`created_by` VARCHAR(50) DEFAULT NULL COMMENT '创建人',
`updated_by` VARCHAR(50) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`),
KEY `idx_purchase_id` (`purchase_id`),
KEY `idx_purchase_bid_winner` (`purchase_id`, `is_bid_winner`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='招投标供应商明细表';

View File

@@ -1,4 +1,4 @@
-- 添加采购交易管理菜单
-- 添加招投标信息维护菜单
-- 注意: 执行前请确认已存在"信息维护"父菜单
-- 如果不存在,请先执行以下语句创建父菜单:
-- INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
@@ -7,21 +7,21 @@
-- 查询信息维护父菜单ID
SET @parent_menu_id = (SELECT menu_id FROM sys_menu WHERE menu_name='信息维护' AND parent_id=0 LIMIT 1);
-- 添加采购交易管理菜单
-- 添加招投标信息维护菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES
('采购交易管理', @parent_menu_id, 12, 'purchaseTransaction', 'ccdiPurchaseTransaction/index', 1, 0, 'C', '0', '0', 'ccdi:purchaseTransaction:list', 'shopping', 'admin', NOW(), '', NULL, '采购交易信息管理菜单');
('招投标信息维护', @parent_menu_id, 12, 'purchaseTransaction', 'ccdiPurchaseTransaction/index', 1, 0, 'C', '0', '0', 'ccdi:purchaseTransaction:list', 'shopping', 'admin', NOW(), '', NULL, '招投标信息维护菜单');
-- 获取刚插入的菜单ID
SET @menu_id = LAST_INSERT_ID();
-- 添加按钮权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES
('采购交易查询', @menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:query', '#', 'admin', NOW(), ''),
('采购交易新增', @menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:add', '#', 'admin', NOW(), ''),
('采购交易修改', @menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:edit', '#', 'admin', NOW(), ''),
('采购交易删除', @menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:remove', '#', 'admin', NOW(), ''),
('采购交易导入', @menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:import', '#', 'admin', NOW(), '');
('招投标信息查询', @menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:query', '#', 'admin', NOW(), ''),
('招投标信息新增', @menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:add', '#', 'admin', NOW(), ''),
('招投标信息修改', @menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:edit', '#', 'admin', NOW(), ''),
('招投标信息删除', @menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:remove', '#', 'admin', NOW(), ''),
('招投标信息导入', @menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'ccdi:purchaseTransaction:import', '#', 'admin', NOW(), '');
-- 查询结果验证
SELECT
@@ -40,5 +40,5 @@ SELECT
m.create_time AS '创建时间'
FROM sys_menu m
LEFT JOIN sys_menu p ON m.parent_id = p.menu_id
WHERE m.menu_name = '采购交易管理' OR m.parent_id = @menu_id
WHERE m.menu_name = '招投标信息维护' OR m.parent_id = @menu_id
ORDER BY m.parent_id, m.order_num;

View File

@@ -0,0 +1,81 @@
-- 招投标信息维护:供应商明细子表、历史数据回填与菜单改名
CREATE TABLE IF NOT EXISTS `ccdi_purchase_transaction_supplier` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`purchase_id` VARCHAR(32) NOT NULL COMMENT '采购事项ID',
`supplier_name` VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '供应商名称',
`supplier_uscc` VARCHAR(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '供应商统一信用代码',
`contact_person` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '供应商联系人',
`contact_phone` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '供应商联系电话',
`supplier_bank_account` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '供应商银行账户',
`is_bid_winner` TINYINT NOT NULL DEFAULT 0 COMMENT '是否中标1-是0-否',
`sort_order` INT NOT NULL DEFAULT 1 COMMENT '排序',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`created_by` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`updated_by` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`),
KEY `idx_purchase_id` (`purchase_id`),
KEY `idx_purchase_bid_winner` (`purchase_id`, `is_bid_winner`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='招投标供应商明细表';
INSERT INTO ccdi_purchase_transaction_supplier (
purchase_id,
supplier_name,
supplier_uscc,
contact_person,
contact_phone,
supplier_bank_account,
is_bid_winner,
sort_order,
create_time,
update_time,
created_by,
updated_by
)
SELECT
t.purchase_id,
t.supplier_name,
t.supplier_uscc,
t.contact_person,
t.contact_phone,
t.supplier_bank_account,
1,
1,
IFNULL(t.create_time, NOW()),
IFNULL(t.update_time, NOW()),
t.created_by,
t.updated_by
FROM ccdi_purchase_transaction t
LEFT JOIN ccdi_purchase_transaction_supplier s
ON s.purchase_id = t.purchase_id
AND s.is_bid_winner = 1
WHERE IFNULL(t.supplier_name, '') <> ''
AND s.id IS NULL;
UPDATE sys_menu
SET menu_name = '招投标信息维护',
remark = '招投标信息维护菜单',
update_by = 'admin',
update_time = NOW()
WHERE perms = 'ccdi:purchaseTransaction:list'
OR path = 'purchaseTransaction';
UPDATE sys_menu
SET menu_name = CASE perms
WHEN 'ccdi:purchaseTransaction:query' THEN '招投标信息查询'
WHEN 'ccdi:purchaseTransaction:add' THEN '招投标信息新增'
WHEN 'ccdi:purchaseTransaction:edit' THEN '招投标信息修改'
WHEN 'ccdi:purchaseTransaction:remove' THEN '招投标信息删除'
WHEN 'ccdi:purchaseTransaction:import' THEN '招投标信息导入'
ELSE menu_name
END,
update_by = 'admin',
update_time = NOW()
WHERE perms IN (
'ccdi:purchaseTransaction:query',
'ccdi:purchaseTransaction:add',
'ccdi:purchaseTransaction:edit',
'ccdi:purchaseTransaction:remove',
'ccdi:purchaseTransaction:import'
);