package com.talentcard.server.business.payment.huifu; import com.alibaba.fastjson.JSON; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Map; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.stereotype.Service; @Service public class HuifuPictureUploadGateway { private final HuifuPaymentProperties properties; private final Path uploadDirectory; private final RestTemplate restTemplate = new RestTemplate(); public HuifuPictureUploadGateway( HuifuPaymentProperties properties, @Value("${app.upload.directory:uploads}") String uploadDirectory ) { this.properties = properties; this.uploadDirectory = Path.of(uploadDirectory).toAbsolutePath().normalize(); } @SuppressWarnings("unchecked") public String uploadUrl(String fileUrl, String fileType) { ensureConfigured(); if (blank(fileType) || !fileType.matches("F\\d{2,3}")) { throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付图片类型无效"); } Path file = resolveUploadedFile(fileUrl); Map data = Map.of( "req_seq_id", BusinessIdGenerator.next("HP"), "req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE), "file_type", fileType ); MultiValueMap form = new LinkedMultiValueMap<>(); form.add("sys_id", properties.getSystemId().trim()); form.add("product_id", properties.getProductId().trim()); form.add("data", JSON.toJSONString(data)); form.add("file", new FileSystemResource(file)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); try { Map response = restTemplate.postForObject(endpoint(), new HttpEntity<>(form, headers), Map.class); if (response == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "汇付图片上传没有返回结果"); Object raw = response.get("data"); Map result = raw instanceof Map value ? (Map) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; String code = value(result, "resp_code", "sub_resp_code", "code"); String fileId = value(result, "file_id", "fileId"); if (!providerSuccess(code) || fileId.isBlank()) { String message = value(result, "resp_desc", "sub_resp_desc", "message"); throw new BusinessException(ErrorCode.BAD_REQUEST, message.isBlank() ? "汇付图片上传失败:" + code : message); } return fileId; } catch (BusinessException exception) { throw exception; } catch (RestClientException exception) { throw new HuifuPaymentGatewayException( "Huifu onboarding picture upload failed", outcomeUnknown(exception), exception ); } } private Path resolveUploadedFile(String fileUrl) { if (blank(fileUrl)) throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片不能为空"); try { String path = fileUrl.trim(); if (path.startsWith("http://") || path.startsWith("https://")) path = URI.create(path).getPath(); if (!path.startsWith("/uploads/")) { throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片必须是本系统上传的图片"); } String filename = Path.of(path).getFileName().toString(); Path resolved = uploadDirectory.resolve(filename).normalize(); if (!resolved.startsWith(uploadDirectory) || !Files.isRegularFile(resolved)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片不存在,请重新上传"); } String extension = extension(filename); if (!("jpg".equals(extension) || "jpeg".equals(extension) || "png".equals(extension) || "bmp".equals(extension))) { throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付仅支持 JPG、BMP、PNG 图片"); } if (Files.size(resolved) > 2L * 1024 * 1024) { throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片不能超过2MB"); } return resolved; } catch (BusinessException exception) { throw exception; } catch (Exception exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "进件图片地址无效"); } } private void ensureConfigured() { if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "汇付支付尚未启用"); if (blank(properties.getSystemId()) || blank(properties.getProductId())) { throw new BusinessException(ErrorCode.CONFLICT, "汇付图片上传配置不完整"); } } private String endpoint() { return ("prod".equalsIgnoreCase(properties.getEnvironment()) ? "https://api.huifu.com" : "https://opps-stbmertest.testpnr.com") + "/v2/supplementary/picture"; } private static boolean providerSuccess(String code) { return "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } private static String value(Map data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } private static boolean blank(String value) { return value == null || value.isBlank(); } private static String extension(String filename) { int dot = filename.lastIndexOf('.'); return dot < 0 ? "" : filename.substring(dot + 1).toLowerCase(); } private static boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } }