fix:发票盖章用到底图被图片加密拦截的问题

This commit is contained in:
dzc committed 2026-10-30 16:28:49 +08:00
1 parent 9347410909
commit be93d4ae50
6 files changed
+368 -25

No files matched your search

+24 -2
View File
@@ -144,7 +144,7 @@ JPEG、PNG、GIF、BMP、WebP 位图按实际文件头识别和校验:声明
| 部分 | 接入位置与行为 |
| --- | --- |
| Java 文件入口 | `FileAccessGuard`,`SysFileController`,`SysFileServiceImpl`;租户跳过只作用于文件查询并恢复原状态 |
| Java 内部读取 | `RemoteFileService` 保留 `getFile` 调用,通过编码桥接和专属 Feign 拦截器签名,保留文件名中的字面 `%xx`;钱包、硬件导出/日志/取证通过相同签名读取;票据 URLConnection 单独适配 |
| Java 内部读取 | `RemoteFileService.getFile` 通过编码桥接和专属 Feign 拦截器签名,保留文件名中的字面 `%xx`;钱包、硬件导出/日志/取证及发票盖章通过相同签名读取 |
| APK | 终端 `/app-version/ad/page` 和 `/app/last-version` 返回对象使用 `package` 签名,后台列表/编辑和数据库仍为原始值;独立静态 APK 地址保持不变 |
| 后台 | `src/utils/file-access*.ts`;`FileImage` 保留已加载画面,未加载的懒图片继续续期,打开/切换预览时取有效签名;失败重试仅刷新当前文件,不清空全局签名缓存;上传回显、素材库、头像、视频、版本复制和下载均已接入;KKFileView 在编码之前签名源地址 |
| 后台编辑器 | 只更新编辑器 DOM 的资源属性,不替换整篇文档;输出 HTML 剥离临时签名,续期不进入编辑历史 |
@@ -156,6 +156,28 @@ JPEG、PNG、GIF、BMP、WebP 位图按实际文件头识别和校验:声明
第三方 URL、静态资源、`data:`、`blob:`、本地临时文件不加本协议签名。复制访问链接是临时访问地址;编辑、存储文件引用使用原始值。
### Java 图片合成等内部读取
文件中心图片使用 `RemoteFileService.getFile(fileName)`,每次调用由专属 Feign 拦截器生成 `service` 签名,并通过服务发现访问 UPMS。业务字段如果保存的是完整或相对文件链接,先用 `FileAccessUrl.fileName(source)` 提取原始文件名:
```java
String fileName = FileAccessUrl.fileName(source);
if (fileName == null) throw new IllegalArgumentException("不是文件中心引用");
try (Response response = remoteFileService.getFile(fileName)) {
if (response.status() < 200 || response.status() >= 300 || response.body() == null) {
throw new IOException("文件读取失败");
}
try (InputStream input = response.body().asInputStream()) {
BufferedImage image = ImageIO.read(input);
// 校验 image 非空后合成。
}
}
```
提取工具支持原始文件名、`/sys-file/oss/file`、`/admin/sys-file/oss/file` 及带网关前缀的 `/admin/sys-file/oss/file` 链接;查询中的文件名只解码一次,重复、缺失或非法文件名拒绝处理。链接中的主机和旧签名不参与内部调用,始终读取当前环境的 UPMS,不会向原链接主机发送 `service` 凭据。
`WalletRechargeInvoiceServiceImpl.stamp` 已按此方式读取底图。因此 `http://127.0.0.1:9999/admin/sys-file/oss/file?...`、历史域名链接和相对链接都不再依赖 `IMAGE_BASE_URL` 或公网来源列表。非文件中心图片保留原有 HTTP 读取方式,相对静态资源仍使用 `IMAGE_BASE_URL`。调用方和 UPMS 必须加载相同的 `clients.service` 活动密钥;本次修复需要重新构建并发布 Wallet 及更新后的 common-core,无需为本机地址扩大 `allowed-origins`。
旧终端兼容只接受完整 `Dart/<版本> (dart:io)` UA,且扩展名在 jpg/jpeg/png/gif/webp/bmp/svg/ico 内。完全没有签名参数才可走兼容分支;错误或过期签名不会回退。APK、日志、文档不凭 Dart UA 放行。
## 发布和回滚
@@ -188,7 +210,7 @@ Java 定向回归:
```bash
mvn -pl pjcloud-upms/pjcloud-upms-biz,pjcloud-integrated/pjcloud-integrated-biz,pjcloud-wallet/pjcloud-wallet-biz,pjcloud-hardware/pjcloud-hardware-biz,pjcloud-canteen/pjcloud-canteen-biz \
-am test \
-Dtest=FileAccessTest,FileReadTest,FileUploadGuardTest,FileUploadPolicyTest,FileUploadControllerTest,AppVersionFileAccessTest,WalletManageBatchExportTest,WalletLedgerBatchExportTest,HardBatchExportFileTest,HardBatchExportQueryTest \
-Dtest=FileAccessTest,FileReadTest,FileUploadGuardTest,FileUploadPolicyTest,FileUploadControllerTest,AppVersionFileAccessTest,FileAccessUrlTest,WalletRechargeInvoiceFileAccessTest,WalletManageBatchExportTest,WalletLedgerBatchExportTest,HardBatchExportFileTest,HardBatchExportQueryTest \
-Dsurefire.failIfNoSpecifiedTests=false
```
+12
View File
@@ -150,3 +150,15 @@ Java 定向测试及各端构建命令见 [接入与发布说明](README.md)。
- 执行 `mvn -pl pjcloud-upms/pjcloud-upms-biz -am test -Dtest=FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:23 项通过,0 失败、错误或跳过,Maven 构建成功。
- 新增测试覆盖实际根 Referer、完整页面 Referer、无末尾斜杠的配置、默认/自定义端口、域名伪造、错误协议、重复/缺失 Referer、目录范围不扩大、关闭白名单、导航及非图片拒绝、上传仍需鉴权、GET/HEAD 请求到存储的边界。
- 本机验证日志为 `/tmp/pj-file-access-origin-tests.log`。未修改线上配置或发布服务,线上恢复情况需在新版 UPMS 和对应公共依赖发布后确认。若根来源配置共享给其他启用文件配置的服务,也需先更新这些服务的公共依赖。
## 发票盖章内部读取修复
2026-10-30 反馈 `WalletRechargeInvoiceServiceImpl.stamp` 访问 `http://127.0.0.1:9999/admin/sys-file/oss/file?fileName=...` 返回 403。原实现使用 `FileAccessSigner.signUrl`,不在公网来源配置中的主机不会被签名,随后 URLConnection 发出的裸请求在严格模式被拒绝。
盖章现在使用 `FileAccessUrl.fileName` 解析文件引用,再由 `RemoteFileService.getFile` 通过服务发现读取 UPMS,并在请求时生成 `service` 签名。文件中心读取不再依赖 `IMAGE_BASE_URL` 或原链接的主机;文件访问入口及其鉴权规则未修改。
- 执行 `mvn -pl pjcloud-wallet/pjcloud-wallet-biz,pjcloud-upms/pjcloud-upms-biz -am test -Dtest=WalletRechargeInvoiceFileAccessTest,FileAccessUrlTest,FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:31 项通过,0 失败、错误或跳过,Maven 构建成功。
- 新回归覆盖本机网关、历史域名及网关前缀、相对路径、原始文件名、旧签名替换、中文/加号/百分号编码、重复与非法文件名、读取失败不上传、源流关闭、PNG 盖章合成,以及第三方图片请求保持原样。
- 本机验证日志为 `/tmp/pj-internal-image-access-tests.log`。测试中的 Feign 文件服务为替身,未访问真实注册中心或存储;未修改 Nacos、密钥或运行中的服务。
需重新构建并发布 Wallet,包含更新后的 common-core;确认 Wallet 与 UPMS 的 `clients.service` 密钥一致,再在实际环境重新生成发票验收。
@@ -0,0 +1,56 @@
package com.bsd.pjcloud.common.core.file;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/** Resolves stored file references for internal reads through RemoteFileService. */
public final class FileAccessUrl {
private FileAccessUrl() { }
/**
* Returns the original filename, or null for a URL outside the file-center routes.
* The source host is deliberately not used: callers must read the result from
* their own UPMS, never sign or fetch the supplied host with service credentials.
*/
public static String fileName(String source) {
if (source == null || source.isBlank()) throw new IllegalArgumentException("Empty file reference");
String value = source.trim();
if (!value.contains("/") && !value.contains("?") && !value.contains(":")) {
return validateFileName(value);
}
URI uri = URI.create(value);
String path = uri.getRawPath();
if (path == null) return null;
if (!path.startsWith("/")) path = "/" + path;
if (!(path.equals("/sys-file/oss/file") || path.endsWith("/admin/sys-file/oss/file"))) return null;
if ((uri.isAbsolute() && !("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme())))
|| (!uri.isAbsolute() && uri.getRawAuthority() != null) || uri.getUserInfo() != null
|| uri.getRawFragment() != null) {
throw new IllegalArgumentException("Invalid file-center URL");
}
String name = null;
if (uri.getRawQuery() != null) {
for (String pair : uri.getRawQuery().split("&")) {
String[] parts = pair.split("=", 2);
if (decode(parts[0]).equals("fileName")) {
if (name != null) throw new IllegalArgumentException("Duplicate fileName");
name = decode(parts.length == 2 ? parts[1] : "");
}
}
}
return validateFileName(name);
}
private static String validateFileName(String name) {
if (name == null || name.isBlank() || name.length() > 1024 || name.contains("/") || name.contains("\\")
|| name.equals(".") || name.equals("..") || name.chars().anyMatch(Character::isISOControl)) {
throw new IllegalArgumentException("Invalid file-center filename");
}
return name;
}
private static String decode(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8);
}
}
@@ -21,6 +21,7 @@ import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
import com.bsd.pjcloud.admin.api.feign.RemoteFileService;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.common.core.file.FileAccessUrl;
import com.bsd.pjcloud.common.core.util.CollectionUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
@@ -54,6 +55,7 @@ import com.bsd.pjcloud.wallet.utils.ConvertCnPriceUtil;
import com.bsd.pjcloud.wallet.utils.flow.FlowUtil;
import com.bsd.pjcloud.wallet.utils.flow.dto.WalletRechargeInvoiceFlowDto;
import com.bsd.pjcloud.wallet.utils.page.PaginationOrderSqlExtractor;
import feign.Response;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.transaction.annotation.Transactional;
@@ -66,6 +68,7 @@ import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -406,9 +409,6 @@ public class WalletRechargeInvoiceServiceImpl extends ServiceImpl<WalletRecharge
/**
* 生成盖章图片
*/
@org.springframework.beans.factory.annotation.Autowired
private com.bsd.pjcloud.common.core.file.FileAccessSigner fileAccessSigner;
@Override
public String stamp(WalletRechargeInvoiceEntity invoice) {
// 查看有没有盖章
@@ -421,34 +421,15 @@ public class WalletRechargeInvoiceServiceImpl extends ServiceImpl<WalletRecharge
log.error("|{}|盖章失败,审核成功才能开发票,invoice:|{}|", TAG, JSONUtil.toJsonStr(invoice));
return null;
}
String imageBaseUrl = ParamResolver.getStr("IMAGE_BASE_URL");
if (ObjectUtil.isEmpty(imageBaseUrl)) {
throw new RuntimeException("请配置图片基础路径");
}
if (ObjectUtil.isEmpty(invoice.getPicUrl())) {
log.error("|{}|盖章失败,发票图片为空,invoice:|{}|", TAG, JSONUtil.toJsonStr(invoice));
return null;
}
// 开始盖章
String picUrl = invoice.getPicUrl();
if (!picUrl.startsWith("http://") && !picUrl.startsWith("https://")) {
if (imageBaseUrl.endsWith("/") && picUrl.startsWith("/")) {
picUrl = imageBaseUrl + picUrl.substring(1);
} else if (!imageBaseUrl.endsWith("/") && !picUrl.startsWith("/")) {
picUrl = imageBaseUrl + "/" + picUrl;
} else {
picUrl = imageBaseUrl + picUrl;
}
}
try {
// 读取底图
URLConnection connection = new URL(fileAccessSigner.signUrl(picUrl, "service")).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
BufferedImage baseImage;
try (InputStream inputStream = connection.getInputStream()) {
baseImage = ImageIO.read(inputStream);
}
BufferedImage baseImage = readInvoiceImage(picUrl);
if (ObjectUtil.isEmpty(baseImage)) {
log.error("|{}|盖章失败,底图解析失败,picUrl:|{}|invoice:|{}|", TAG, picUrl, JSONUtil.toJsonStr(invoice));
return null;
@@ -515,6 +496,34 @@ public class WalletRechargeInvoiceServiceImpl extends ServiceImpl<WalletRecharge
}
}
private BufferedImage readInvoiceImage(String source) throws IOException {
String fileName = FileAccessUrl.fileName(source);
if (fileName != null) {
// Feign 自动附加 service 签名;不通过公网域名或本机网关读取文件中心图片。
try (Response response = remoteFileService.getFile(fileName)) {
if (response == null || response.status() < 200 || response.status() >= 300 || response.body() == null) {
throw new IOException("读取发票底图失败,文件服务状态:" + (response == null ? "无响应" : response.status()));
}
try (InputStream inputStream = response.body().asInputStream()) {
return ImageIO.read(inputStream);
}
}
}
// 保留非文件中心图片原有的读取方式,不向第三方地址附加服务签名。
String url = source;
if (!url.startsWith("http://") && !url.startsWith("https://")) {
String imageBaseUrl = ParamResolver.getStr("IMAGE_BASE_URL");
if (ObjectUtil.isEmpty(imageBaseUrl)) throw new IOException("请配置图片基础路径");
url = imageBaseUrl.replaceAll("/+$", "") + "/" + url.replaceAll("^/+", "");
}
URLConnection connection = new URL(url).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
try (InputStream inputStream = connection.getInputStream()) {
return ImageIO.read(inputStream);
}
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(value, max));
}
@@ -0,0 +1,43 @@
package com.bsd.pjcloud.wallet.service.impl.recharge;
import com.bsd.pjcloud.common.core.file.FileAccessUrl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class FileAccessUrlTest {
@Test
void queryIsDecodedOnceWhileBareFileNamesKeepLiteralEscapes() {
assertEquals("照片 +&%2F.png", FileAccessUrl.fileName(
"/admin/sys-file/oss/file?fileName=%E7%85%A7%E7%89%87%20%2B%26%252F.png"));
assertEquals("literal%2F.png", FileAccessUrl.fileName("literal%2F.png"));
assertEquals("base.png", FileAccessUrl.fileName(
"http://127.0.0.1:9999/api/admin/sys-file/oss/file?%66ileName=base.png"));
}
@Test
void otherImageUrlsAreNotTreatedAsFileCenterObjects() {
for (String source : List.of("https://other.example/static/a.png", "/static/a.png",
"https://other.example/not-admin/sys-file/oss/file?fileName=a.png",
"https://other.example/admin/sys-file/oss/file-extra?fileName=a.png")) {
assertNull(FileAccessUrl.fileName(source), source);
}
}
@Test
void ambiguousMalformedOrUnsafeFileNamesAreRejected() {
for (String source : List.of("", "..", "bad\\name.png", "/admin/sys-file/oss/file",
"/admin/sys-file/oss/file?fileName=a.png&%66ileName=b.png",
"/admin/sys-file/oss/file?fileName=%ZZ",
"/admin/sys-file/oss/file?fileName=a%0Ab.png",
"/admin/sys-file/oss/file?fileName=dir%2Fa.png",
"/admin/sys-file/oss/file?fileName=dir%5Ca.png",
"https://user@other.example/admin/sys-file/oss/file?fileName=a.png",
"//other.example/admin/sys-file/oss/file?fileName=a.png",
"/admin/sys-file/oss/file?fileName=a.png#fragment")) {
assertThrows(IllegalArgumentException.class, () -> FileAccessUrl.fileName(source), source);
}
}
}
@@ -0,0 +1,201 @@
package com.bsd.pjcloud.wallet.service.impl.recharge;
import com.bsd.pjcloud.admin.api.config.FileAccessFeignConfiguration;
import com.bsd.pjcloud.admin.api.feign.RemoteFileService;
import com.bsd.pjcloud.common.core.file.FileAccessProperties;
import com.bsd.pjcloud.common.core.file.FileAccessSignature;
import com.bsd.pjcloud.common.core.file.FileAccessSigner;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.flow.task.constant.TaskCheckStatusEnum;
import com.bsd.pjcloud.wallet.api.entity.recharge.WalletRechargeInvoiceEntity;
import com.sun.net.httpserver.HttpServer;
import feign.Feign;
import feign.Request;
import feign.Response;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class WalletRechargeInvoiceFileAccessTest {
private static final String STAMPED_URL = "/admin/sys-file/oss/file?fileName=stamped.png";
private final RemoteFileService files = mock(RemoteFileService.class);
private final WalletRechargeInvoiceServiceImpl service = new WalletRechargeInvoiceServiceImpl(
null, null, null, null, null, files, null, null);
@Test
void localGatewayAndStoredReferencesUseFreshServiceSignaturesWithoutPublicOriginConfiguration() throws Exception {
long now = 1_800_000_000L;
String key = Base64.getEncoder().encodeToString("0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8));
var properties = new FileAccessProperties();
properties.setPublicBaseUrl("https://public.example/api");
var client = new FileAccessProperties.Client();
client.setKeys(Map.of("v1", key));
properties.setClients(Map.of("service", client));
var signer = new FileAccessSigner(properties, Clock.fixed(Instant.ofEpochSecond(now), ZoneOffset.UTC));
byte[] image = image();
var captured = new AtomicReference<Request>();
RemoteFileService signedClient = Feign.builder().contract(new SpringMvcContract())
.requestInterceptor(new FileAccessFeignConfiguration().fileAccessRequestInterceptor(signer))
.client((request, options) -> {
captured.set(request);
return Response.builder().request(request).status(200).reason("OK").headers(Map.of()).body(image).build();
}).target(RemoteFileService.class, "http://upms-internal");
when(files.getFile(anyString())).thenAnswer(call -> signedClient.getFile(call.getArgument(0)));
when(files.innerUpload(any(), isNull(), isNull(), isNull())).thenReturn(R.ok(Map.of("url", STAMPED_URL)));
String name = "发票 +&%2F.png";
String encoded = URLEncoder.encode(name, StandardCharsets.UTF_8).replace("+", "%20");
for (String source : List.of(
"http://127.0.0.1:9999/admin/sys-file/oss/file?fileName=" + encoded,
"/admin/sys-file/oss/file?fileName=" + encoded,
"admin/sys-file/oss/file?fileName=" + encoded,
"/api/admin/sys-file/oss/file?fileName=" + encoded,
"/sys-file/oss/file?fileName=" + encoded,
"https://old.example/gateway/api/admin/sys-file/oss/file?appId=admin&kid=old&ts=1&sign=expired&fileName=" + encoded,
name)) {
assertEquals(STAMPED_URL, service.stamp(invoice(source)), source);
URI request = URI.create(captured.get().url());
assertEquals("upms-internal", request.getHost());
assertEquals("/sys-file/oss/file", request.getPath());
Map<String, String> params = query(request);
assertEquals(name, params.get("fileName"));
assertEquals("service", params.get("appId"));
assertEquals("v1", params.get("kid"));
assertEquals(Long.toString(now), params.get("ts"));
assertEquals(FileAccessSignature.sign(name, "service", "v1", now, key), params.get("sign"));
}
verify(files, times(7)).getFile(name);
}
@Test
void readsAndClosesSourceBeforeUploadingACompositedImage() throws Exception {
byte[] image = image();
var closed = new AtomicBoolean();
var input = new ByteArrayInputStream(image) {
@Override public void close() throws IOException { closed.set(true); super.close(); }
};
when(files.getFile("base.png")).thenReturn(response(200).body(input, image.length).build());
when(files.innerUpload(any(), isNull(), isNull(), isNull())).thenAnswer(call -> {
assertTrue(closed.get());
MultipartFile upload = call.getArgument(0);
assertEquals("image/png", upload.getContentType());
BufferedImage stamped = ImageIO.read(upload.getInputStream());
assertNotNull(stamped);
assertEquals(700, stamped.getWidth());
assertEquals(400, stamped.getHeight());
assertTrue(java.util.Arrays.stream(stamped.getRGB(0, 0, 700, 400, null, 0, 700))
.anyMatch(pixel -> pixel != Color.WHITE.getRGB()));
return R.ok(Map.of("url", STAMPED_URL));
});
assertEquals(STAMPED_URL, service.stamp(invoice("/admin/sys-file/oss/file?fileName=base.png")));
verify(files).innerUpload(any(), isNull(), isNull(), isNull());
}
@Test
void failedMissingAndNonImageResponsesNeverUploadAStampedResult() {
for (Response response : List.of(
response(403).body("forbidden", StandardCharsets.UTF_8).build(),
response(404).body(new byte[0]).build(),
response(500).body(new byte[0]).build(),
response(200).build(),
response(200).body("not an image", StandardCharsets.UTF_8).build())) {
when(files.getFile("base.png")).thenReturn(response);
assertNull(service.stamp(invoice("/admin/sys-file/oss/file?fileName=base.png")));
}
verify(files, never()).innerUpload(any(), any(), any(), any());
}
@Test
void malformedFileCenterReferencesFailBeforeAnyRemoteReadOrUpload() {
for (String source : List.of(
"/admin/sys-file/oss/file?fileName=one.png&fileName=two.png",
"/admin/sys-file/oss/file?fileName=",
"/admin/sys-file/oss/file?fileName=..%2Fsecret.png",
"file:/admin/sys-file/oss/file?fileName=base.png")) {
assertNull(service.stamp(invoice(source)));
}
verifyZeroInteractions(files);
}
@Test
void thirdPartyImagesKeepTheirOriginalHttpRequestWithoutServiceCredentials() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
var sourceRequest = new AtomicReference<URI>();
byte[] image = image();
server.createContext("/external.png", exchange -> {
sourceRequest.set(exchange.getRequestURI());
exchange.sendResponseHeaders(200, image.length);
try (var output = exchange.getResponseBody()) { output.write(image); }
});
server.start();
try {
when(files.innerUpload(any(), isNull(), isNull(), isNull())).thenReturn(R.ok(Map.of("url", STAMPED_URL)));
String source = "http://127.0.0.1:" + server.getAddress().getPort() + "/external.png?version=1";
assertEquals(STAMPED_URL, service.stamp(invoice(source)));
assertEquals("/external.png?version=1", sourceRequest.get().toString());
verify(files, never()).getFile(anyString());
} finally {
server.stop(0);
}
}
private WalletRechargeInvoiceEntity invoice(String source) {
var invoice = new WalletRechargeInvoiceEntity();
invoice.setId(1L);
invoice.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode());
invoice.setPicUrl(source);
return invoice;
}
private Response.Builder response(int status) {
return Response.builder().request(Request.create(Request.HttpMethod.GET, "http://upms-internal/sys-file/oss/file",
Map.of(), null, StandardCharsets.UTF_8, null)).status(status).reason("test").headers(Map.of());
}
private byte[] image() throws IOException {
BufferedImage image = new BufferedImage(700, 400, BufferedImage.TYPE_INT_RGB);
var graphics = image.createGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
graphics.dispose();
try (var output = new ByteArrayOutputStream()) {
ImageIO.write(image, "PNG", output);
return output.toByteArray();
}
}
private Map<String, String> query(URI uri) {
Map<String, String> values = new LinkedHashMap<>();
for (String pair : uri.getRawQuery().split("&")) {
String[] parts = pair.split("=", 2);
assertNull(values.put(parts[0], URLDecoder.decode(parts[1], StandardCharsets.UTF_8)));
}
return values;
}
}