🐛 修复 MinIO 预签名走外网 HTTPS,并过滤无效应收应付变更记录

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 12:44:39 +08:00
parent ea1829cd75
commit e0da79d745
5 changed files with 166 additions and 97 deletions
@@ -95,7 +95,13 @@ public class FileConfig {
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
String fileStoragePrefix = "file.storage." + prefix;
mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint")));
mergedProperties.setTransformEndpoint(firstNotBlank(properties.getTransformEndpoint(), environment.getProperty(fileStoragePrefix + ".transform-endpoint"), environment.getProperty(prefix + ".transform-endpoint")));
mergedProperties.setTransformEndpoint(firstNotBlank(
properties.getTransformEndpoint(),
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
environment.getProperty(prefix + ".transform-endpoint"),
// 兼容 blade-resource / oss 配置,避免仅改对象存储外网地址时分片上传仍走内网 HTTP
environment.getProperty("oss.transform-endpoint")
));
mergedProperties.setAccessKeyId(firstNotBlank(properties.getAccessKeyId(), environment.getProperty(fileStoragePrefix + ".access-key-id"), environment.getProperty(fileStoragePrefix + ".access-key"), environment.getProperty(prefix + ".access-key-id"), environment.getProperty(prefix + ".access-key")));
mergedProperties.setAccessKeySecret(firstNotBlank(properties.getAccessKeySecret(), environment.getProperty(fileStoragePrefix + ".access-key-secret"), environment.getProperty(fileStoragePrefix + ".secret-key"), environment.getProperty(prefix + ".access-key-secret"), environment.getProperty(prefix + ".secret-key")));
mergedProperties.setBucketName(firstNotBlank(properties.getBucketName(), environment.getProperty(fileStoragePrefix + ".bucket-name"), environment.getProperty(prefix + ".bucket-name")));
@@ -25,11 +25,11 @@
*/
package org.springblade.file.service.impl;
import io.minio.ComposeObjectArgs;
import io.minio.ComposeSource;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.ComposeObjectArgs;
import io.minio.ComposeSource;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
@@ -42,6 +42,7 @@ import org.springblade.file.config.FileStorageProperties;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -64,6 +65,7 @@ public class MinioFileServiceImpl extends AbstractFileService {
private final FileStorageProperties.StoreProperties properties;
private final MinioClient minioClient;
private volatile MinioClient transformPresignClient;
@Override
public String initiateMultipartUpload(String objectKey) {
@@ -73,9 +75,13 @@ public class MinioFileServiceImpl extends AbstractFileService {
@Override
public String getUploadPartUrl(String objectKey, String uploadId, Integer partNumber, Long expires) {
try {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.PUT).bucket(properties.getBucketName()).object(partObjectKey(objectKey, uploadId, partNumber))
.expiry(Math.toIntExact(resolveExpires(expires))).build());
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(properties.getBucketName())
.object(partObjectKey(objectKey, uploadId, partNumber))
.expiry(Math.toIntExact(resolveExpires(expires)))
.build());
return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.error("MinIO获取分片上传链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("获取分片上传链接异常");
@@ -143,12 +149,13 @@ public class MinioFileServiceImpl extends AbstractFileService {
return null;
}
try {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
String presignedUrl = resolvePresignClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(properties.getBucketName())
.object(resolveObjectKey(objectKey))
.expiry(Math.toIntExact(resolveExpires(expires)))
.build());
return applyTransformPathPrefix(presignedUrl);
} catch (Exception exception) {
log.error("MinIO获取文件链接异常 objectKey:{}", objectKey, exception);
throw new ServiceException("MinIO获取文件链接异常");
@@ -170,6 +177,77 @@ public class MinioFileServiceImpl extends AbstractFileService {
}
}
/**
* 浏览器侧预签名优先使用外网地址,避免 HTTPS 页面访问 HTTP MinIO 被 Mixed Content 拦截。
* MinIO SDK 不允许 endpoint 带 path,因此外网地址中的 /oss 等前缀在签名后单独拼回。
*/
private MinioClient resolvePresignClient() {
String transformEndpoint = properties.getTransformEndpoint();
if (StringUtil.isBlank(transformEndpoint)) {
return minioClient;
}
MinioClient cachedClient = transformPresignClient;
if (cachedClient != null) {
return cachedClient;
}
synchronized (this) {
if (transformPresignClient == null) {
transformPresignClient = MinioClient.builder()
.endpoint(resolveTransformOrigin(transformEndpoint))
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
.build();
}
return transformPresignClient;
}
}
private String resolveTransformOrigin(String transformEndpoint) {
URI transformUri = URI.create(transformEndpoint.trim());
if (StringUtil.isBlank(transformUri.getScheme()) || StringUtil.isBlank(transformUri.getHost())) {
throw new ServiceException("MinIO外网地址配置不正确");
}
StringBuilder origin = new StringBuilder();
origin.append(transformUri.getScheme()).append("://").append(transformUri.getHost());
if (transformUri.getPort() > 0) {
origin.append(':').append(transformUri.getPort());
}
return origin.toString();
}
private String applyTransformPathPrefix(String presignedUrl) {
String transformEndpoint = properties.getTransformEndpoint();
if (StringUtil.isBlank(transformEndpoint) || StringUtil.isBlank(presignedUrl)) {
return presignedUrl;
}
String pathPrefix = normalizePathPrefix(URI.create(transformEndpoint.trim()).getPath());
if (StringUtil.isBlank(pathPrefix)) {
return presignedUrl;
}
URI url = URI.create(presignedUrl);
String path = url.getRawPath() == null ? "" : url.getRawPath();
if (path.equals(pathPrefix) || path.startsWith(pathPrefix + "/")) {
return presignedUrl;
}
StringBuilder result = new StringBuilder();
result.append(url.getScheme()).append("://").append(url.getHost());
if (url.getPort() > 0) {
result.append(':').append(url.getPort());
}
result.append(pathPrefix).append(path);
if (StringUtil.isNotBlank(url.getRawQuery())) {
result.append('?').append(url.getRawQuery());
}
return result.toString();
}
private String normalizePathPrefix(String path) {
if (StringUtil.isBlank(path) || "/".equals(path)) {
return "";
}
String normalized = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
return normalized.startsWith("/") ? normalized : "/" + normalized;
}
private String resolveObjectKey(String objectKey) {
if (StringUtil.isBlank(properties.getRootDirectory())) {
return objectKey;
@@ -432,13 +432,23 @@ public class ReceivablePayableDetailServiceImpl
@Override
public IPage<ReceivablePayableChangeRecordVO> changeRecords(IPage<?> page, Long detailId) {
LambdaQueryWrapper<ReceivablePayableChangeRecord> wrapper = Wrappers.<ReceivablePayableChangeRecord>lambdaQuery()
.eq(ReceivablePayableChangeRecord::getDetailId, detailId)
.eq(ReceivablePayableChangeRecord::getIsDeleted, 0)
.orderByDesc(ReceivablePayableChangeRecord::getAdjustTime);
IPage<ReceivablePayableChangeRecord> entityPage = changeRecordMapper.selectPage(new Page<>(page.getCurrent(), page.getSize()), wrapper);
Page<ReceivablePayableChangeRecordVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
voPage.setRecords(entityPage.getRecords().stream().map(record -> Objects.requireNonNull(
List<ReceivablePayableChangeRecord> meaningfulRecords = changeRecordMapper.selectList(
Wrappers.<ReceivablePayableChangeRecord>lambdaQuery()
.eq(ReceivablePayableChangeRecord::getDetailId, detailId)
.eq(ReceivablePayableChangeRecord::getIsDeleted, 0)
.orderByDesc(ReceivablePayableChangeRecord::getAdjustTime))
.stream()
.filter(record -> hasMeaningfulChange(record.getChangeContent()))
.toList();
long current = page.getCurrent();
long size = page.getSize();
int fromIndex = (int) Math.max(0L, (current - 1L) * size);
int toIndex = (int) Math.min(meaningfulRecords.size(), fromIndex + size);
List<ReceivablePayableChangeRecord> pageRecords = fromIndex >= meaningfulRecords.size()
? List.of()
: meaningfulRecords.subList(fromIndex, toIndex);
Page<ReceivablePayableChangeRecordVO> voPage = new Page<>(current, size, meaningfulRecords.size());
voPage.setRecords(pageRecords.stream().map(record -> Objects.requireNonNull(
BeanUtil.copyProperties(record, ReceivablePayableChangeRecordVO.class))).toList());
return voPage;
}
@@ -613,14 +623,8 @@ public class ReceivablePayableDetailServiceImpl
appendChange(changes, "运输单价", existing.getUnitPrice(), adjusted.getUnitPrice());
appendChange(changes, "里程", existing.getMileage(), mileage);
appendChange(changes, "运输费", existing.getFreightAmount(), freightAmount);
if (!Objects.equals(existing.getRemark(), adjusted.getRemark())) {
changes.add("【备注】从[" + Objects.toString(existing.getRemark(), "") + "]调整为["
+ Objects.toString(adjusted.getRemark(), "") + "]");
}
if (!Objects.equals(existing.getChangeReason(), adjusted.getChangeReason())) {
changes.add("【变更原因】从[" + Objects.toString(existing.getChangeReason(), "") + "]调整为["
+ Objects.toString(adjusted.getChangeReason(), "") + "]");
}
appendChange(changes, "备注", existing.getRemark(), adjusted.getRemark());
appendChange(changes, "变更原因", existing.getChangeReason(), adjusted.getChangeReason());
Map<String, Object> oldFeeItems = parseMap(existing.getFeeItemsJson());
for (String name : allowedFeeItems) {
appendChange(changes, name, decimal(oldFeeItems.get(name)), money(effectiveFeeItems.get(name)));
@@ -2367,12 +2371,56 @@ public class ReceivablePayableDetailServiceImpl
}
private void appendChange(List<String> changes, String field, String before, String after) {
if (!Objects.equals(before, after)) {
changes.add("【" + field + "】从[" + Objects.toString(before, "") + "]调整为["
+ Objects.toString(after, "") + "]");
String oldValue = normalizeChangeText(before);
String newValue = normalizeChangeText(after);
if (!Objects.equals(oldValue, newValue)) {
changes.add("【" + field + "】从[" + oldValue + "]调整为[" + newValue + "]");
}
}
/**
* 变更文本归一化:null / 空白视为相同,避免出现「从[]调整为[]」无效记录
*/
private String normalizeChangeText(String value) {
return value == null ? "" : value.trim();
}
/**
* 是否存在实际前后差异;整段内容均无变化时不展示
*/
private boolean hasMeaningfulChange(String content) {
if (Func.isBlank(content)) {
return false;
}
String text = content.trim();
int suffixStart = text.lastIndexOf('(');
if (suffixStart > 0 && text.endsWith(")")) {
text = text.substring(0, suffixStart);
}
boolean hasPart = false;
for (String part : text.split(";")) {
if (Func.isBlank(part)) {
continue;
}
hasPart = true;
if (isMeaningfulChangePart(part.trim())) {
return true;
}
}
return !hasPart;
}
private boolean isMeaningfulChangePart(String part) {
int fromIdx = part.indexOf("从[");
int toIdx = part.indexOf("]调整为[");
if (fromIdx < 0 || toIdx < fromIdx || !part.endsWith("]")) {
return true;
}
String before = part.substring(fromIdx + 2, toIdx);
String after = part.substring(toIdx + 5, part.length() - 1);
return !Objects.equals(before, after);
}
private String formatValue(BigDecimal value) {
return money(value).stripTrailingZeros().toPlainString();
}
@@ -2617,6 +2665,9 @@ public class ReceivablePayableDetailServiceImpl
}
private void saveChangeRecord(ReceivablePayableDetail detail, String content, String reason, String lineNo) {
if (!hasMeaningfulChange(content)) {
return;
}
ReceivablePayableChangeRecord record = new ReceivablePayableChangeRecord();
record.setDetailId(detail.getId());
record.setLineNo(lineNo);
+6
View File
@@ -120,6 +120,12 @@ baidu:
request-timeout: 30s
token-refresh-advance: 1m
# 凭证分片上传等走 blade-file,外网地址需单独配置(与 /resource/oss 的 transformEndpoint 保持一致)
file:
storage:
minio:
transform-endpoint: ${MINIO_TRANSFORM_ENDPOINT:https://tms-prod.xinghy56.com/oss}
powerjob:
worker:
server-address: 172.16.203.228:7700
@@ -1,72 +0,0 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tool.node;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import org.springblade.core.tool.utils.Func;
import java.io.Serial;
import java.util.Objects;
/**
* 树型节点类
*
* @author smallchill
*/
@Data
public class TreeNode extends BaseNode<TreeNode> {
@Serial
private static final long serialVersionUID = 1L;
private String title;
@JsonSerialize(using = ToStringSerializer.class)
private Long key;
@JsonSerialize(using = ToStringSerializer.class)
private Long value;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
TreeNode other = (TreeNode) obj;
return Func.equals(this.getId(), other.getId());
}
@Override
public int hashCode() {
return Objects.hash(id, parentId);
}
}