feat(quotation): 添加部署工具类并优化折扣配置功能

- 在DateTimeUtil中新增parse方法支持自定义格式解析
- 新增DeployTest类实现SSH文件部署功能
- 将多个VO类的id字段从Integer改为Long类型
- 重构DiscountConfigController中的保存逻辑
- 在ModelConfigItemLanguageVO中添加创建时间和更新时间字段
- 修改相关Mapper XML文件以支持新增字段查询
- 优化折扣配置的数据持久化流程
- 修复折扣查询时的状态过滤条件
This commit is contained in:
曹鹏飞 2026-02-25 19:15:56 +08:00
parent d1f3aa8fd2
commit 85987c0b01
12 changed files with 435 additions and 31 deletions

View File

@ -7,7 +7,7 @@ import lombok.experimental.Accessors;
@Accessors(chain = true) @Accessors(chain = true)
public class DictionaryItemTranslateVO { public class DictionaryItemTranslateVO {
private Integer id; private Long id;
//语言代码 //语言代码
private String code; private String code;
@ -19,7 +19,7 @@ public class DictionaryItemTranslateVO {
private String value; private String value;
//字典值id //字典值id
private Integer dictionaryItemId; private Long dictionaryItemId;
//字典值编号 //字典值编号
private String dictionaryItemCode; private String dictionaryItemCode;

View File

@ -7,7 +7,7 @@ import java.time.LocalDateTime;
@Data @Data
public class DictionaryItemVO { public class DictionaryItemVO {
private Integer id; private Long id;
/** /**
* 字典值名称 * 字典值名称

View File

@ -7,6 +7,8 @@ import java.time.LocalDateTime;
@Data @Data
public class ModelConfigVO { public class ModelConfigVO {
private Long id;
/** /**
* 模块名称 * 模块名称
*/ */

View File

@ -1,8 +1,11 @@
package com.nflg.mobilebroken.common.pojo.vo.quotation; package com.nflg.mobilebroken.common.pojo.vo.quotation;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@Data @Data
@ -51,5 +54,25 @@ public class ModelConfigItemLanguageVO {
*/ */
private String imageUrl; private String imageUrl;
/**
* 创建人
*/
private String createBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新人
*/
private String updateBy;
/**
* 更新时间
*/
private LocalDateTime updateTime;
private List<ModelConfigItemLanguageVO> children; private List<ModelConfigItemLanguageVO> children;
} }

View File

@ -62,6 +62,10 @@ public class DateTimeUtil {
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)); return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN));
} }
public static LocalDateTime parse(String dateStr,String format) {
return LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern(format));
}
public static int between(LocalDate date1, LocalDate date2) { public static int between(LocalDate date1, LocalDate date2) {
return (int) ChronoUnit.DAYS.between(date1, date2) + 1; return (int) ChronoUnit.DAYS.between(date1, date2) + 1;
} }

View File

@ -1,6 +1,7 @@
package com.nflg.mobilebroken.quotation.controller.admin; package com.nflg.mobilebroken.quotation.controller.admin;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -12,26 +13,21 @@ import com.nflg.mobilebroken.common.pojo.request.ModelConfigSearchRequest;
import com.nflg.mobilebroken.common.pojo.request.ModelPriceSaveRequest; import com.nflg.mobilebroken.common.pojo.request.ModelPriceSaveRequest;
import com.nflg.mobilebroken.common.pojo.vo.DynamicHeaderVO; import com.nflg.mobilebroken.common.pojo.vo.DynamicHeaderVO;
import com.nflg.mobilebroken.common.pojo.vo.ModelDiscountConfigVO; import com.nflg.mobilebroken.common.pojo.vo.ModelDiscountConfigVO;
import com.nflg.mobilebroken.common.util.AdminUserUtil;
import com.nflg.mobilebroken.common.util.DateTimeUtil; import com.nflg.mobilebroken.common.util.DateTimeUtil;
import com.nflg.mobilebroken.common.util.NumberUtil; import com.nflg.mobilebroken.common.util.NumberUtil;
import com.nflg.mobilebroken.quotation.controller.ControllerBase; import com.nflg.mobilebroken.quotation.controller.ControllerBase;
import com.nflg.mobilebroken.repository.entity.DictionaryItem; import com.nflg.mobilebroken.repository.entity.*;
import com.nflg.mobilebroken.repository.entity.QuotationModelDiscountArea; import com.nflg.mobilebroken.repository.service.*;
import com.nflg.mobilebroken.repository.entity.QuotationModelPriceItemArea;
import com.nflg.mobilebroken.repository.service.IDictionaryItemService;
import com.nflg.mobilebroken.repository.service.IQuotationModelDiscountAreaService;
import com.nflg.mobilebroken.repository.service.IQuotationModelDiscountService;
import com.nflg.mobilebroken.repository.service.IQuotationModelPriceItemAreaService;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.time.LocalDateTime;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -53,6 +49,9 @@ public class DiscountConfigController extends ControllerBase {
@Resource @Resource
private IQuotationModelDiscountAreaService discountAreaService; private IQuotationModelDiscountAreaService discountAreaService;
@Resource
private IQuotationModelDiscountApplyService discountApplyService;
@Resource @Resource
private IQuotationModelPriceItemAreaService priceItemAreaService; private IQuotationModelPriceItemAreaService priceItemAreaService;
@ -103,7 +102,7 @@ public class DiscountConfigController extends ControllerBase {
List<QuotationModelDiscountArea> discountAreas = discountAreaService.lambdaQuery() List<QuotationModelDiscountArea> discountAreas = discountAreaService.lambdaQuery()
.in(QuotationModelDiscountArea::getDiscountId, pdata.getRecords().stream().map(ModelDiscountConfigVO::getId).collect(Collectors.toList())) .in(QuotationModelDiscountArea::getDiscountId, pdata.getRecords().stream().map(ModelDiscountConfigVO::getId).collect(Collectors.toList()))
.list(); .list();
return ApiResult.success(pdata,data->{ return ApiResult.success(pdata, data -> {
Map<String, Object> map = objectMapper.convertValue(data, new TypeReference<>() { Map<String, Object> map = objectMapper.convertValue(data, new TypeReference<>() {
}); });
areas.forEach(area -> { areas.forEach(area -> {
@ -127,12 +126,55 @@ public class DiscountConfigController extends ControllerBase {
}); });
} }
// /** /**
// * 保存 * 保存
// */ */
// @Transactional @Transactional
// @PostMapping("/save") @PostMapping("/save")
// public ApiResult<Void> save(@Valid @RequestBody List<ModelDiscountSaveRequest> datas) { public ApiResult<Void> save(@RequestBody @NotEmpty List<Map<String, Object>> datas) {
// List<Long> modelIds = datas.stream().map(data -> (Long) data.get("modelId")).collect(Collectors.toList());
// } List<QuotationModelDiscount> discounts = new ArrayList<>();
List<QuotationModelDiscountArea> discountAreas = new ArrayList<>();
List<QuotationModelDiscountApply> discountApplies = new ArrayList<>();
datas.forEach(data -> {
Long modelId = (Long) data.get("modelId");
QuotationModelDiscount discount = new QuotationModelDiscount()
.setId(IdUtil.getSnowflakeNextId())
.setModelId(modelId)
.setCreateById(AdminUserUtil.getUserId())
.setCreateBy(AdminUserUtil.getUserName())
.setCreateTime(LocalDateTime.now());
discounts.add(discount);
List<DictionaryItem> areas = dictionaryItemService.getListByDictionaryCode(Constant.DICTIONARY_DIRECT_SALES_CATEGORY);
for (DictionaryItem area : areas) {
if (data.containsKey(area.getCode() + "_ratio")) {
QuotationModelDiscountArea discountArea = new QuotationModelDiscountArea()
.setDiscountId(discount.getId())
.setAreaId(area.getId())
.setRatio(new BigDecimal(data.get(area.getCode() + "_ratio").toString()))
.setDiscountStartDate(DateTimeUtil.parse(data.get(area.getCode() + "_start").toString(), "yyyy-MM-dd"))
.setDiscountEndDate(DateTimeUtil.parse(data.get(area.getCode() + "_end").toString(), "yyyy-MM-dd"));
discountAreas.add(discountArea);
}
}
Integer[] userIds = (Integer[]) data.get("apply");
for (Integer userId : userIds) {
discountApplies.add(new QuotationModelDiscountApply()
.setDiscountId(discount.getId())
.setSourceId(userId)
.setCreateById(AdminUserUtil.getUserId())
.setCreateBy(AdminUserUtil.getUserName())
.setCreateTime(LocalDateTime.now())
);
}
});
discountService.lambdaUpdate()
.set(QuotationModelDiscount::getDiscountStatus, 2)
.in(QuotationModelDiscount::getModelId, modelIds)
.update();
discountService.saveBatch(discounts);
discountAreaService.saveBatch(discountAreas);
discountApplyService.saveBatch(discountApplies);
return ApiResult.success();
}
} }

View File

@ -0,0 +1,332 @@
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Test;
import java.io.*;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class DeployTest {
private static final String serviceName = "quotation";
private static final String localPath = System.getProperty("user.dir") + "//target//";
private static final String remotePath = "/mnt/mobilebroken/" + serviceName + "/";
private static final String jarName = "nflg-mobilebroken-" + serviceName + "-1.0.0-SNAPSHOT.jar";
@Test
public void DeployToSit() throws Exception {
SSHUtil sshUtil = new SSHUtil();
sshUtil.connect("192.168.163.73", 22, "root", "NFcfs2025");
//处理主jar包
handleFile(sshUtil, localPath + jarName, remotePath + jarName);
//处理字体目录
// handleDir(sshUtil, localPath, remotePath, "fonts");
//处理lib目录
// handleDir(sshUtil, localPath, remotePath, "lib");
//执行脚本启动服务
sshUtil.exec("cd " + remotePath + " && ./restart.sh");
sshUtil.disconnect();
}
private void handleDir(SSHUtil sshUtil, String localPath, String remotePath, String dirName) throws Exception {
printInfo("处理目录:" + dirName);
List<Path> files = getFileList(localPath + dirName);
for (Path file : files) {
handleFile(sshUtil, file.toString(), remotePath +dirName+ "/" + file.getFileName().toString());
}
printInfo("处理目录完成");
}
public List<Path> getFileList(String folderPath) throws IOException {
Path path = Paths.get(folderPath);
if (Files.exists(path) && Files.isDirectory(path)) {
List<Path> fileList = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
for (Path entry : stream) {
fileList.add(entry);
}
}
return fileList;
}
return null;
}
private void handleFile(SSHUtil sshUtil, String localPath, String remotePath) throws Exception {
BasicFileAttributes localFileAttr = Files.readAttributes(Paths.get(localPath), BasicFileAttributes.class);
printInfo("处理文件:{},大小:{}", localPath, getSize(localFileAttr.size()));
if (sshUtil.fileExists(remotePath)) {
if (!StrUtil.equals(getRemoteFileMD5(sshUtil, remotePath), getLocalFileMD5(localPath), true)) {
printError("文件不一致,开始上传");
sshUtil.uploadFile(localPath, remotePath);
} else {
printInfo("文件一致");
}
} else {
printError("文件不存在,开始上传");
sshUtil.uploadFile(localPath, remotePath);
}
printInfo("处理完成");
}
private String getRemoteFileMD5(SSHUtil sshUtil, String remotePath) throws Exception {
String md5 = StrUtil.subPre(sshUtil.execWithReturn("md5sum " + remotePath), 32);
printInfo("远程文件MD5为" + md5);
return md5;
}
private String getLocalFileMD5(String localPath) throws Exception {
String md5 = DigestUtils.md5Hex(new FileInputStream(localPath));
printInfo("本地文件MD5为" + md5);
return md5;
}
private String getSize(long size) {
long s = 1024;
if (size < s) {
return size + "B";
}
s = s * 1024;
if (size < s) {
return String.format("%.2f", size / 1.00 / 1024) + "KB";
}
s = s * 1024;
if (size < s) {
return String.format("%.2f", size / 1.00 / 1024 / 1024) + "MB";
}
s = s * 1024;
return String.format("%.2f", size / 1.00 / 1024 / 1024 / 1024) + "GB";
}
private static class SSHUtil {
private Session session;
private ChannelSftp channelSftp;
/**
* 建立SSH连接
*/
public void connect(String host, int port, String userName, String password) throws JSchException {
printInfo("开始连接服务器,ip:{},port:{},userName:{},password:{}", host, port, userName, password);
JSch jsch = new JSch();
session = jsch.getSession(userName, host, port);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
session.connect();
printInfo("连接成功");
Channel channel = session.openChannel("sftp");
channel.connect();
channelSftp = (ChannelSftp) channel;
}
/**
* 执行远程命令并返回输出
*/
public void exec(String command) {
printInfo("开始执行命令:{}", command);
if (session == null || !session.isConnected()) {
throw new IllegalStateException("SSH未连接");
}
ChannelExec channel = null;
SshResultCallback callback = new SshResultCallback() {
@Override
public void onOutput(String output) {
printInfo(false, output);
}
@Override
public void onErrorOutput(String errorOutput) {
printError(false, errorOutput);
}
@Override
public void onCompleted(int exitStatus) {
printInfo("输出完毕,退出状态:" + exitStatus);
}
@Override
public void onError(Exception e) {
printError(false, e.getMessage());
}
};
try {
// 创建执行命令的通道
channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(command);
// 获取输入流以读取命令输出
InputStream in = channel.getInputStream();
InputStream err = channel.getExtInputStream();
channel.connect();
// 读取命令输出
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
BufferedReader errReader = new BufferedReader(new InputStreamReader(err));
String line;
while ((line = reader.readLine()) != null) {
callback.onOutput(line);
}
// 读取错误输出
String errLine;
while ((errLine = errReader.readLine()) != null) {
callback.onErrorOutput(errLine);
}
// 获取退出状态
int exitStatus = channel.getExitStatus();
callback.onCompleted(exitStatus);
} catch (JSchException | IOException e) {
callback.onError(e);
} finally {
if (channel != null) {
channel.disconnect();
}
if (session != null) {
session.disconnect();
}
printInfo("执行命令完毕");
}
}
public String execWithReturn(String command) throws Exception {
printInfo("开始执行命令:{}", command);
if (session == null || !session.isConnected()) {
throw new IllegalStateException("SSH未连接");
}
ChannelExec channel = null;
BufferedReader reader = null;
try {
// 创建执行命令的通道
channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(command);
// 获取输入流以读取命令输出
InputStream in = channel.getInputStream();
InputStream err = channel.getExtInputStream();
channel.connect();
reader = new BufferedReader(new InputStreamReader(in));
String data = reader.readLine();
// log.info("执行命令成功,返回数据:" + data);
BufferedReader errReader = new BufferedReader(new InputStreamReader(err));
String errData = errReader.readLine();
// log.info("执行命令失败,返回数据:" + errData);
if (StrUtil.isBlank(errData)) {
return data;
} else {
errReader.close();
throw new Exception("执行命令失败:" + errData);
}
} finally {
if (channel != null) {
channel.disconnect();
}
if (reader != null) {
try {
reader.close();
} catch (Exception ignored) {
}
}
printInfo("执行命令完毕");
}
}
/**
* 检查远程文件是否存在
*/
public boolean fileExists(String remotePath) throws SftpException {
try {
channelSftp.stat(remotePath);
return true;
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
return false;
}
throw e;
}
}
/**
* 上传文件
*/
public void uploadFile(String localPath, String remotePath) throws SftpException {
printInfo("开始上传本地文件{}到远程{}", localPath, remotePath);
channelSftp.put(localPath, remotePath);
printInfo("上传完成");
}
/**
* 关闭连接
*/
public void disconnect() {
if (channelSftp != null) {
channelSftp.disconnect();
}
if (session != null) {
session.disconnect();
}
}
}
public interface SshResultCallback {
void onOutput(String output);
void onErrorOutput(String errorOutput);
void onCompleted(int exitStatus);
void onError(Exception e);
}
private static void printError(String msg) {
if (StrUtil.isNotBlank(msg)) {
System.out.println(red + msg + reset);
}
}
private static void printError(boolean addTime, String msg) {
if (addTime) {
System.out.println(red + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " " + msg + reset);
} else {
System.out.println(red + msg + reset);
}
}
private static void printInfo(String msg) {
printInfo(true, msg);
}
private static void printInfo(boolean addTime, String msg) {
if (addTime) {
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " " + msg);
} else {
System.out.println(msg);
}
}
private static void printInfo(String format, Object... args) {
printInfo(true, format, args);
}
private static void printInfo(boolean addTime, String format, Object... args) {
if (addTime) {
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " " + StrUtil.format(format, args));
} else {
System.out.println(StrUtil.format(format, args));
}
}
public static final String red = "\u001B[31m";
public static final String reset = "\u001B[0m";
}

