Ver código fonte

feat: 企业入驻材料 — 营业执照/场景描述/合同/法人身份证 + OSS上传

- 企业入驻表单新增4项材料: 营业执照(图片)、业务场景描述(文本)、业务上下游合同(多文件)、法人手持身份证(图片)
- 仅格式校验(文件类型/大小),不做内容校验
- 文件上传迁移至阿里云OSS,支持自定义域名/CDN
- 企业列表新增"详情"按钮,详情页按文件类型渲染(图片预览/文档下载)
- 下载走后端代理强制Content-Disposition:attachment
alphah 1 dia atrás
pai
commit
ab16715362
18 arquivos alterados com 603 adições e 47 exclusões
  1. 29 1
      frontend/src/api/module_payment/enterprise.ts
  2. 40 0
      frontend/src/api/module_payment/enterprise/schema.ts
  3. 112 6
      frontend/src/views/module_payment/enterprise/components/EnterpriseDetail.vue
  4. 181 1
      frontend/src/views/module_payment/enterprise/components/EnterpriseForm.vue
  5. 10 1
      frontend/src/views/module_payment/enterprise/index.vue
  6. 7 0
      java/pom.xml
  7. 6 0
      java/sql/006_enterprise_materials.sql
  8. 26 0
      java/src/main/java/com/payment/platform/core/oss/OssProperties.java
  9. 86 0
      java/src/main/java/com/payment/platform/core/oss/OssService.java
  10. 8 5
      java/src/main/java/com/payment/platform/module/common/controller/FileController.java
  11. 16 31
      java/src/main/java/com/payment/platform/module/common/service/FileService.java
  12. 12 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseCreateDTO.java
  13. 12 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseUpdateDTO.java
  14. 12 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseVO.java
  15. 15 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/entity/EnterpriseEntity.java
  16. 4 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/service/AlipayEnterpriseService.java
  17. 16 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/service/EnterpriseService.java
  18. 11 2
      java/src/main/resources/application.yml

+ 29 - 1
frontend/src/api/module_payment/enterprise.ts

@@ -1,5 +1,33 @@
 import request from "@/utils/request";
 
