feat: 完成履约管理模块全量功能迭代

本次迭代包含以下核心功能:
1. 新增履约时效总览可视化页面,支持多维度数据统计
2. 实现物料/客户/供应商的Excel批量导入导出功能
3. 新增订单批量结单功能,优化结单流程校验
4. 完善日志配置,新增文件日志落地
5. 修复分类查询逻辑,优化多租户数据隔离
6. 新增甲方履约结单管理页面与权限控制
7. 重构部分Mapper与Service接口,增强代码健壮性
This commit is contained in:
2026-06-18 11:10:36 +08:00
parent 7a8e4297e0
commit 7b71822a32
37 changed files with 1759 additions and 66 deletions

View File

@@ -1,14 +1,17 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.bid.BizClient;
import com.ruoyi.system.service.bid.IBizClientService;
@@ -60,6 +63,35 @@ public class BizClientController extends BaseController {
return toAjax(service.deleteBizClientByIds(clientIds));
}
// ========== Excel 导入导出 ==========
@PreAuthorize("@ss.hasPermi('bid:client:export')")
@Log(title = "甲方客户", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BizClient query) {
List<BizClient> list = service.selectBizClientList(query);
ExcelUtil<BizClient> util = new ExcelUtil<BizClient>(BizClient.class);
util.exportExcel(response, list, "客户数据");
}
@PreAuthorize("@ss.hasPermi('bid:client:import')")
@Log(title = "甲方客户", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<BizClient> util = new ExcelUtil<BizClient>(BizClient.class);
List<BizClient> clientList = util.importExcel(file.getInputStream());
Long tenantId = getDeptId();
if (tenantId == null) { tenantId = 1L; }
String message = service.importClient(clientList, updateSupport, getUsername(), tenantId);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<BizClient> util = new ExcelUtil<BizClient>(BizClient.class);
util.importTemplateExcel(response, "客户数据");
}
/**
* 查询客户的关联历史发货单
* 链路: client → client_quote → rfq → delivery_order

View File

@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -83,13 +84,27 @@ public class BizDeliveryOrderController extends BaseController {
return toAjax(service.recall(id));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit')")
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit') or @ss.hasPermi('bid:clientdelivery:closeDate:edit')")
@Log(title = "结单时间", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/closeDate")
public AjaxResult setCloseDate(@PathVariable Long id, @RequestParam String closeDate) {
return toAjax(service.setCloseDate(id, closeDate, getUsername()));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit') or @ss.hasPermi('bid:clientdelivery:closeDate:edit')")
@Log(title = "结单时间", businessType = BusinessType.UPDATE)
@PutMapping("/batchCloseDate")
public AjaxResult batchSetCloseDate(@RequestBody Map<String, Object> params) {
@SuppressWarnings("unchecked")
List<Integer> rawIds = (List<Integer>) params.get("ids");
String closeDate = (String) params.get("closeDate");
if (rawIds == null || rawIds.isEmpty()) {
return AjaxResult.error("请选择要结单的订单");
}
List<Long> ids = rawIds.stream().map(Integer::longValue).collect(java.util.stream.Collectors.toList());
return toAjax(service.batchSetCloseDate(ids, closeDate, getUsername()));
}
// ════════════════════════════════════════
// 物料发货记录
// ════════════════════════════════════════════
@@ -107,18 +122,66 @@ public class BizDeliveryOrderController extends BaseController {
@PreAuthorize("@ss.hasPermi('bid:order:transit')")
@GetMapping("/transit/stats")
public AjaxResult transitStats() {
return success(service.selectTransitStats());
Long tenantId = getDeptId();
return success(service.selectTransitStats(tenantId));
}
@PreAuthorize("@ss.hasPermi('bid:order:history')")
@PreAuthorize("@ss.hasPermi('bid:order:history') or @ss.hasPermi('bid:clientdelivery:closeDate')")
@GetMapping("/history/stats")
public AjaxResult historyStats() {
return success(service.selectHistoryStats());
public AjaxResult historyStats(@RequestParam(required = false) String type) {
Long tenantId = getDeptId();
return success(service.selectHistoryStats(tenantId, type));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit')")
@GetMapping("/closeDate/stats")
public AjaxResult closeDateStats() {
return success(service.selectCloseDateStats());
Long tenantId = getDeptId();
return success(service.selectCloseDateStats(tenantId));
}
/** 履约时效可视化 - 时间线数据(供应商履约 + 甲方履约通用) */
@PreAuthorize("@ss.hasPermi('bid:order:timeline') or @ss.hasPermi('bid:clientdelivery:timeline') or @ss.hasPermi('bid:order:transit') or @ss.hasPermi('bid:order:history')")
@GetMapping("/timeline")
public AjaxResult timeline(@RequestParam(required = false) String type,
@RequestParam(required = false) String status,
@RequestParam(required = false) String dateFrom,
@RequestParam(required = false) String dateTo) {
Long tenantId = getDeptId();
List<Map<String, Object>> orders = service.selectTimelineData(tenantId, type, status, dateFrom, dateTo);
// 计算准时/延期统计数据
long onTime = orders.stream().filter(o -> {
Object cd = o.get("actualCloseDate");
Object dd = o.get("delayDate");
if (cd == null) return false;
// 有延期日期时以延期日期为准,否则以约定交货日为准
Object base = dd != null ? dd : o.get("deliveryDate");
if (base == null) return false;
try {
return java.sql.Date.valueOf(cd.toString()).compareTo(java.sql.Date.valueOf(base.toString())) <= 0;
} catch (Exception e) { return false; }
}).count();
long delayed = orders.stream().filter(o -> {
Object cd = o.get("actualCloseDate");
Object dd = o.get("delayDate");
if (cd == null) return false;
Object base = dd != null ? dd : o.get("deliveryDate");
if (base == null) return false;
try {
return java.sql.Date.valueOf(cd.toString()).compareTo(java.sql.Date.valueOf(base.toString())) > 0;
} catch (Exception e) { return false; }
}).count();
long pending = orders.stream().filter(o -> o.get("actualCloseDate") == null).count();
Map<String, Object> result = new java.util.HashMap<>();
result.put("orders", orders);
Map<String, Object> stats = new java.util.HashMap<>();
stats.put("onTime", onTime);
stats.put("delayed", delayed);
stats.put("pending", pending);
result.put("stats", stats);
return success(result);
}
}

View File

@@ -1,14 +1,17 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.bid.BizMaterial;
import com.ruoyi.system.service.bid.IBizMaterialService;
@@ -61,6 +64,35 @@ public class BizMaterialController extends BaseController {
return toAjax(service.deleteBizMaterialByIds(materialIds));
}
// ========== Excel 导入导出 ==========
@PreAuthorize("@ss.hasPermi('bid:material:export')")
@Log(title = "物料管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BizMaterial query) {
List<BizMaterial> list = service.selectBizMaterialList(query);
ExcelUtil<BizMaterial> util = new ExcelUtil<BizMaterial>(BizMaterial.class);
util.exportExcel(response, list, "物料数据");
}
@PreAuthorize("@ss.hasPermi('bid:material:import')")
@Log(title = "物料管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<BizMaterial> util = new ExcelUtil<BizMaterial>(BizMaterial.class);
List<BizMaterial> materialList = util.importExcel(file.getInputStream());
Long tenantId = getDeptId();
if (tenantId == null) { tenantId = 1L; }
String message = service.importMaterial(materialList, updateSupport, getUsername(), tenantId);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<BizMaterial> util = new ExcelUtil<BizMaterial>(BizMaterial.class);
util.importTemplateExcel(response, "物料数据");
}
// ========== 物料详情页接口 ==========
@PreAuthorize("@ss.hasPermi('bid:material:detail')")

View File

@@ -1,14 +1,17 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.bid.BizSupplier;
import com.ruoyi.system.service.bid.IBizSupplierService;
@@ -54,4 +57,33 @@ public class BizSupplierController extends BaseController {
public AjaxResult remove(@PathVariable Long[] supplierIds) {
return toAjax(service.deleteBizSupplierByIds(supplierIds));
}
// ========== Excel 导入导出 ==========
@PreAuthorize("@ss.hasPermi('bid:supplier:export')")
@Log(title = "供应商管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BizSupplier query) {
List<BizSupplier> list = service.selectBizSupplierList(query);
ExcelUtil<BizSupplier> util = new ExcelUtil<BizSupplier>(BizSupplier.class);
util.exportExcel(response, list, "供应商数据");
}
@PreAuthorize("@ss.hasPermi('bid:supplier:import')")
@Log(title = "供应商管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<BizSupplier> util = new ExcelUtil<BizSupplier>(BizSupplier.class);
List<BizSupplier> supplierList = util.importExcel(file.getInputStream());
Long tenantId = getDeptId();
if (tenantId == null) { tenantId = 1L; }
String message = service.importSupplier(supplierList, updateSupport, getUsername(), tenantId);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<BizSupplier> util = new ExcelUtil<BizSupplier>(BizSupplier.class);
util.importTemplateExcel(response, "供应商数据");
}
}

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<property name="log.path" value="logs" />
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
@@ -8,14 +9,38 @@
</encoder>
</appender>
<logger name="com.ruoyi" level="info" />
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/ruoyi.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/ruoyi.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<appender name="error-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<logger name="com.ruoyi" level="debug" />
<logger name="org.springframework" level="warn" />
<logger name="sys-user" level="info" />
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
<appender-ref ref="error-file" />
</root>
<logger name="sys-user" level="info">
<appender-ref ref="console" />
</logger>
</configuration>