View File

@ -28,8 +28,8 @@ public class QuotationModelDiscount implements Serializable {
/** /**
* 流水号 * 流水号
*/ */
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.ASSIGN_ID)
private Integer id; private Long id;
/** /**
* 机型编号 * 机型编号

View File

@ -28,13 +28,13 @@ public class QuotationModelDiscountApply implements Serializable {
/** /**
* 主键 * 主键
*/ */
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.ASSIGN_ID)
private Integer id; private Long id;
/** /**
* 折扣ID * 折扣ID
*/ */
private Integer discountId; private Long discountId;
/** /**
* 用户类型 0 内部用户1 代理商公司 * 用户类型 0 内部用户1 代理商公司

View File

@ -3,7 +3,8 @@
<mapper namespace="com.nflg.mobilebroken.repository.mapper.QuotationModelConfigItemMapper"> <mapper namespace="com.nflg.mobilebroken.repository.mapper.QuotationModelConfigItemMapper">
<select id="getVOListByConfigId" resultType="com.nflg.mobilebroken.common.pojo.vo.quotation.ModelConfigItemLanguageVO"> <select id="getVOListByConfigId" resultType="com.nflg.mobilebroken.common.pojo.vo.quotation.ModelConfigItemLanguageVO">
SELECT mcil.id,mci.parent_id as 'item_parent_id',mci.id as 'item_id',mcil.part_name,mcil.part_remark,mci.* SELECT mcil.id,mci.parent_id as 'item_parent_id',mci.id as 'item_id',mcil.part_name,mcil.part_remark
,mcil.create_by,mcil.create_time,mcil.update_by,mcil.update_time,mci.*
FROM quotation_model_config_item mci FROM quotation_model_config_item mci
INNER JOIN quotation_model_config_item_language mcil ON mci.id=mcil.config_item_id INNER JOIN quotation_model_config_item_language mcil ON mci.id=mcil.config_item_id
INNER JOIN `language` l ON l.id=mcil.language_id INNER JOIN `language` l ON l.id=mcil.language_id

View File

@ -3,7 +3,7 @@
<mapper namespace="com.nflg.mobilebroken.repository.mapper.QuotationModelConfigMapper"> <mapper namespace="com.nflg.mobilebroken.repository.mapper.QuotationModelConfigMapper">
<select id="search" resultType="com.nflg.mobilebroken.common.pojo.vo.ModelConfigVO"> <select id="search" resultType="com.nflg.mobilebroken.common.pojo.vo.ModelConfigVO">
SELECT di.`name` as 'moduleName',ps.`name` AS 'seriesName',pt.`name` as 'typeName',pm.batch_number AS 'modelId' SELECT qmc.id,di.`name` as 'moduleName',ps.`name` AS 'seriesName',pt.`name` as 'typeName',pm.batch_number AS 'modelId'
,pm.`no` as 'modelNo',pm.recommend,pm.image,qmc.config_version,qmc.create_by,qmc.create_time,qmc.update_by ,pm.`no` as 'modelNo',pm.recommend,pm.image,qmc.config_version,qmc.create_by,qmc.create_time,qmc.update_by
,qmc.update_time ,qmc.update_time
FROM product_model pm FROM product_model pm

View File

@ -10,7 +10,7 @@
LEFT JOIN product_type pt on pm.type_number=pt.batch_number AND pt.state=1 LEFT JOIN product_type pt on pm.type_number=pt.batch_number AND pt.state=1
LEFT JOIN product_series ps ON pm.series_number=ps.batch_number AND ps.state=1 LEFT JOIN product_series ps ON pm.series_number=ps.batch_number AND ps.state=1
LEFT JOIN dictionary_item di ON di.id=pm.module_id LEFT JOIN dictionary_item di ON di.id=pm.module_id
WHERE pm.state=1 WHERE pm.state=1 and qmd.discount_status=1
<where> <where>
<if test="request.moduleId!=null"> <if test="request.moduleId!=null">
AND pm.module_id=#{request.moduleId} AND pm.module_id=#{request.moduleId}