+// 企业详情类型
+export interface EnterpriseDetail {
+  id?: number;
+  enterprise_id?: string;
+  out_biz_no?: string;
+  account_id?: string;
+  name?: string;
+  short_name?: string;
+  status?: string;
+  sign_url?: string;
+  pc_invite_url?: string;
+  invite_time?: string;
+  expire_time?: string;
+  identity_type?: string;
+  identity?: string;
+  identity_open_id?: string;
+  register_mode?: string;
+  sign_fund_way?: string;
+  base_info?: Record<string, any>;
+  profiles?: Record<string, any>;
+  created_time?: string;
+  updated_time?: string;
+  business_license?: string;
+  business_scenario?: string;
+  business_contracts?: string;
+  legal_rep_id_photo?: string;
+}
+
 // 身份类型选项
 export const IDENTITY_TYPE_OPTIONS = [
   { label: "支付宝用户ID", value: "ALIPAY_USER_ID" },
@@ -118,7 +146,7 @@ const enterpriseApi = {
    * 申请企业邀请码
    * @param data 申请邀请码数据
    */
-  applyInvite: (data: { identity_type: string; identity?: string; identity_open_id?: string }) => {
+  applyInvite: (data: Record<string, any>) => {
     return request({
       url: `/payment/enterprise/invite`,
       method: "post",

+ 40 - 0
frontend/src/api/module_payment/enterprise/schema.ts

@@ -43,6 +43,26 @@ export interface EnterpriseBaseSchema {
    * 扩展参数
    */
   profiles?: Record<string, any>;
+
+  /**
+   * 营业执照(图片路径)
+   */
+  business_license?: string;
+
+  /**
+   * 业务场景描述
+   */
+  business_scenario?: string;
+
+  /**
+   * 业务上下游合同(JSON数组,多个文件路径)
+   */
+  business_contracts?: string;
+
+  /**
+   * 法人手持身份证照片(图片路径)
+   */
+  legal_rep_id_photo?: string;
 }
 
 export interface EnterpriseCreateSchema extends EnterpriseBaseSchema {
@@ -94,6 +114,26 @@ export interface EnterpriseUpdateSchema {
    * 扩展参数
    */
   profiles?: Record<string, any>;
+
+  /**
+   * 营业执照(图片路径)
+   */
+  business_license?: string;
+
+  /**
+   * 业务场景描述
+   */
+  business_scenario?: string;
+
+  /**
+   * 业务上下游合同(JSON数组,多个文件路径)
+   */
+  business_contracts?: string;
+
+  /**
+   * 法人手持身份证照片(图片路径)
+   */
+  legal_rep_id_photo?: string;
 }
 
 export interface EnterpriseOutSchema {

+ 112 - 6
frontend/src/views/module_payment/enterprise/components/EnterpriseDetail.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="enterprise-detail">
+  <div class="enterprise-detail" v-loading="loading">
     <el-descriptions :column="3" border>
       <el-descriptions-item label="企业名称" :span="2">
         {{ detailData.name || "-" }}
@@ -94,6 +94,73 @@
       </el-descriptions>
     </template>
 
+    <el-divider content-position="left">入驻材料</el-divider>
+
+    <el-descriptions :column="3" border>
+      <!-- 营业执照 -->
+      <el-descriptions-item label="营业执照" :span="3">
+        <template v-if="detailData.business_license">
+          <el-image
+            :src="detailData.business_license"
+            :preview-src-list="[detailData.business_license]"
+            fit="contain"
+            style="max-width: 400px; max-height: 300px"
+          />
+          <div style="margin-top: 4px">
+            <el-link type="primary" :href="downloadUrl(detailData.business_license)" :underline="false">
+              <el-icon><Download /></el-icon> 下载
+            </el-link>
+          </div>
+        </template>
+        <span v-else>-</span>
+      </el-descriptions-item>
+
+      <!-- 业务场景描述 -->
+      <el-descriptions-item label="业务场景描述" :span="3">
+        <div style="white-space: pre-wrap; line-height: 1.6;">{{ detailData.business_scenario || "-" }}</div>
+      </el-descriptions-item>
+
+      <!-- 业务上下游合同 -->
+      <el-descriptions-item label="业务上下游合同" :span="3">
+        <template v-if="contractFiles.length > 0">
+          <div v-for="(file, i) in contractFiles" :key="i" style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
+            <template v-if="isImageFile(file)">
+              <el-image
+                :src="file"
+                :preview-src-list="contractFiles"
+                fit="cover"
+                style="width: 80px; height: 60px; border-radius: 4px"
+              />
+            </template>
+            <el-icon v-else :size="24" color="#909399"><Document /></el-icon>
+            <el-link type="primary" :href="file" target="_blank">{{ fileName(file) }}</el-link>
+            <el-link type="primary" :href="downloadUrl(file)" :underline="false">
+              <el-icon><Download /></el-icon>
+            </el-link>
+          </div>
+        </template>
+        <span v-else>-</span>
+      </el-descriptions-item>
+
+      <!-- 法人手持身份证 -->
+      <el-descriptions-item label="法人手持身份证" :span="3">
+        <template v-if="detailData.legal_rep_id_photo">
+          <el-image
+            :src="detailData.legal_rep_id_photo"
+            :preview-src-list="[detailData.legal_rep_id_photo]"
+            fit="contain"
+            style="max-width: 400px; max-height: 300px"
+          />
+          <div style="margin-top: 4px">
+            <el-link type="primary" :href="downloadUrl(detailData.legal_rep_id_photo)" :underline="false">
+              <el-icon><Download /></el-icon> 下载
+            </el-link>
+          </div>
+        </template>
+        <span v-else>-</span>
+      </el-descriptions-item>
+    </el-descriptions>
+
     <el-divider content-position="left">时间信息</el-divider>
 
     <el-descriptions :column="3" border>
@@ -108,14 +175,15 @@
 </template>
 
 <script setup lang="ts">
+import type { EnterpriseDetail } from "@/api/module_payment/enterprise";
 import EnterpriseAPI, {
-  EnterpriseDetail,
   STATUS_TAG_TYPE,
   STATUS_LABEL,
   REGISTER_MODE_OPTIONS,
   SIGN_FUND_WAY_OPTIONS,
 } from "@/api/module_payment/enterprise";
-import { onMounted, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
+import { Download, Document } from "@element-plus/icons-vue";
 
 interface Props {
   outBizNo: string;
@@ -141,11 +209,49 @@ function formatSignFundWay(way?: string) {
   return option ? option.label : way;
 }
 
+const contractFiles = computed(() => {
+  if (!detailData.value.business_contracts) return [];
+  try {
+    const parsed = typeof detailData.value.business_contracts === "string"
+      ? JSON.parse(detailData.value.business_contracts)
+      : detailData.value.business_contracts;
+    return Array.isArray(parsed) ? parsed : [];
+  } catch {
+    return [];
+  }
+});
+
+const IMG_EXT = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"];
+
+function isImageFile(url: string) {
+  const name = url.toLowerCase();
+  return IMG_EXT.some((ext) => name.endsWith(ext) || name.includes(ext + "?"));
+}
+
+function fileName(url: string) {
+  const name = url.split("/").pop() || url;
+  const qs = name.indexOf("?");
+  return qs > 0 ? name.substring(0, qs) : name;
+}
+
+function downloadUrl(ossUrl: string) {
+  return `/api/v1/common/file/download?url=${encodeURIComponent(ossUrl)}`;
+}
+
+const loading = ref(false);
+
 async function fetchDetail() {
   if (!props.outBizNo) return;
-  const res = await EnterpriseAPI.detail(props.outBizNo);
-  if (res.data) {
-    detailData.value = res.data;
+  loading.value = true;
+  try {
+    const res = await EnterpriseAPI.detail(props.outBizNo);
+    if (res.data) {
+      detailData.value = res.data;
+    }
+  } catch {
+    // 错误已由请求拦截器统一提示
+  } finally {
+    loading.value = false;
   }
 }
 

+ 181 - 1
frontend/src/views/module_payment/enterprise/components/EnterpriseForm.vue

@@ -95,6 +95,69 @@
         </el-row>
       </template>
     </el-form>
+
+    <!-- 入驻材料 -->
+    <el-divider content-position="left">入驻材料</el-divider>
+    <el-form
+      ref="materialFormRef"
+      :model="materialForm"
+      :rules="materialRules"
+      label-suffix=":"
+      label-width="auto"
+      label-position="right"
+    >
+      <el-form-item label="营业执照" prop="business_license">
+        <SingleImageUpload
+          v-model="materialForm.business_license"
+          accept="image/*"
+          :max-file-size="5"
+          :show-tip="true"
+          tip-text="支持 jpg/png 格式,文件大小不超过 5MB"
+        />
+      </el-form-item>
+
+      <el-form-item label="业务场景描述" prop="business_scenario">
+        <el-input
+          v-model="materialForm.business_scenario"
+          type="textarea"
+          :rows="4"
+          placeholder="请描述企业的业务场景"
+          maxlength="2000"
+          show-word-limit
+        />
+      </el-form-item>
+
+      <el-form-item label="业务上下游合同" prop="business_contracts">
+        <el-upload
+          ref="contractUploadRef"
+          v-model:file-list="contractFileList"
+          action="#"
+          :accept="'.pdf,.doc,.docx,.jpg,.jpeg,.png'"
+          :before-upload="handleContractBeforeUpload"
+          :http-request="handleContractUpload"
+          :on-remove="handleContractRemove"
+          :limit="10"
+          multiple
+          drag
+        >
+          <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+          <div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
+          <template #tip>
+            <div class="el-upload__tip">支持 pdf/doc/docx/jpg/png,单个不超过10MB,最多10个文件</div>
+          </template>
+        </el-upload>
+      </el-form-item>
+
+      <el-form-item label="法人手持身份证" prop="legal_rep_id_photo">
+        <SingleImageUpload
+          v-model="materialForm.legal_rep_id_photo"
+          accept="image/*"
+          :max-file-size="5"
+          :show-tip="true"
+          tip-text="支持 jpg/png 格式,文件大小不超过 5MB"
+        />
+      </el-form-item>
+    </el-form>
   </div>
 </template>
 
@@ -104,9 +167,13 @@ import {
 } from "@/api/module_payment/enterprise";
 import EnterpriseAPI from "@/api/module_payment/enterprise";
 import ProviderAPI, { type ServiceProviderOption } from "@/api/module_system/service_provider";
+import ParamsAPI from "@/api/module_system/params";
+import SingleImageUpload from "@/components/Upload/SingleImageUpload.vue";
+import { UploadFilled } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
 import { computed, onMounted, reactive, ref, watch } from "vue";
 import { useLoadingAction } from "@/composables/useLoadingAction";
+import type { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus";
 
 interface Props {
   type: "apply" | "update";
@@ -182,12 +249,103 @@ watch(
           formData.alipay_id_type = 'openid';
           formData.identity = data.identity_open_id;
         }
+        // 回显入驻材料
+        materialForm.business_license = data.business_license || "";
+        materialForm.business_scenario = data.business_scenario || "";
+        materialForm.legal_rep_id_photo = data.legal_rep_id_photo || "";
+        if (data.business_contracts) {
+          try {
+            const contracts = typeof data.business_contracts === "string"
+              ? JSON.parse(data.business_contracts)
+              : data.business_contracts;
+            contractFileList.value = (contracts as any[]).map((url: string, i: number) => ({
+              name: url.split("/").pop() || `contract_${i + 1}`,
+              url,
+            }));
+          } catch { /* ignore parse error */ }
+        }
       }
     }
   },
   { immediate: true }
 );
 
+// ---------- 入驻材料 ----------
+const materialFormRef = ref();
+const contractUploadRef = ref();
+const materialForm = reactive({
+  business_license: "",
+  business_scenario: "",
+  business_contracts: "",
+  legal_rep_id_photo: "",
+});
+
+const materialRules = reactive({
+  business_license: [{ required: true, message: "请上传营业执照", trigger: "change" }],
+  business_scenario: [
+    { required: true, message: "请输入业务场景描述", trigger: "blur" },
+    { max: 2000, message: "不超过2000字", trigger: "blur" },
+  ],
+  business_contracts: [{ required: true, message: "请上传业务上下游合同", trigger: "change" }],
+  legal_rep_id_photo: [{ required: true, message: "请上传法人手持身份证照片", trigger: "change" }],
+});
+
+const contractFileList = ref<UploadUserFile[]>([]);
+
+function handleContractBeforeUpload(file: UploadRawFile) {
+  const allowed = [".pdf", ".doc", ".docx", ".jpg", ".jpeg", ".png"];
+  const ext = "." + file.name.split(".").pop()?.toLowerCase();
+  if (!allowed.includes(ext)) {
+    ElMessage.warning("合同文件仅支持 pdf/doc/docx/jpg/png 格式");
+    return false;
+  }
+  if (file.size > 10 * 1024 * 1024) {
+    ElMessage.warning("单个文件不能超过10MB");
+    return false;
+  }
+  return true;
+}
+
+async function handleContractUpload(options: UploadRequestOptions) {
+  const formData = new FormData();
+  formData.append("file", options.file);
+  try {
+    const res = await ParamsAPI.uploadFile(formData);
+    if (res.data.code === 0 && res.data.data) {
+      const fileInfo = res.data.data;
+      // 替换 el-upload 已添加的条目,避免 push 导致重复
+      const idx = contractFileList.value.findIndex((f: any) => f.uid === options.file.uid);
+      const entry: any = {
+        name: fileInfo.file_name,
+        url: fileInfo.file_url,
+        status: "success",
+      };
+      if (idx >= 0) {
+        contractFileList.value[idx] = entry;
+      } else {
+        contractFileList.value.push(entry);
+      }
+      materialForm.business_contracts = JSON.stringify(
+        contractFileList.value.map((f: any) => f.url).filter(Boolean)
+      );
+      options.onSuccess(res.data);
+    } else {
+      ElMessage.error(res.data.msg || "上传失败");
+      options.onError(res.data as any);
+    }
+  } catch (e) {
+    ElMessage.error("合同上传失败");
+    options.onError(e as any);
+  }
+}
+
+function handleContractRemove(_file: UploadUserFile, fileList: UploadUserFile[]) {
+  contractFileList.value = fileList;
+  materialForm.business_contracts = JSON.stringify(
+    fileList.map((f) => f.url).filter(Boolean)
+  );
+}
+
 const identityLabel = computed(() => {
   const map: Record<string, string> = {
     ALIPAY_USER_ID: "支付宝uid",
@@ -254,10 +412,16 @@ async function submitForm() {
   const valid = await dataFormRef.value?.validate().catch(() => false);
   if (!valid || isSubmitting.value) return;
 
+  // 仅 apply 类型校验入驻材料
+  if (props.type === "apply") {
+    const materialValid = await materialFormRef.value?.validate().catch(() => false);
+    if (!materialValid) return;
+  }
+
   isSubmitting.value = true;
 
   try {
-    const submitData: { identity_type: string; identity?: string; identity_open_id?: string; service_provider_id?: number | null; scope_label?: string } = {
+    const submitData: Record<string, any> = {
       identity_type: formData.identity_type,
       identity: undefined,
       identity_open_id: undefined,
@@ -265,6 +429,14 @@ async function submitForm() {
       scope_label: formData.scope_label,
     };
 
+    // 入驻材料
+    if (props.type === "apply") {
+      submitData.business_license = materialForm.business_license;
+      submitData.business_scenario = materialForm.business_scenario;
+      submitData.business_contracts = materialForm.business_contracts;
+      submitData.legal_rep_id_photo = materialForm.legal_rep_id_photo;
+    }
+
     // 根据ID类型设置相应的字段
     if (formData.identity_type === 'ALIPAY_USER_ID') {
       if (formData.alipay_id_type === 'uid') {
@@ -305,6 +477,14 @@ async function submitForm() {
 function resetForm() {
   Object.assign(formData, JSON.parse(JSON.stringify(initialFormData)));
   dataFormRef.value?.resetFields();
+  Object.assign(materialForm, {
+    business_license: "",
+    business_scenario: "",
+    business_contracts: "",
+    legal_rep_id_photo: "",
+  });
+  contractFileList.value = [];
+  materialFormRef.value?.resetFields();
 }
 
 defineExpose({

+ 10 - 1
frontend/src/views/module_payment/enterprise/index.vue

@@ -53,8 +53,12 @@
                 </template>
               </el-table-column>
               <el-table-column v-if="contentCols.find((col) => col.prop === 'operation')?.show" fixed="right" label="操作"
-                align="center" min-width="220">
+                align="center" min-width="280">
                 <template #default="scope">
+                  <el-button type="primary" size="small" link
+                    @click="handleOpenDetail(scope.row)">
+                    详情
+                  </el-button>
                   <el-button v-hasPerm="['module_payment:facetoface:apply']" type="success" size="small" link
                     :disabled="scope.row.f2f_status && scope.row.f2f_status !== 'CLOSED'"
                     @click="handleOpenF2fApply(scope.row)">
@@ -462,6 +466,11 @@ const f2fRules = reactive<FormRules>({
   ],
 });
 
+function handleOpenDetail(row: any) {
+  currentOutBizNo.value = row.enterprise_id;
+  handleOpenDialog("detail");
+}
+
 function handleOpenF2fApply(enterprise: any) {
   currentF2fEnterprise.value = enterprise;
   Object.assign(f2fForm, {

+ 7 - 0
java/pom.xml

@@ -160,6 +160,13 @@
             <version>3.1.0</version>
         </dependency>
 
+        <!-- Alibaba Cloud OSS -->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.17.4</version>
+        </dependency>
+
         <!-- EasyExcel -->
         <dependency>
             <groupId>com.alibaba</groupId>

+ 6 - 0
java/sql/006_enterprise_materials.sql

@@ -0,0 +1,6 @@
+-- 企业入驻材料字段 — 营业执照/业务场景/合同/法人照片
+ALTER TABLE pay_enterprise
+    ADD COLUMN IF NOT EXISTS business_license   VARCHAR(500),
+    ADD COLUMN IF NOT EXISTS business_scenario  TEXT,
+    ADD COLUMN IF NOT EXISTS business_contracts TEXT,
+    ADD COLUMN IF NOT EXISTS legal_rep_id_photo VARCHAR(500);

+ 26 - 0
java/src/main/java/com/payment/platform/core/oss/OssProperties.java

@@ -0,0 +1,26 @@
+package com.payment.platform.core.oss;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "aliyun.oss")
+public class OssProperties {
+
+    /** OSS endpoint,如 oss-cn-hangzhou.aliyuncs.com */
+    private String endpoint = "";
+
+    /** OSS bucket 名称 */
+    private String bucket = "";
+
+    /** 访问密钥(复用 alibaba-cloud.access-key-id) */
+    private String accessKeyId = "";
+
+    /** 访问密钥(复用 alibaba-cloud.access-key-secret) */
+    private String accessKeySecret = "";
+
+    /** 自定义域名 / CDN 域名(可选,为空则用默认 OSS 域名) */
+    private String customDomain = "";
+}

+ 86 - 0
java/src/main/java/com/payment/platform/core/oss/OssService.java

@@ -0,0 +1,86 @@
+package com.payment.platform.core.oss;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import com.aliyun.oss.model.PutObjectResult;
+import com.payment.platform.common.exception.BusinessException;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.UUID;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OssService {
+
+    private final OssProperties ossProperties;
+
+    private OSS ossClient;
+
+    @PostConstruct
+    public void init() {
+        if (isConfigured()) {
+            ossClient = new OSSClientBuilder().build(
+                    ossProperties.getEndpoint(),
+                    ossProperties.getAccessKeyId(),
+                    ossProperties.getAccessKeySecret());
+            log.info("OSS client initialized: bucket={}, endpoint={}",
+                    ossProperties.getBucket(), ossProperties.getEndpoint());
+        } else {
+            log.warn("OSS not configured — uploads will fail until endpoint/bucket/credentials are set");
+        }
+    }
+
+    @PreDestroy
+    public void destroy() {
+        if (ossClient != null) {
+            ossClient.shutdown();
+        }
+    }
+
+    public boolean isConfigured() {
+        return ossProperties.getEndpoint() != null && !ossProperties.getEndpoint().isBlank()
+                && ossProperties.getBucket() != null && !ossProperties.getBucket().isBlank()
+                && ossProperties.getAccessKeyId() != null && !ossProperties.getAccessKeyId().isBlank()
+                && ossProperties.getAccessKeySecret() != null && !ossProperties.getAccessKeySecret().isBlank();
+    }
+
+    /**
+     * 上传文件到 OSS,返回完整的访问 URL
+     */
+    public String upload(MultipartFile file) {
+        if (!isConfigured()) {
+            throw new BusinessException(500, "OSS 未配置,无法上传文件");
+        }
+        String objectKey = buildObjectKey(file.getOriginalFilename());
+        try {
+            PutObjectResult result = ossClient.putObject(
+                    ossProperties.getBucket(), objectKey, file.getInputStream());
+            return buildUrl(objectKey);
+        } catch (IOException e) {
+            throw new BusinessException(400, "文件上传失败: " + e.getMessage());
+        }
+    }
+
+    private String buildObjectKey(String originalFilename) {
+        String ext = "";
+        if (originalFilename != null && originalFilename.contains(".")) {
+            ext = originalFilename.substring(originalFilename.lastIndexOf('.'));
+        }
+        return "upload/" + UUID.randomUUID() + ext;
+    }
+
+    private String buildUrl(String objectKey) {
+        String domain = ossProperties.getCustomDomain();
+        if (domain == null || domain.isBlank()) {
+            domain = ossProperties.getBucket() + "." + ossProperties.getEndpoint();
+        }
+        return "https://" + domain + "/" + objectKey;
+    }
+}

+ 8 - 5
java/src/main/java/com/payment/platform/module/common/controller/FileController.java

@@ -25,13 +25,16 @@ public class FileController {
         return Result.ok(fileService.upload(file));
     }
 
-    @PostMapping("/download")
-    public ResponseEntity<InputStreamResource> download(@RequestBody Map<String, String> body) {
-        String filePath = body.get("file_path");
-        InputStream is = fileService.download(filePath);
+    @GetMapping("/download")
+    public ResponseEntity<InputStreamResource> download(@RequestParam("url") String url) {
+        InputStream is = fileService.downloadFromUrl(url);
+        String filename = url.substring(url.lastIndexOf('/') + 1);
+        // 截掉 OSS URL 可能带有的 query string
+        int qs = filename.indexOf('?');
+        if (qs > 0) filename = filename.substring(0, qs);
         return ResponseEntity.ok()
                 .header(HttpHeaders.CONTENT_DISPOSITION,
-                        "attachment; filename=\"" + filePath.substring(filePath.lastIndexOf('/') + 1) + "\"")
+                        "attachment; filename=\"" + filename + "\"")
                 .contentType(MediaType.APPLICATION_OCTET_STREAM)
                 .body(new InputStreamResource(is));
     }

+ 16 - 31
java/src/main/java/com/payment/platform/module/common/service/FileService.java

@@ -1,55 +1,40 @@
 package com.payment.platform.module.common.service;
 
 import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.core.oss.OssService;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
 import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.UUID;
 
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class FileService {
 
-    @Value("${file.upload-path:static/upload}")
-    private String uploadPath;
+    private final OssService ossService;
 
     public Map<String, String> upload(MultipartFile file) {
-        try {
-            Path dir = Paths.get(uploadPath);
-            if (!Files.exists(dir)) Files.createDirectories(dir);
-            String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
-            Path target = dir.resolve(filename);
-            file.transferTo(target);
+        String url = ossService.upload(file);
 
-            Map<String, String> result = new LinkedHashMap<>();
-            result.put("file_name", filename);
-            result.put("origin_name", file.getOriginalFilename());
-            result.put("file_path", target.toString());
-            result.put("file_url", "/static/upload/" + filename);
-            result.put("file_size", String.valueOf(file.getSize()));
-            return result;
-        } catch (IOException e) {
-            throw new BusinessException(400, "上传失败: " + e.getMessage());
-        }
+        Map<String, String> result = new LinkedHashMap<>();
+        result.put("file_name", url.substring(url.lastIndexOf('/') + 1));
+        result.put("origin_name", file.getOriginalFilename());
+        result.put("file_path", url);
+        result.put("file_url", url);
+        result.put("file_size", String.valueOf(file.getSize()));
+        return result;
     }
 
-    public InputStream download(String filePath) {
+    public InputStream downloadFromUrl(String url) {
         try {
-            Path path = Paths.get(filePath);
-            if (!Files.exists(path))
-                throw new BusinessException(404, "文件不存在: " + filePath);
-            return Files.newInputStream(path);
-        } catch (IOException e) {
-            throw new BusinessException(500, "文件读取失败: " + e.getMessage());
+            return new java.net.URL(url).openStream();
+        } catch (java.io.IOException e) {
+            throw new BusinessException(500, "文件下载失败: " + e.getMessage());
         }
     }
 }

+ 12 - 0
java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseCreateDTO.java

@@ -28,4 +28,16 @@ public class EnterpriseCreateDTO {
 
     @Schema(description = "描述")
     private String description;
+
+    @Schema(description = "营业执照(图片路径)")
+    private String businessLicense;
+
+    @Schema(description = "业务场景描述")
+    private String businessScenario;
+
+    @Schema(description = "业务上下游合同(JSON数组)")
+    private String businessContracts;
+
+    @Schema(description = "法人手持身份证照片(图片路径)")
+    private String legalRepIdPhoto;
 }

+ 12 - 0
java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseUpdateDTO.java

@@ -29,4 +29,16 @@ public class EnterpriseUpdateDTO {
 
     @Schema(description = "描述")
     private String description;
+
+    @Schema(description = "营业执照(图片路径)")
+    private String businessLicense;
+
+    @Schema(description = "业务场景描述")
+    private String businessScenario;
+
+    @Schema(description = "业务上下游合同(JSON数组)")
+    private String businessContracts;
+
+    @Schema(description = "法人手持身份证照片(图片路径)")
+    private String legalRepIdPhoto;
 }

+ 12 - 0
java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseVO.java

@@ -77,4 +77,16 @@ public class EnterpriseVO {
 
     @Schema(description = "更新时间")
     private OffsetDateTime updatedTime;
+
+    @Schema(description = "营业执照")
+    private String businessLicense;
+
+    @Schema(description = "业务场景描述")
+    private String businessScenario;
+
+    @Schema(description = "业务上下游合同")
+    private String businessContracts;
+
+    @Schema(description = "法人手持身份证照片")
+    private String legalRepIdPhoto;
 }

+ 15 - 0
java/src/main/java/com/payment/platform/module/payment/enterprise/entity/EnterpriseEntity.java

@@ -33,9 +33,24 @@ public class EnterpriseEntity extends PaymentTenantBaseEntity {
     private Long serviceProviderId;
     private String scopeLabel;
 
+    /** 入驻材料:营业执照(图片路径) */
+    private String businessLicense;
+
+    /** 入驻材料:业务场景描述 */
+    private String businessScenario;
+
+    /** 入驻材料:业务上下游合同(JSON数组,多个文件路径) */
+    private String businessContracts;
+
+    /** 入驻材料:法人手持身份证照片(图片路径) */
+    private String legalRepIdPhoto;
+
     @JsonRawValue
     public String getBaseInfo() { return baseInfo; }
 
     @JsonRawValue
     public String getProfiles() { return profiles; }
+
+    @JsonRawValue
+    public String getBusinessContracts() { return businessContracts; }
 }

+ 4 - 0
java/src/main/java/com/payment/platform/module/payment/enterprise/service/AlipayEnterpriseService.java

@@ -58,6 +58,10 @@ public class AlipayEnterpriseService {
             pending.setStatus("PENDING");
             pending.setServiceProviderId(serviceProviderId);
             pending.setScopeLabel(scopeLabel);
+            pending.setBusinessLicense((String) data.get("business_license"));
+            pending.setBusinessScenario((String) data.get("business_scenario"));
+            pending.setBusinessContracts((String) data.get("business_contracts"));
+            pending.setLegalRepIdPhoto((String) data.get("legal_rep_id_photo"));
             enterpriseMapper.insert(pending);
 
             Map<String, Object> result = new LinkedHashMap<>();

+ 16 - 0
java/src/main/java/com/payment/platform/module/payment/enterprise/service/EnterpriseService.java

@@ -75,6 +75,10 @@ public class EnterpriseService {
         e.setDescription(dto.getDescription());
         e.setServiceProviderId(dto.getServiceProviderId());
         e.setScopeLabel(dto.getScopeLabel());
+        e.setBusinessLicense(dto.getBusinessLicense());
+        e.setBusinessScenario(dto.getBusinessScenario());
+        e.setBusinessContracts(dto.getBusinessContracts());
+        e.setLegalRepIdPhoto(dto.getLegalRepIdPhoto());
         enterpriseMapper.insert(e);
         return toVO(enterpriseMapper.selectById(e.getId()));
     }
@@ -92,6 +96,10 @@ public class EnterpriseService {
         if (dto.getSignFundWay() != null) e.setSignFundWay(dto.getSignFundWay());
         if (dto.getStatus() != null) e.setStatus(dto.getStatus());
         if (dto.getDescription() != null) e.setDescription(dto.getDescription());
+        if (dto.getBusinessLicense() != null) e.setBusinessLicense(dto.getBusinessLicense());
+        if (dto.getBusinessScenario() != null) e.setBusinessScenario(dto.getBusinessScenario());
+        if (dto.getBusinessContracts() != null) e.setBusinessContracts(dto.getBusinessContracts());
+        if (dto.getLegalRepIdPhoto() != null) e.setLegalRepIdPhoto(dto.getLegalRepIdPhoto());
         enterpriseMapper.updateById(e);
 
         // 对应 Python: 当状态为已激活或认证时,从支付宝同步企业信息
@@ -122,6 +130,10 @@ public class EnterpriseService {
         vo.setCreatedTime(e.getCreatedTime());
         vo.setServiceProviderId(e.getServiceProviderId());
         vo.setScopeLabel(e.getScopeLabel());
+        vo.setBusinessLicense(e.getBusinessLicense());
+        vo.setBusinessScenario(e.getBusinessScenario());
+        vo.setBusinessContracts(e.getBusinessContracts());
+        vo.setLegalRepIdPhoto(e.getLegalRepIdPhoto());
         return vo;
     }
 
@@ -151,6 +163,10 @@ public class EnterpriseService {
         vo.setUpdatedTime(e.getUpdatedTime());
         vo.setServiceProviderId(e.getServiceProviderId());
         vo.setScopeLabel(e.getScopeLabel());
+        vo.setBusinessLicense(e.getBusinessLicense());
+        vo.setBusinessScenario(e.getBusinessScenario());
+        vo.setBusinessContracts(e.getBusinessContracts());
+        vo.setLegalRepIdPhoto(e.getLegalRepIdPhoto());
         return vo;
     }
 }

+ 11 - 2
java/src/main/resources/application.yml

@@ -95,12 +95,21 @@ alibaba-cloud:
   access-key-id: LTAI5t5jHkQMY2T7V5n4ofMr
   access-key-secret: RYaofbQBRc93LC1oxCbgJGTVMCzNW4
 
-# File upload
+# File upload (OSS)
 file:
   upload-path: static/upload
-  allowed-extensions: .gif,.jpg,.jpeg,.png,.ico,.svg,.xls,.xlsx
+  allowed-extensions: .gif,.jpg,.jpeg,.png,.ico,.svg,.xls,.xlsx,.pdf,.doc,.docx
   max-size: 10485760
 
+# Aliyun OSS
+aliyun:
+  oss:
+    endpoint: "oss-cn-beijing.aliyuncs.com"
+    bucket: "hunanxiaojunzioss"
+    access-key-id: "LTAI5t5jHkQMY2T7V5n4ofMr"
+    access-key-secret: "RYaofbQBRc93LC1oxCbgJGTVMCzNW4"
+    custom-domain: ""
+
 # Logging
 logging:
   level: