Forráskód Böngészése

fix: 文件后缀校验 — 前后端双重验证 jpg/png/pdf

- before-upload: 前端拦截非法后缀 + 超过5MB
- accept 属性 + 上传提示文字: 支持 jpg / png / pdf,最多5个
- 后端 uploadImage: 后缀白名单校验 + imageType自动匹配
alphaH 1 napja
szülő
commit
9fbad32678

+ 45 - 15
frontend/src/views/module_payment/account/index.vue

@@ -124,12 +124,13 @@
                 <el-input v-model="onboardForm.scene_directions" type="textarea" :rows="3"
                   placeholder="谁/通过什么媒介(APP/web/小程序)/主要为谁提供什么服务/用于在什么场景给什么人群转账" maxlength="1024" show-word-limit />
               </el-form-item>
-              <el-form-item label="场景截图" prop="scene_image" required>
-                <el-upload ref="uploadRef" :action="''" :auto-upload="false"
-                  :file-list="onboardFiles" :on-change="handleOnboardFileChange" :limit="5"
-                  list-type="picture-card" accept="image/*,.pdf">
+              <el-form-item label="转账场景截图" prop="scene_image" required>
+                <el-upload ref="uploadRef" :http-request="handleOnboardUpload" :file-list="onboardFiles"
+                  :limit="5" list-type="picture-card" accept=".jpg,.jpeg,.png,.pdf"
+                  :before-upload="handleBeforeUpload" :on-remove="handleOnboardFileRemove">
                   <el-icon><Plus /></el-icon>
                 </el-upload>
+                <div class="upload-tip">支持 jpg / png / pdf,最多 5 个文件</div>
               </el-form-item>
               <el-form-item label="站点信息">
                 <el-checkbox-group v-model="onboardSiteTypes">
@@ -858,6 +859,7 @@ const SITE_TYPE_OPTIONS = [
 const onboardFormRef = ref<FormInstance>();
 const onboardLoading = ref(false);
 const onboardFiles = ref<any[]>([]);
+const onboardImageIds = ref<string[]>([]);
 const uploadRef = ref();
 const onboardSiteTypes = ref<string[]>([]);
 const onboardSites = reactive<Record<string, string>>({});
@@ -873,26 +875,53 @@ const onboardRules: FormRules = {
   scene_directions: [{ required: true, message: "请填写场景说明", trigger: "blur" }],
 };
 
-function handleOnboardFileChange(file: any) {
-  onboardFiles.value.push(file);
+const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".pdf"];
+
+function handleBeforeUpload(file: File) {
+  const ext = "." + file.name.split(".").pop()?.toLowerCase();
+  if (!ALLOWED_EXTENSIONS.includes(ext)) {
+    ElMessage.error("仅支持 jpg / png / pdf 格式");
+    return false;
+  }
+  if (file.size > 5 * 1024 * 1024) {
+    ElMessage.error("文件大小不能超过 5MB");
+    return false;
+  }
+  return true;
+}
+
+/** 自定义上传 — 选图即调用 image.upload */
+async function handleOnboardUpload(option: any) {
+  try {
+    const res = await AccountAPI.uploadImage(option.file, currentEnterpriseId.value!);
+    const imageId = res.data.data?.image_id;
+    if (imageId) {
+      onboardImageIds.value.push(imageId);
+      option.onSuccess();
+    } else {
+      option.onError(new Error("上传失败"));
+    }
+  } catch (e) {
+    option.onError(e);
+  }
+}
+
+function handleOnboardFileRemove(_file: any, fileList: any) {
+  // 移除对应位置的 image_id
+  const idx = fileList.findIndex((f: any) => f.uid === _file.uid);
+  if (idx >= 0) onboardImageIds.value.splice(idx, 1);
 }
 
 async function handleOnboardSubmit() {
   if (!onboardFormRef.value) return;
   await onboardFormRef.value.validate(async (valid) => {
     if (!valid) return;
-    if (onboardFiles.value.length === 0) {
-      ElMessage.warning("请上传场景截图");
+    if (onboardImageIds.value.length === 0) {
+      ElMessage.warning("请上传转账场景截图");
       return;
     }
     onboardLoading.value = true;
     try {
-      const imageIds: string[] = [];
-      for (const f of onboardFiles.value) {
-        const res = await AccountAPI.uploadImage(f.raw, currentEnterpriseId.value!);
-        const id = res.data.data?.image_id;
-        if (id) imageIds.push(id);
-      }
       // 构建 sites JSON
       let sites: string | undefined;
       if (onboardSiteTypes.value.length > 0) {
@@ -906,7 +935,7 @@ async function handleOnboardSubmit() {
         enterprise_id: currentEnterpriseId.value!,
         scene_code: onboardForm.scene_code,
         scene_directions: onboardForm.scene_directions,
-        scene_image: imageIds.join(","),
+        scene_image: onboardImageIds.value.join(","),
         sites,
         alipay_user_id: enterpriseStore.getCurrentEnterprise?.identity || "",
       });
@@ -927,6 +956,7 @@ async function handleOnboardSubmit() {
 function handleOnboardReset() {
   onboardFormRef.value?.resetFields();
   onboardFiles.value = [];
+  onboardImageIds.value = [];
   onboardSiteTypes.value = [];
   Object.keys(onboardSites).forEach(k => delete onboardSites[k]);
 }

+ 8 - 4
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java

@@ -221,11 +221,15 @@ public class AlipayTransferService {
      * alipay.fund.expandindirect.image.upload — 上传进件图片,返回 image_id 作为 scene_image 的 file_key
      */
     public String uploadImage(String enterpriseId, byte[] fileBytes, String fileName) {
+        if (fileName == null) throw new BusinessException(400, "文件名不能为空");
+        String ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
+        if (!ext.equals(".jpg") && !ext.equals(".jpeg") && !ext.equals(".png") && !ext.equals(".pdf"))
+            throw new BusinessException(400, "仅支持 jpg / png / pdf 格式,不支持: " + ext);
         try {
             AlipayFundExpandindirectImageUploadRequest request = new AlipayFundExpandindirectImageUploadRequest();
             request.setProductCode(ONBOARD_PRODUCT_CODE);
             request.setBizScene(ONBOARD_BIZ_SCENE);
-            request.setImageType("jpg");
+            request.setImageType(ext.equals(".pdf") ? "pdf" : "jpg");
             FileItem fileItem = new FileItem(fileName, fileBytes, fileName.substring(fileName.lastIndexOf('.') + 1));
             request.setImageContent(fileItem);
             AlipayFundExpandindirectImageUploadResponse response =
@@ -243,11 +247,11 @@ public class AlipayTransferService {
                                               String sceneDirections, String sceneImageKeys,
                                               String sites, String alipayUserId) {
         if (sceneCode == null || sceneCode.isBlank())
-            throw new BusinessException(400, "场景码不能为空");
+            throw new BusinessException(400, "转账场景码不能为空");
         if (sceneDirections == null || sceneDirections.isBlank())
-            throw new BusinessException(400, "场景说明不能为空");
+            throw new BusinessException(400, "转账场景说明不能为空");
         if (sceneImageKeys == null || sceneImageKeys.isBlank())
-            throw new BusinessException(400, "场景截图不能为空");
+            throw new BusinessException(400, "转账场景截图不能为空");
         String outBizNo = SnowflakeIdGenerator.nextIdStr();
         try {
             AlipayFundExpandindirectCreateModel model = new AlipayFundExpandindirectCreateModel();

+ 31 - 0
logs/payment-platform.log

@@ -1732,3 +1732,34 @@ Caused by: java.io.EOFException: null
 2026-07-15 14:21:41.842 [tomcat-handler-0] INFO  o.a.c.c.C.[.[localhost].[/api/v1] - Initializing Spring DispatcherServlet 'dispatcherServlet'
 2026-07-15 14:21:41.842 [tomcat-handler-0] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
 2026-07-15 14:21:41.845 [tomcat-handler-0] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 2 ms
+2026-07-15 14:26:12.853 [SpringApplicationShutdownHook] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} closing ...
+2026-07-15 14:26:12.855 [SpringApplicationShutdownHook] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} closed
+2026-07-15 14:26:17.453 [background-preinit] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 8.0.1.Final
+2026-07-15 14:26:17.514 [main] INFO  c.p.platform.PaymentApplication - Starting PaymentApplication using Java 21.0.11 with PID 39220 (D:\project2\payment-platform\java\target\classes started by 1 in D:\project2\payment-platform)
+2026-07-15 14:26:17.515 [main] INFO  c.p.platform.PaymentApplication - The following 1 profile is active: "dev"
+2026-07-15 14:26:18.340 [main] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Multiple Spring Data modules found, entering strict repository configuration mode
+2026-07-15 14:26:18.342 [main] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
+2026-07-15 14:26:18.391 [main] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 39 ms. Found 0 Redis repository interfaces.
+2026-07-15 14:26:18.928 [main] INFO  o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 8001 (http)
+2026-07-15 14:26:18.936 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8001"]
+2026-07-15 14:26:18.937 [main] INFO  o.a.catalina.core.StandardService - Starting service [Tomcat]
+2026-07-15 14:26:18.938 [main] INFO  o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.20]
+2026-07-15 14:26:18.980 [main] INFO  o.a.c.c.C.[.[localhost].[/api/v1] - Initializing Spring embedded WebApplicationContext
+2026-07-15 14:26:18.980 [main] INFO  o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1324 ms
+2026-07-15 14:26:19.003 [main] INFO  c.a.d.s.b.a.DruidDataSourceAutoConfigure - Init DruidDataSource
+2026-07-15 14:26:20.407 [main] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
+2026-07-15 14:26:21.106 [main] WARN  c.p.p.c.alipay.AlipayClientFactory - 支付宝默认配置不完整,将仅使用服务商/租户专属客户端
+2026-07-15 14:26:21.246 [main] INFO  c.p.platform.core.oss.OssService - OSS client initialized: bucket=hunanxiaojunzioss, endpoint=oss-cn-beijing.aliyuncs.com
+2026-07-15 14:26:21.562 [main] INFO  c.p.p.m.p.e.s.EnterpriseNameSyncScheduler - [企业名称同步] 调度器启动
+2026-07-15 14:26:21.801 [main] INFO  c.p.p.m.p.n.s.NotificationService - 已注册 7 个通知处理器: [AccountHandler, BillHandler, EmployeeHandler, EnterpriseHandler, InstitutionHandler, OrderHandler, VoucherHandler]
+2026-07-15 14:26:22.337 [main] INFO  o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@53570fff, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6e5df971, org.springframework.security.web.context.SecurityContextHolderFilter@1964ef9, org.springframework.security.web.header.HeaderWriterFilter@58a0f75b, org.springframework.web.filter.CorsFilter@3c7787a7, org.springframework.security.web.authentication.logout.LogoutFilter@2709e075, com.payment.platform.core.security.JwtAuthFilter@176054b7, com.payment.platform.core.security.TenantApiKeyAuthFilter@2d5e051e, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3891db3a, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4a778943, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2ca464bb, org.springframework.security.web.session.SessionManagementFilter@1b84d03d, org.springframework.security.web.access.ExceptionTranslationFilter@491f3fb0, org.springframework.security.web.access.intercept.AuthorizationFilter@6c2a33af]
+2026-07-15 14:26:22.710 [main] INFO  o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8001"]
+2026-07-15 14:26:22.722 [main] INFO  o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port 8001 (http) with context path '/api/v1'
+2026-07-15 14:26:22.840 [main] INFO  c.p.platform.PaymentApplication - Started PaymentApplication in 5.896 seconds (process running for 6.453)
+2026-07-15 14:26:24.762 [main] INFO  c.p.p.core.config.AppStartupRunner - 系统配置缓存初始化完成
+2026-07-15 14:26:26.373 [main] INFO  c.p.p.core.config.AppStartupRunner - 数据字典缓存初始化完成 (10 个字典类型)
+2026-07-15 14:28:00.697 [tomcat-handler-0] INFO  o.a.c.c.C.[.[localhost].[/api/v1] - Initializing Spring DispatcherServlet 'dispatcherServlet'
+2026-07-15 14:28:00.698 [tomcat-handler-0] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
+2026-07-15 14:28:00.701 [tomcat-handler-0] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 3 ms
+2026-07-15 14:31:21.986 [SpringApplicationShutdownHook] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} closing ...
+2026-07-15 14:31:21.988 [SpringApplicationShutdownHook] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} closed