fix(hjc): 证件上传改为存入 OSS(此前只写本地盘、返回的地址打不开)
问题:上传接口只把文件写进本地磁盘,再把地址拼成 {bucketDomain}{path} 或
{file-server}/api/file{path}——文件从未进过 OSS;前者指向桶里不存在的对象,
后者该前缀在文件服务主机上是 nginx 404。两条分支给出的地址都打不开,
证件预览坏、OCR 取图也必然失败。
做法(照后台 hjc-vue 的姿势):新增 HjcOssUploadUtil,把文件转交统一 OSS 上传服务
POST {config.server-url}/oss/upload(multipart file + tenantId 头,匿名可用),
取 data.path(原图,避开 data.url 的 x-oss-process 缩略参数)作为地址。
- 不转发调用方的 Authorization:hjc 的 JWT 与核心后端不是同一套签名,转过去会被判 401
- part 的 Content-Type 保留为图片类型(否则 OSS 存成 octet-stream,浏览器变下载、
且 OCR 取图的 image/* 校验会拒掉自家上传的图)
- 上传失败即整个上传失败,不再回退成打不开的地址
- 未改动 common 的 FileController(多项目共用代码)
新增打桩单测 10 个(请求形状、path 优先、缩略参数剥离、失败码/非 JSON/无地址、类型嗅探)。
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
package com.gxwebsoft.hjc.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import com.gxwebsoft.common.core.security.JwtSubject;
|
||||
import com.gxwebsoft.common.core.security.JwtUtil;
|
||||
import com.gxwebsoft.common.core.utils.FileServerUtil;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.system.entity.Role;
|
||||
@@ -22,8 +19,10 @@ import com.gxwebsoft.hjc.entity.HjcEnterprise;
|
||||
import com.gxwebsoft.hjc.entity.HjcEnterpriseMaterial;
|
||||
import com.gxwebsoft.hjc.service.HjcEnterpriseMaterialService;
|
||||
import com.gxwebsoft.hjc.service.HjcEnterpriseService;
|
||||
import com.gxwebsoft.hjc.util.HjcOssUploadUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -33,7 +32,6 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -43,6 +41,7 @@ import java.util.Map;
|
||||
* 汇吉采 网站企业 注册/登录(企业名称+密码)
|
||||
*/
|
||||
@Tag(name = "汇吉采-企业登录")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/hjc/auth")
|
||||
public class HjcAuthController extends BaseController {
|
||||
@@ -60,43 +59,30 @@ public class HjcAuthController extends BaseController {
|
||||
@Resource
|
||||
private HjcEnterpriseMaterialService hjcEnterpriseMaterialService;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
private HjcOssUploadUtil hjcOssUploadUtil;
|
||||
|
||||
/** 注册必传的资质证件材料类型 */
|
||||
private static final List<String> REQUIRED_MATERIAL_TYPES =
|
||||
Arrays.asList("idcard_front", "idcard_back", "handbook", "license");
|
||||
|
||||
@Operation(summary = "注册证件上传(匿名,注册页专用,不写文件表)")
|
||||
@Operation(summary = "注册证件上传(匿名,注册页与资质页通用;文件存入 OSS)")
|
||||
@PostMapping("/upload")
|
||||
public ApiResult<Map<String, Object>> upload(@RequestParam("file") MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return fail("上传文件不能为空", null);
|
||||
}
|
||||
try {
|
||||
String dir = configProperties.getUploadPath();
|
||||
File upload = FileServerUtil.upload(file, dir, Boolean.TRUE.equals(configProperties.getUploadUuidName()));
|
||||
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1);
|
||||
// 默认走自建文件服务器
|
||||
String url = configProperties.getFileServer() + "/api/file" + path;
|
||||
// 云存储(OSS)时改用 bucketDomain
|
||||
Integer tenantId = getTenantId();
|
||||
if (tenantId != null) {
|
||||
JSONObject setting = JSONObject.parseObject(redisUtil.get("setting:upload:" + tenantId));
|
||||
if (setting != null) {
|
||||
String uploadMethod = setting.getString("uploadMethod");
|
||||
String bucketDomain = setting.getString("bucketDomain");
|
||||
if (StrUtil.isNotBlank(uploadMethod) && !"file".equals(uploadMethod)
|
||||
&& StrUtil.isNotBlank(bucketDomain)) {
|
||||
url = bucketDomain + path;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 委托统一 OSS 上传服务(server.websoft.top/api/oss/upload):它会按租户取云存储配置、
|
||||
// 把文件写进 OSS 并返回可访问地址。此前这里只写本地磁盘再拼 OSS 域名,文件从未进过 OSS。
|
||||
String url = hjcOssUploadUtil.upload(file, tenantId == null ? null : tenantId.toString());
|
||||
Map<String, Object> data = new HashMap<>(4);
|
||||
data.put("url", url);
|
||||
data.put("name", file.getOriginalFilename());
|
||||
return success(data);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
log.warn("HjcUpload: 证件上传失败 tenantId={} name={} size={}",
|
||||
getTenantId(), file.getOriginalFilename(), file.getSize(), e);
|
||||
return fail("上传失败", null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.gxwebsoft.hjc.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.apache.tika.Tika;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 证件图片上传:委托统一的 OSS 上传服务。
|
||||
*
|
||||
* <p><b>为什么是「委托」而不是自己 putObject</b>:平台里已有一个可用的 OSS 上传服务
|
||||
* (`{@code config.server-url}/oss/upload`,即 {@code server.websoft.top/api},与
|
||||
* {@code RequestUtil} 调的是同一个后端),后台(hjc-vue 的 {@code UploadImage})正是走它上传图片——
|
||||
* 它会按 {@code tenantId} 取该租户的云存储配置、把文件写进 OSS 并返回可访问地址。
|
||||
* 此前 hjc 的证件上传只写本地磁盘、再把地址拼成 OSS 域名,**文件从未进过 OSS**,
|
||||
* 于是返回的地址打不开(渲染 404、OCR 取图也 404)。这里按同一姿势委托上传,避免在 mp-java 里
|
||||
* 另起一套 OSS 凭据与代码({@code config.*} 里那套 AK 是给别的用途的,不该在这里重复消费)。</p>
|
||||
*
|
||||
* <p><b>不转发调用方的 Authorization</b>:该服务实测匿名即可用(只认 {@code tenantId} 头),
|
||||
* 而 hjc 前端的 JWT 与核心后端不是同一套签名,转发过去只会被判 401。故只带 {@code tenantId}。</p>
|
||||
*
|
||||
* <p><b>multipart 的 part 类型要正确</b>:该服务把上传 part 的 Content-Type 直接写成 OSS 对象元数据
|
||||
* (实测:part 传 image/jpeg,OSS 返回的 Content-Type 就是 image/jpeg)。若传成
|
||||
* application/octet-stream,浏览器会变成下载、且 {@code HjcOcrImageFetcher} 的 {@code image/*} 校验会把
|
||||
* 自家上传的证件图拒掉,因此这里显式解析/嗅探图片类型。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HjcOssUploadUtil {
|
||||
|
||||
/** 统一 OSS 上传服务路径(相对 {@code config.server-url}) */
|
||||
static final String OSS_UPLOAD_PATH = "/oss/upload";
|
||||
/** 连接超时 */
|
||||
static final int CONNECT_TIMEOUT_MS = 3000;
|
||||
/** 读写超时:证件图最大 5M,跨服务上传给足时间 */
|
||||
static final int READ_TIMEOUT_MS = 20000;
|
||||
|
||||
private static final Tika TIKA = new Tika();
|
||||
|
||||
@Resource
|
||||
private ConfigProperties configProperties;
|
||||
|
||||
private final OkHttpClient http = new OkHttpClient.Builder()
|
||||
.connectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 上传文件到 OSS,返回可访问的地址
|
||||
*
|
||||
* @param file 上传的文件(证件图片)
|
||||
* @param tenantId 租户 ID:OSS 上传服务据此取该租户的云存储配置
|
||||
* @return 图片地址(原图地址,不带缩略参数)
|
||||
* @throws IOException 上传失败(服务不可达、返回失败码、未返回地址)
|
||||
*/
|
||||
public String upload(MultipartFile file, String tenantId) throws IOException {
|
||||
byte[] bytes = file.getBytes();
|
||||
String filename = StrUtil.blankToDefault(file.getOriginalFilename(), "upload");
|
||||
RequestBody filePart = RequestBody.create(bytes, MediaType.parse(resolveContentType(file, bytes, filename)));
|
||||
MultipartBody body = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", filename, filePart)
|
||||
.build();
|
||||
Request.Builder builder = new Request.Builder()
|
||||
.url(configProperties.getServerUrl() + OSS_UPLOAD_PATH)
|
||||
.post(body);
|
||||
if (StrUtil.isNotBlank(tenantId)) {
|
||||
builder.header("tenantId", tenantId);
|
||||
}
|
||||
String responseBody = execute(builder.build());
|
||||
String url = extractUrl(responseBody);
|
||||
log.info("HjcUpload: 证件已上传 OSS tenantId={} name={} bytes={} url={}",
|
||||
tenantId, filename, bytes.length, url);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 HTTP 调用并返回响应体。该方法是唯一的 HTTP 出口,测试中可覆盖以注入打桩响应。
|
||||
*/
|
||||
protected String execute(Request request) throws IOException {
|
||||
try (Response response = http.newCall(request).execute()) {
|
||||
String body = response.body() == null ? null : response.body().string();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("OSS 上传服务返回 HTTP " + response.code() + ":" + brief(body));
|
||||
}
|
||||
if (StrUtil.isBlank(body)) {
|
||||
throw new IOException("OSS 上传服务返回空响应");
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中取可访问地址:响应形如
|
||||
* {@code {"code":0,"message":"操作成功","data":{"path":"https://oss.wsdns.cn/2026…/x.jpg","url":"…?x-oss-process=…"}}}
|
||||
*
|
||||
* <p>优先取 {@code data.path}:{@code data.url} 会带 {@code ?x-oss-process} 缩略参数,
|
||||
* 而证件要原图(缩略会影响证件识别的清晰度)。</p>
|
||||
*/
|
||||
static String extractUrl(String responseBody) throws IOException {
|
||||
JSONObject json;
|
||||
try {
|
||||
json = JSONObject.parseObject(responseBody);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("OSS 上传服务响应不是合法 JSON:" + brief(responseBody));
|
||||
}
|
||||
if (json == null || json.getIntValue("code") != 0) {
|
||||
String message = json == null ? null : json.getString("message");
|
||||
String error = json == null ? null : json.getString("error");
|
||||
throw new IOException("OSS 上传服务返回失败:" + message + " " + brief(error));
|
||||
}
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
String path = data == null ? null : data.getString("path");
|
||||
if (isHttpUrl(path)) {
|
||||
return path;
|
||||
}
|
||||
String url = data == null ? null : data.getString("url");
|
||||
if (isHttpUrl(url)) {
|
||||
// 兜底:去掉 x-oss-process 之类的处理参数,尽量拿原图
|
||||
int queryIndex = url.indexOf('?');
|
||||
return queryIndex > 0 ? url.substring(0, queryIndex) : url;
|
||||
}
|
||||
throw new IOException("OSS 上传服务未返回图片地址:" + brief(responseBody));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 part 的 Content-Type:优先用客户端声明的图片类型,否则用 Tika 按内容嗅探。
|
||||
*
|
||||
* <p>不能让证件图以 octet-stream 落到 OSS —— 见类注释。</p>
|
||||
*/
|
||||
static String resolveContentType(MultipartFile file, byte[] bytes, String filename) {
|
||||
String declared = file.getContentType();
|
||||
if (declared != null && declared.startsWith("image/")) {
|
||||
return declared;
|
||||
}
|
||||
try {
|
||||
String detected = TIKA.detect(bytes, filename);
|
||||
if (StrUtil.isNotBlank(detected) && detected.startsWith("image/")) {
|
||||
return detected;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("HjcUpload: 嗅探文件类型失败 name={}", filename, e);
|
||||
}
|
||||
return StrUtil.isNotBlank(declared) ? declared : "application/octet-stream";
|
||||
}
|
||||
|
||||
private static boolean isHttpUrl(String value) {
|
||||
return value != null && (value.startsWith("http://") || value.startsWith("https://"));
|
||||
}
|
||||
|
||||
private static String brief(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return text.length() <= 200 ? text : text.substring(0, 200) + "...";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.gxwebsoft.hjc.util;
|
||||
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okio.Buffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 证件上传(委托统一 OSS 上传服务)单测:不联网,覆盖请求形状与响应解析。
|
||||
*
|
||||
* <p>与百度云 OCR 客户端同一套路:唯一的 HTTP 出口 {@link HjcOssUploadUtil#execute(Request)} 被覆盖成打桩。</p>
|
||||
*/
|
||||
class HjcOssUploadUtilTest {
|
||||
|
||||
private static final String SERVER_URL = "https://server.websoft.top/api";
|
||||
|
||||
private static final String OK_RESPONSE =
|
||||
"{\"code\":0,\"message\":\"操作成功\",\"data\":{"
|
||||
+ "\"id\":85363,\"name\":\"license.jpg\",\"length\":51786,"
|
||||
+ "\"path\":\"https://oss.wsdns.cn/20260910/0a5e8112405243c6b8071eaff5e4971e.jpg\","
|
||||
+ "\"url\":\"https://oss.wsdns.cn/20260910/0a5e8112405243c6b8071eaff5e4971e.jpg?x-oss-process=image/resize,w_300\""
|
||||
+ "}}";
|
||||
|
||||
private StubUploader uploader;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
uploader = new StubUploader();
|
||||
ConfigProperties configProperties = new ConfigProperties();
|
||||
ReflectionTestUtils.setField(configProperties, "serverUrl", SERVER_URL);
|
||||
ReflectionTestUtils.setField(uploader, "configProperties", configProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 上传成功返回原图地址() throws IOException {
|
||||
uploader.response = OK_RESPONSE;
|
||||
|
||||
String url = uploader.upload(jpeg("license.jpg"), "10626");
|
||||
|
||||
assertEquals("https://oss.wsdns.cn/20260910/0a5e8112405243c6b8071eaff5e4971e.jpg", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 请求形状正确() throws IOException {
|
||||
uploader.response = OK_RESPONSE;
|
||||
|
||||
uploader.upload(jpeg("license.jpg"), "10626");
|
||||
|
||||
Request request = uploader.lastRequest;
|
||||
assertEquals(SERVER_URL + "/oss/upload", request.url().toString());
|
||||
assertEquals("POST", request.method());
|
||||
assertEquals("10626", request.header("tenantId"));
|
||||
// 不转发调用方的 Authorization:hjc 的 JWT 与核心后端不是同一套签名,转过去只会被 401
|
||||
assertEquals(null, request.header("Authorization"));
|
||||
|
||||
RequestBody body = request.body();
|
||||
assertTrue(body.contentType().toString().startsWith("multipart/form-data"));
|
||||
MultipartBody multipart = (MultipartBody) body;
|
||||
assertEquals(1, multipart.parts().size());
|
||||
MultipartBody.Part part = multipart.parts().get(0);
|
||||
// okhttp 4 的 Part 没有 name()/fileName(),从 Content-Disposition 取
|
||||
String disposition = part.headers().get("Content-Disposition");
|
||||
assertTrue(disposition.contains("name=\"file\""), disposition);
|
||||
assertTrue(disposition.contains("filename=\"license.jpg\""), disposition);
|
||||
// part 的类型必须保留为图片:该服务会把它写成 OSS 对象元数据,
|
||||
// 否则浏览器变下载、且 OCR 取图的 image/* 校验会拒掉自己上传的图
|
||||
assertEquals("image/jpeg", part.body().contentType().toString());
|
||||
Buffer buffer = new Buffer();
|
||||
part.body().writeTo(buffer);
|
||||
assertTrue(buffer.readByteArray().length > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 没有tenantId时不带该请求头() throws IOException {
|
||||
uploader.response = OK_RESPONSE;
|
||||
|
||||
uploader.upload(jpeg("license.jpg"), null);
|
||||
|
||||
assertEquals(null, uploader.lastRequest.header("tenantId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 只有带缩略参数的url时返回去掉参数的原图地址() throws IOException {
|
||||
uploader.response = "{\"code\":0,\"data\":{\"url\":\"https://oss.wsdns.cn/a/b.jpg?x-oss-process=image/resize,w_300\"}}";
|
||||
|
||||
String url = uploader.upload(jpeg("license.jpg"), "10626");
|
||||
|
||||
assertEquals("https://oss.wsdns.cn/a/b.jpg", url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 服务返回失败码时抛异常并带出message() {
|
||||
uploader.response = "{\"code\":401,\"message\":\"请退出重新登录\",\"error\":\"MalformedJwtException\"}";
|
||||
|
||||
IOException e = assertThrows(IOException.class, () -> uploader.upload(jpeg("license.jpg"), "10626"));
|
||||
|
||||
assertTrue(e.getMessage().contains("请退出重新登录"));
|
||||
assertTrue(e.getMessage().contains("401") || e.getMessage().contains("失败"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 响应不是JSON时抛异常() {
|
||||
uploader.response = "<html>502 Bad Gateway</html>";
|
||||
|
||||
IOException e = assertThrows(IOException.class, () -> uploader.upload(jpeg("license.jpg"), "10626"));
|
||||
|
||||
assertTrue(e.getMessage().contains("不是合法 JSON"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 未返回地址时抛异常() {
|
||||
uploader.response = "{\"code\":0,\"message\":\"操作成功\",\"data\":{\"id\":1,\"name\":\"a.jpg\"}}";
|
||||
|
||||
IOException e = assertThrows(IOException.class, () -> uploader.upload(jpeg("license.jpg"), "10626"));
|
||||
|
||||
assertTrue(e.getMessage().contains("未返回图片地址"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void part类型优先用客户端声明的图片类型() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "a.png", "image/png", new byte[]{1, 2, 3});
|
||||
|
||||
assertEquals("image/png", HjcOssUploadUtil.resolveContentType(file, fileBytes(file), "a.png"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 客户端没声明图片类型时用嗅探() {
|
||||
// 只声明成 octet-stream(部分客户端如此),靠内容嗅探出 image/jpeg
|
||||
MockMultipartFile file = new MockMultipartFile("file", "a.jpg", "application/octet-stream",
|
||||
jpegBytes());
|
||||
|
||||
assertEquals("image/jpeg", HjcOssUploadUtil.resolveContentType(file, fileBytes(file), "a.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 既没声明也嗅探不出时回退octet_stream() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "a.bin", null,
|
||||
"not-an-image".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String contentType = HjcOssUploadUtil.resolveContentType(file, fileBytes(file), "a.bin");
|
||||
|
||||
assertFalse(contentType.startsWith("image/"));
|
||||
}
|
||||
|
||||
// ---------- 测试替身与夹具 ----------
|
||||
|
||||
private static class StubUploader extends HjcOssUploadUtil {
|
||||
|
||||
private String response;
|
||||
private Request lastRequest;
|
||||
|
||||
@Override
|
||||
protected String execute(Request request) throws IOException {
|
||||
this.lastRequest = request;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
private static MockMultipartFile jpeg(String filename) {
|
||||
return new MockMultipartFile("file", filename, "image/jpeg", jpegBytes());
|
||||
}
|
||||
|
||||
/** 最小 JPEG 头(FFD8FFE0 + JFIF),供 Tika 嗅探 */
|
||||
private static byte[] jpegBytes() {
|
||||
List<Byte> bytes = new ArrayList<>();
|
||||
int[] head = {0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
|
||||
0x00, 0x01, 0x00, 0x00, 0xFF, 0xD9};
|
||||
byte[] result = new byte[head.length];
|
||||
for (int i = 0; i < head.length; i++) {
|
||||
result[i] = (byte) head[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] fileBytes(MockMultipartFile file) {
|
||||
try {
|
||||
return file.getBytes();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user