Commit d7f3b33d by 杨浩

网络安全修复

parent a9506ff9
......@@ -78,6 +78,11 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
</dependencies>
</project>
......@@ -37,6 +37,8 @@ import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
......@@ -194,4 +196,15 @@ public class FoodnexusTenantAutoConfiguration {
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
// 设置HttpOnly(关键配置,此方法无版本限制)
serializer.setUseHttpOnlyCookie(true);
// 补充其他安全配置
serializer.setSameSite("Lax"); // 防御CSRF
serializer.setCookiePath("/"); // 作用域
return serializer;
}
}
......@@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import java.util.HashMap;
......
......@@ -15,4 +15,7 @@ public class SupplierMonthOrderPageReqVO extends PageParam {
@Schema(description = "月度(yyyy-MM)")
private String month;
@Schema(description = "供应商id")
private Long supplierId;
}
......@@ -76,6 +76,9 @@ public class ErpPurchaseReturnPageReqVO extends PageParam {
@Schema(description = "客户订单code")
private String customerOrderCode;
@Schema(description = "客户订单code")
private String orderCode;
@Schema(description = "客户名称")
private String customerName;
......
......@@ -41,4 +41,7 @@ public class ErpSupplierPageReqVO extends PageParam {
@Schema(description = "状态")
private Integer status;
@Schema(description = "")
private String businessGoodsType;
}
\ No newline at end of file
......@@ -27,6 +27,9 @@ public class ErpSaleReturnPageReqVO extends PageParam {
@Schema(description = "客户编号", example = "1724")
private Long customerId;
@Schema(description = "客户名称")
private String customerName;
@Schema(description = "退货时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] returnTime;
......@@ -64,4 +67,7 @@ public class ErpSaleReturnPageReqVO extends PageParam {
@Schema(description = "客户订单id")
private Long customerOrderId;
@Schema(description = "客户订单编号")
private String customerOrderCode;
}
\ No newline at end of file
......@@ -164,7 +164,7 @@ public interface ErpPurchaseOrderMapper extends BaseMapperX<ErpPurchaseOrderDO>
wrapper.eq(CommonUtil.isNotEmpty(supplierId), "t.supplier_id", supplierId);
wrapper.in("t.delivery_status", CommonUtil.asList(ErpDeliveryStatus.RECONCILIATION.getStatus(),
ErpDeliveryStatus.ARRIVAL.getStatus()));
wrapper.in("t1.order_status", CommonUtil.asList(CustomerOrderStatus.SIGN_RECEIPT.getKey()), CustomerOrderStatus.FINISH.getKey());
wrapper.in("t1.order_status", CommonUtil.asList(CustomerOrderStatus.SIGN_RECEIPT.getKey(), CustomerOrderStatus.FINISH.getKey(), CustomerOrderStatus.RETURN.getKey()));
wrapper.eq(CommonUtil.isNotBlank(pageReqVO.getMonth()), "DATE_FORMAT(t.create_time, '%Y-%m') ", pageReqVO.getMonth());
wrapper.orderByDesc("t.id");
......
......@@ -76,6 +76,9 @@ public interface ErpPurchaseReturnMapper extends BaseMapperX<ErpPurchaseReturnDO
query.betweenIfPresent(ErpSaleReturnDO::getDeliveryTime, reqVO.getDeliveryTime());
query.groupBy(ErpPurchaseReturnDO::getId);
}
if (CommonUtil.isNotBlank(reqVO.getOrderCode())) {
reqVO.setCustomerOrderCode(reqVO.getOrderCode());
}
if (CommonUtil.isNotBlank(reqVO.getCustomerOrderCode())) {
query.leftJoin("order_customer_order oco3 on oco3.id = t.customer_order_id");
query.eq("oco3.code", reqVO.getCustomerOrderCode());
......
......@@ -29,6 +29,7 @@ public interface ErpSupplierMapper extends BaseMapperX<ErpSupplierDO> {
.likeIfPresent(ErpSupplierDO::getContact, reqVO.getContact())
.eqIfPresent(ErpSupplierDO::getAuditStatus, reqVO.getAuditStatus())
.eqIfPresent(ErpSupplierDO::getStatus, reqVO.getStatus())
.likeIfPresent(ErpSupplierDO::getBusinessGoodsType, reqVO.getBusinessGoodsType())
.orderByDesc(ErpSupplierDO::getId));
}
......
......@@ -6,6 +6,7 @@ import cn.iocoder.foodnexus.framework.common.util.CommonUtil;
import cn.iocoder.foodnexus.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.foodnexus.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.foodnexus.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnPageReqVO;
import cn.iocoder.foodnexus.module.erp.dal.dataobject.sale.ErpCustomerDO;
import cn.iocoder.foodnexus.module.erp.dal.dataobject.sale.ErpSaleOutDO;
import cn.iocoder.foodnexus.module.erp.dal.dataobject.sale.ErpSaleReturnDO;
import cn.iocoder.foodnexus.module.erp.dal.dataobject.sale.ErpSaleReturnItemDO;
......@@ -60,6 +61,16 @@ public interface ErpSaleReturnMapper extends BaseMapperX<ErpSaleReturnDO> {
.like(ProductSpuDO::getName, reqVO.getProductName())
.groupBy(ErpSaleReturnDO::getId);
}
if (CommonUtil.isNotEmpty(reqVO.getCustomerName())) {
query.leftJoin(ErpCustomerDO.class, ErpCustomerDO::getId, ErpSaleReturnDO::getCustomerId);
query.like(ErpCustomerDO::getName, reqVO.getCustomerName());
query.groupBy(ErpSaleReturnDO::getId);
}
if (CommonUtil.isNotEmpty(reqVO.getCustomerOrderCode())) {
query.leftJoin("order_customer_order oco on t.customer_order_id = oco.id");
query.eq("oco.code", reqVO.getCustomerOrderCode());
query.groupBy(ErpSaleReturnDO::getId);
}
return selectJoinPage(reqVO, ErpSaleReturnDO.class, query);
}
......
......@@ -195,7 +195,7 @@ public class ErpPurchaseReturnServiceImpl implements ErpPurchaseReturnService {
private List<ErpPurchaseReturnItemDO> validatePurchaseReturnItems(List<ErpPurchaseReturnSaveReqVO.Item> list) {
// 1. 校验产品存在
List<ProductSpuDO> productList = productService.validProductList(
List<ProductSpuDO> productList = productService.getSpuList(
convertSet(list, ErpPurchaseReturnSaveReqVO.Item::getProductId));
Map<Long, ProductSpuDO> productMap = convertMap(productList, ProductSpuDO::getId);
// 2. 转化为 ErpPurchaseReturnItemDO 列表
......
......@@ -10,6 +10,7 @@ import cn.iocoder.foodnexus.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.foodnexus.module.infra.controller.admin.file.vo.file.*;
import cn.iocoder.foodnexus.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.foodnexus.module.infra.service.file.FileService;
import cn.iocoder.foodnexus.module.infra.service.util.FileUploadValidator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
......@@ -20,6 +21,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
......@@ -45,9 +47,10 @@ public class FileController {
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
String fileName = FileUploadValidator.validateFileFormat(file);
byte[] content = IoUtil.readBytes(file.getInputStream());
return success(fileService.createFile(content, file.getOriginalFilename(),
uploadReqVO.getDirectory(), file.getContentType()));
return success(fileService.createFile(content, fileName,
null, file.getContentType()));
}
@GetMapping("/presigned-url")
......@@ -112,6 +115,26 @@ public class FileController {
writeAttachment(response, path, content);
}
@GetMapping("/downland/{id}")
@PermitAll
@TenantIgnore
@Operation(summary = "下载文件")
@Parameter(name = "configId", description = "配置编号", required = true)
public void downland(HttpServletRequest request,
HttpServletResponse response,
@PathVariable("id") Long id) throws Exception {
FileDO file = fileService.getFile(id);
// 读取内容
byte[] content = fileService.getFileContent(file.getConfigId(), file.getPath());
if (content == null) {
log.warn("[getFileContent][configId({}) path({}) 文件不存在]", file.getConfigId(), file.getPath());
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
writeAttachment(response, file.getPath(), content);
}
@GetMapping("/page")
@Operation(summary = "获得文件分页")
@PreAuthorize("@ss.hasPermission('infra:file:query')")
......
......@@ -16,13 +16,13 @@ public class FileUploadReqVO {
@NotNull(message = "文件附件不能为空")
private MultipartFile file;
@Schema(description = "文件目录", example = "XXX/YYY")
/*@Schema(description = "文件目录", example = "XXX/YYY")
private String directory;
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
}*/
}
......@@ -6,6 +6,7 @@ import cn.iocoder.foodnexus.module.infra.controller.admin.file.vo.file.FileCreat
import cn.iocoder.foodnexus.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import cn.iocoder.foodnexus.module.infra.controller.app.file.vo.AppFileUploadReqVO;
import cn.iocoder.foodnexus.module.infra.service.file.FileService;
import cn.iocoder.foodnexus.module.infra.service.util.FileUploadValidator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
......@@ -35,9 +36,10 @@ public class AppFileController {
@PermitAll
public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
String fileName = FileUploadValidator.validateFileFormat(file);
byte[] content = IoUtil.readBytes(file.getInputStream());
return success(fileService.createFile(content, file.getOriginalFilename(),
uploadReqVO.getDirectory(), file.getContentType()));
return success(fileService.createFile(content, fileName,
null, file.getContentType()));
}
@GetMapping("/presigned-url")
......
......@@ -16,13 +16,13 @@ public class AppFileUploadReqVO {
@NotNull(message = "文件附件不能为空")
private MultipartFile file;
@Schema(description = "文件目录", example = "XXX/YYY")
/*@Schema(description = "文件目录", example = "XXX/YYY")
private String directory;
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
}*/
}
......@@ -33,6 +33,7 @@ public interface ErrorCodeConstants {
ErrorCode FILE_PATH_EXISTS = new ErrorCode(1_001_003_000, "文件路径已存在");
ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在");
ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空");
ErrorCode FILE_NOT_ALLOWED = new ErrorCode(1_001_003_003, "文件格式不支持");
// ========== 代码生成器 1-001-004-000 ==========
ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在");
......
package cn.iocoder.foodnexus.module.infra.framework.file.core.client;
import cn.hutool.core.util.StrUtil;
/**
* 文件客户端
*
......
......@@ -53,4 +53,8 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
return config.getBasePath() + File.separator + path;
}
public String getDomain() {
return config.getDomain();
}
}
......@@ -83,8 +83,9 @@ public class FileTypeUtils {
String contentType = getMineType(content, filename);
response.setContentType(contentType);
// 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html
if (StrUtil.containsIgnoreCase(contentType, "image/")) {
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
if (filename.toLowerCase().endsWith(".svg")) {
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
} else if (StrUtil.containsIgnoreCase(contentType, "image/")) {
response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename));
}// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
else if (StrUtil.containsIgnoreCase(contentType, "video")) {
......@@ -94,6 +95,7 @@ public class FileTypeUtils {
response.setHeader("Accept-Ranges", "bytes");
} else {
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
response.setHeader("X-Content-Type-Options", "nosniff");
}
// 输出附件
......
......@@ -85,4 +85,5 @@ public interface FileService {
*/
byte[] getFileContent(Long configId, String path) throws Exception;
FileDO getFile(Long id);
}
......@@ -14,6 +14,7 @@ import cn.iocoder.foodnexus.module.infra.controller.admin.file.vo.file.FilePresi
import cn.iocoder.foodnexus.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.foodnexus.module.infra.dal.mysql.file.FileMapper;
import cn.iocoder.foodnexus.module.infra.framework.file.core.client.FileClient;
import cn.iocoder.foodnexus.module.infra.framework.file.core.client.local.LocalFileClient;
import cn.iocoder.foodnexus.module.infra.framework.file.core.utils.FileTypeUtils;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
......@@ -86,9 +87,15 @@ public class FileServiceImpl implements FileService {
String url = client.upload(content, path, type);
// 3. 保存到数据库
fileMapper.insert(new FileDO().setConfigId(client.getId())
FileDO fileDO = new FileDO().setConfigId(client.getId())
.setName(name).setPath(path).setUrl(url)
.setType(type).setSize(content.length));
.setType(type).setSize(content.length);
fileMapper.insert(fileDO);
Long id = fileDO.getId();
if (client instanceof LocalFileClient) {
return StrUtil.format("{}/admin-api/infra/file/downland/{}", ((LocalFileClient) client).getDomain(), id);
}
return url;
}
......@@ -198,4 +205,9 @@ public class FileServiceImpl implements FileService {
return client.getContent(path);
}
@Override
public FileDO getFile(Long id) {
return validateFileExists(id);
}
}
package cn.iocoder.foodnexus.module.infra.service.util;
import cn.iocoder.foodnexus.framework.common.exception.ServiceException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import static cn.iocoder.foodnexus.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.foodnexus.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY;
import static cn.iocoder.foodnexus.module.infra.enums.ErrorCodeConstants.FILE_NOT_ALLOWED;
/**
* @author : yanghao
* create at: 2025/11/17 17:22
* @description:
*/
public class FileUploadValidator {
// 允许的文件后缀(新增视频格式)
private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList(
// 图片
"jpg", "jpeg", "png", "gif", "bmp", "webp",
// 文档
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf", "txt",
// 压缩包
"zip", "rar",
// 新增:视频格式
"mp4", "avi", "mov", "wmv", "flv", "mkv", "mpeg", "mpg"
));
// 允许的MIME类型(新增视频MIME)
private static final Set<String> ALLOWED_MIME_TYPES = new HashSet<>(Arrays.asList(
// 图片MIME
"image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp",
// 文档MIME
"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/pdf", "text/plain",
// 压缩包MIME
"application/zip", "application/x-rar-compressed",
// 新增:视频MIME类型
"video/mp4", // mp4
"video/x-msvideo", // avi
"video/quicktime", // mov
"video/x-ms-wmv", // wmv
"video/x-flv", // flv
"video/x-matroska", // mkv
"video/mpeg" // mpeg/mpg
));
/**
* 校验文件内容(通过文件签名)
* 参考:https://en.wikipedia.org/wiki/List_of_file_signatures
*/
public static void validateFileContent(MultipartFile file, String extension) {
try (InputStream is = file.getInputStream()) {
byte[] header = new byte[8]; // 读取文件前8字节(足够识别多数类型)
int read = is.read(header);
if (read == -1) {
throw new IllegalArgumentException("文件内容为空");
}
// 根据扩展名校验对应签名(示例:校验jpg/png/pdf)
switch (extension.toLowerCase()) {
case "jpg", "jpeg":
// JPG签名:FF D8 FF
if (!(header[0] == (byte) 0xFF && header[1] == (byte) 0xD8 && header[2] == (byte) 0xFF)) {
throw new IllegalArgumentException("文件内容与jpg格式不匹配");
}
break;
case "png":
// PNG签名:89 50 4E 47 0D 0A 1A 0A
byte[] pngHeader = {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
if (!Arrays.equals(Arrays.copyOf(header, 8), pngHeader)) {
throw new IllegalArgumentException("文件内容与png格式不匹配");
}
break;
case "pdf":
// PDF签名:25 50 44 46
if (!(header[0] == 0x25 && header[1] == 0x50 && header[2] == 0x44 && header[3] == 0x46)) {
throw new IllegalArgumentException("文件内容与pdf格式不匹配");
}
break;
// 其他格式同理,添加对应签名校验
default:
// 对于未覆盖的格式,至少保证扩展名在白名单内
}
} catch (Exception e) {
throw exception(FILE_NOT_ALLOWED);
}
}
// 允许的字符:字母(a-zA-Z)、数字(0-9)、下划线(_)、短横线(-)、点(.)、空格( )
private static final Pattern SAFE_PATTERN =
Pattern.compile("^[\\p{L}0-9_.\\- ]+$");
/**
* 校验输入是否合法
* @param input 用户输入的字符串(如文件名、业务参数)
* @param fieldName 字段名(用于错误提示)
* @throws IllegalArgumentException 输入不合法时抛出
*/
public static void validate(String input, String fieldName) {
if (input == null || input.isEmpty()) {
throw new ServiceException(1_001_003_004, fieldName + "不能为空");
}
if (!SAFE_PATTERN.matcher(input).matches()) {
throw new ServiceException(1_001_003_004, fieldName + "包含非法字符,特殊符号仅允许 _ - . 空格");
}
}
// 校验逻辑不变(复用原有方法)
public static String validateFileFormat(MultipartFile file) {
// 1. 校验文件是否为空
if (file.isEmpty()) {
throw exception(FILE_IS_EMPTY);
}
// 2. 校验文件后缀
String originalFilename = file.getOriginalFilename();
if (!originalFilename.contains(".")) {
throw exception(FILE_NOT_ALLOWED);
}
// 获取文件后缀(小写)
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
if (!ALLOWED_EXTENSIONS.contains(fileExtension)) {
throw exception(FILE_NOT_ALLOWED);
}
// 3. 校验MIME类型
String contentType = file.getContentType();
if (!ALLOWED_MIME_TYPES.contains(contentType)) {
throw exception(FILE_NOT_ALLOWED);
}
validate(originalFilename, "文件名");
// 5. 文件内容校验(签名验证)
validateFileContent(file, fileExtension);
return UUID.randomUUID().toString().replace("-", "") + fileExtension;
}
}
......@@ -112,7 +112,7 @@ public class InquireCustomerPushServiceImpl implements InquireCustomerPushServic
* @param id
*/
@Override
@CacheEvict(cacheNames = RedisKeyConstants.CUSTOMER_VISIBLE_PRODUCT, key = "#id")
@CacheEvict(cacheNames = RedisKeyConstants.CUSTOMER_VISIBLE_PRODUCT, allEntries = true)
public void confirm(Long id) {
validateInquireCustomerPushExists(id);
inquireCustomerPushMapper.update(Wrappers.<InquireCustomerPushDO>lambdaUpdate()
......
......@@ -65,7 +65,7 @@ public class OperaSupplierPurchaseOrderController {
ErpPurchaseOrderPageReqVO orderPageReqVo = new ErpPurchaseOrderPageReqVO();
orderPageReqVo.setPageSize(pageReqVO.getPageSize());
orderPageReqVo.setPageNo(pageReqVO.getPageNo());
// orderPageReqVo.setSupplierId(supplierApi.querySupplierIdByUserId(getLoginUserId()));
orderPageReqVo.setSupplierId(pageReqVO.getSupplierId());
orderPageReqVo.setCreateMonth(pageReqVO.getMonth());
orderPageReqVo.setDeliveryStatusList(CommonUtil.asList(ErpDeliveryStatus.ARRIVAL.getStatus(), ErpDeliveryStatus.RECONCILIATION.getStatus()));
PageResult<ErpPurchaseOrderDO> pageResult = purchaseOrderService.getPurchaseOrderPage(orderPageReqVo);
......
......@@ -110,7 +110,7 @@ public interface CustomerOrderMapper extends BaseMapperX<CustomerOrderDO> {
String end = year + "-12-31 23:59:59";
MPJQueryWrapper<CustomerOrderDO> queryWrapperX = new MPJQueryWrapper<>();
queryWrapperX.select("DATE_FORMAT(create_time, '%Y年%m月') as 'yearMonth',count(*) as 'orderCount',SUM(order_amount) as 'orderAmount',SUM(actual_amount) as 'payableAmount'");
queryWrapperX.select("CASE WHEN SUM(CASE WHEN order_status='SIGN_RECEIPT' THEN 1 ELSE 0 END)> 0 THEN 0 ELSE 1 END AS 'status'");
queryWrapperX.select("CASE WHEN SUM(CASE WHEN has_finish = 0 THEN 1 ELSE 0 END)> 0 THEN 0 ELSE 1 END AS 'status'");
queryWrapperX.between("create_time", begin, end);
queryWrapperX.in("order_status", CommonUtil.asList(CustomerOrderStatus.SIGN_RECEIPT.getKey(),
CustomerOrderStatus.FINISH.getKey(),
......@@ -156,7 +156,8 @@ public interface CustomerOrderMapper extends BaseMapperX<CustomerOrderDO> {
wrapperX.select("IFNULL(sum(t.actual_amount),0) as 'actualAmount'");
wrapperX.select("CASE WHEN SUM(CASE WHEN t.order_status='SIGN_RECEIPT' THEN 1 ELSE 0 END)> 0 THEN 0 ELSE 1 END AS 'isFinish'");
wrapperX.in(CustomerOrderDO::getOrderStatus, CommonUtil.asList(CustomerOrderStatus.SIGN_RECEIPT.getKey(),
CustomerOrderStatus.FINISH.getKey()));
CustomerOrderStatus.FINISH.getKey(),
CustomerOrderStatus.RETURN.getKey()));
wrapperX.likeIfPresent(ErpCustomerDO::getName, pageReqVO.getCustomerName());
if (CommonUtil.isNotBlank(pageReqVO.getYearMonth())) {
String createMonth = pageReqVO.getYearMonth().trim();
......
package cn.iocoder.foodnexus.module.order.job;
import cn.iocoder.foodnexus.framework.common.enums.UserSystemEnum;
import cn.iocoder.foodnexus.framework.common.util.CommonUtil;
import cn.iocoder.foodnexus.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.foodnexus.module.operations.dal.dataobject.scoringweight.ScoringWeightDO;
import cn.iocoder.foodnexus.module.operations.service.scoringweight.ScoringWeightService;
import cn.iocoder.foodnexus.module.order.controller.app.customerOrder.vo.AppCustomerOrderScoreReqVO;
import cn.iocoder.foodnexus.module.order.dal.dataobject.customerorder.CustomerOrderDO;
import cn.iocoder.foodnexus.module.order.dal.dataobject.customerorderrecord.CustomerOrderRecordDO;
import cn.iocoder.foodnexus.module.order.dal.mysql.customerorder.CustomerOrderMapper;
import cn.iocoder.foodnexus.module.order.dto.CustomerOrderRemark;
import cn.iocoder.foodnexus.module.order.enums.CustomerOrderStatus;
import cn.iocoder.foodnexus.module.order.service.customerorder.CustomerOrderService;
import cn.iocoder.foodnexus.module.order.service.orderScore.OrderScoreService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.yulichang.query.MPJLambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
/**
* @author : yanghao
* create at: 2025/11/14 09:41
* @description: 客户订单定时任务
*/
@Component
@Slf4j
public class CustomerOrderScheduledTasks {
@Autowired
private CustomerOrderService customerOrderService;
@Autowired
private CustomerOrderMapper customerOrderMapper;
@Autowired
private ScoringWeightService scoringWeightService;
public static final int DAYS = 7;
public static final int SCORE = 4;
// 每天凌晨3点执行一次
// 查询签收、完成、退款订单中没有评价的订单,并给出默认订单评价
@Scheduled(cron = "0 0 3 * * ?")
public void defaultOrderScore() {
// 默认时间
LocalDate targetDate = LocalDate.now().minusDays(DAYS);
LocalDateTime startTime = targetDate.atStartOfDay(); // xxxx-xx-xx 00:00:00
LocalDateTime endTime = targetDate.atTime(LocalTime.MAX); // xxxx-xx-xx 23:59:59.999
MPJLambdaWrapperX<CustomerOrderDO> query = new MPJLambdaWrapperX<>();
query.eq(CustomerOrderDO::getHasScore, Boolean.FALSE)
.in(CustomerOrderDO::getOrderStatus, CommonUtil.asList(CustomerOrderStatus.SIGN_RECEIPT.getKey(), CustomerOrderStatus.FINISH.getKey(), CustomerOrderStatus.RETURN.getKey()));
query.leftJoin(CustomerOrderRecordDO.class, CustomerOrderRecordDO::getCustomerOrderId, CustomerOrderDO::getId);
query.ge(CustomerOrderRecordDO::getCreateTime, startTime) // 大于等于起始时间
.le(CustomerOrderRecordDO::getCreateTime, endTime);
query.eq(CustomerOrderRecordDO::getOrderStatus, CustomerOrderStatus.SIGN_RECEIPT.getKey());
List<CustomerOrderDO> customerOrderDOS = customerOrderMapper.selectList(query);
log.info("每日默认订单评价,查询到订单:{}条", customerOrderDOS.size());
if (CommonUtil.isEmpty(customerOrderDOS)) {
return ;
}
int success = 0;
for (CustomerOrderDO order : customerOrderDOS) {
AppCustomerOrderScoreReqVO scoreReqVO = new AppCustomerOrderScoreReqVO();
scoreReqVO.setOrderId(order.getId());
List<ScoringWeightDO> scoringWeightDOS = scoringWeightService.queryByUserSystem(UserSystemEnum.CUSTOMER);
scoreReqVO.setItems(CommonUtil.listConvert(scoringWeightDOS, item -> {
AppCustomerOrderScoreReqVO.Item scoreItem = new AppCustomerOrderScoreReqVO.Item();
scoreItem.setScoreId(item.getId());
scoreItem.setScore(SCORE);
return scoreItem;
}));
try {
customerOrderService.score(scoreReqVO);
success ++;
} catch (Exception e) {
log.error("每日默认订单评价,执行失败,客户订单编码【{}】", order.getCode(), e);
}
}
log.info("每日默认订单评价,查询到订单:{}条,执行成功:{}条", customerOrderDOS.size(), success);
}
}
......@@ -114,7 +114,7 @@ public interface CustomerOrderService {
* 评价订单
* @param reqVO
*/
void score(AppCustomerOrderScoreReqVO reqVO);
void score(@Valid AppCustomerOrderScoreReqVO reqVO);
Map<String, Long> queryStatusCount(Long loginUserId);
......
......@@ -51,6 +51,7 @@ import cn.iocoder.foodnexus.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.foodnexus.module.system.service.user.AdminUserService;
import cn.iocoder.foodnexus.module.system.util.GenCodeUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
......@@ -82,6 +83,7 @@ import static cn.iocoder.foodnexus.module.order.enums.ErrorCodeConstants.*;
*/
@Service
@Validated
@Slf4j
public class CustomerOrderServiceImpl implements CustomerOrderService, CustomerOrderApi {
@Resource
......@@ -603,6 +605,7 @@ public class CustomerOrderServiceImpl implements CustomerOrderService, CustomerO
@Override
@Transactional(rollbackFor = Exception.class)
public void score(AppCustomerOrderScoreReqVO reqVO) {
log.info("评价订单:{}", reqVO);
Long id = reqVO.getOrderId();
CustomerOrderDO customerOrder = getCustomerOrder(id);
if (CommonUtil.isEmpty(customerOrder)) {
......
package cn.iocoder.foodnexus.module.system.aspect;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author : yanghao
* create at: 2025/11/18 13:41
* @description:
*/
@Configuration
public class CookieConfig {
@Bean
public CookieSameSiteSupplier cookieSameSiteSupplier() {
// 全局设置 SameSite=Strict(或 Lax,根据业务调整)
return CookieSameSiteSupplier.ofStrict();
}
// 自定义 Cookie 处理器,添加 HttpOnly 和 Secure
@Bean
public ServletContextInitializer servletContextInitializer() {
return servletContext -> {
// 对所有 Cookie 生效
servletContext.getSessionCookieConfig().setHttpOnly(true); // 禁止 JS 访问
// servletContext.getSessionCookieConfig().setSecure(true); // 仅 HTTPS 传输(生产环境启用)
// 可选:设置 Cookie 有效期(如 30 分钟)
// servletContext.getSessionCookieConfig().setMaxAge(1800);
};
}
}
\ No newline at end of file
package cn.iocoder.foodnexus.module.system.aspect;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author : yanghao
* create at: 2025/11/18 13:57
* @description:
*/
@Component
public class GlobalCookieSecurityFilter implements Filter {
private static final String HTTP_ONLY = "HttpOnly";
private static final String SECURE = "Secure";
private static final String SAME_SITE = "SameSite=Strict"; // 可按需改为 Lax
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResp = (HttpServletResponse) response;
HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(httpResp) {
@Override
public void addHeader(String name, String value) {
if ("Set-Cookie".equalsIgnoreCase(name)) {
String lower = value.toLowerCase();
// HttpOnly
if (!lower.contains("httponly")) {
value += "; " + HTTP_ONLY;
}
// Secure(仅在 HTTPS 生效)
if (!lower.contains("secure")) {
value += "; " + SECURE;
}
// SameSite
if (!lower.contains("samesite")) {
value += "; " + SAME_SITE;
}
}
super.addHeader(name, value);
}
};
chain.doFilter(request, wrapper);
}
}
......@@ -13,9 +13,9 @@ spring:
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
enabled: false
stat-view-servlet:
enabled: true
enabled: false
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
......@@ -227,4 +227,10 @@ iot:
# 插件配置
pf4j:
pluginsDir: ${user.home}/plugins # 插件目录
\ No newline at end of file
pluginsDir: ${user.home}/plugins # 插件目录
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment