Bläddra i källkod

feat: 支付宝多服务商架构 + 短信验证码 + Python配置清理 + 权限修复

- 新增 pay_service_provider 表及完整 Admin CRUD (含前后端)
- AlipayClientFactory 改造为按 enterpriseId → providerId 解析
- 企业入驻支持选择服务商绑定,通知回调合并绑定
- 所有 Alipay 调用方 (~60处) 改为 getClient(enterpriseId)
- 短信验证码:集成阿里云 DysmsAPI SDK 真实发送
- 员工编辑接口补全 (本地+支付宝SDK)
- 权限修复:后端返回完整权限列表,前端收集 type=3 按钮权限
- 租户拦截器忽略新表 + JOIN表 (sys_user_positions等)
- 清理全部 Python 工程残留 (19个文件 + .gitignore/docker等)
- yml 支付宝密钥移入 DB,仅留空占位 fallback
- 前端菜单权限 SQL + 服务商管理页面
- 禁用/启用二次确认、编辑仅限名称+业务范围(密钥走DB)
alphah 15 timmar sedan
förälder
incheckning
e3c789634c
50 ändrade filer med 1292 tillägg och 132 borttagningar
  1. 16 0
      .codegraph/.gitignore
  2. BIN
      docs/api清单.docx
  3. BIN
      docs/调用接口代商家发起开通当面付申请.docx
  4. 8 0
      frontend/src/api/module_payment/employee.ts
  5. 74 0
      frontend/src/api/module_system/service_provider.ts
  6. 1 0
      frontend/src/api/module_system/user.ts
  7. 6 0
      frontend/src/store/modules/user.store.ts
  8. 6 2
      frontend/src/views/module_payment/employee/components/EmployeeForm.vue
  9. 43 3
      frontend/src/views/module_payment/enterprise/components/EnterpriseForm.vue
  10. 322 0
      frontend/src/views/module_system/service_provider/index.vue
  11. 44 0
      java/sql/002_service_provider.sql
  12. 19 0
      java/sql/003_service_provider_menu.sql
  13. 125 55
      java/src/main/java/com/payment/platform/core/alipay/AlipayClientFactory.java
  14. 38 0
      java/src/main/java/com/payment/platform/core/scheduler/ScheduledTasks.java
  15. 23 0
      java/src/main/java/com/payment/platform/core/sms/AliyunSmsConfig.java
  16. 71 0
      java/src/main/java/com/payment/platform/core/sms/AliyunSmsSender.java
  17. 3 1
      java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java
  18. 3 3
      java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java
  19. 7 7
      java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java
  20. 3 3
      java/src/main/java/com/payment/platform/module/payment/department/service/AlipayDepartmentService.java
  21. 26 0
      java/src/main/java/com/payment/platform/module/payment/employee/controller/EmployeeController.java
  22. 28 4
      java/src/main/java/com/payment/platform/module/payment/employee/service/AlipayEmployeeService.java
  23. 6 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseCreateDTO.java
  24. 6 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/dto/EnterpriseVO.java
  25. 2 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/entity/EnterpriseEntity.java
  26. 20 4
      java/src/main/java/com/payment/platform/module/payment/enterprise/service/AlipayEnterpriseService.java
  27. 6 0
      java/src/main/java/com/payment/platform/module/payment/enterprise/service/EnterpriseService.java
  28. 2 2
      java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionScopeSyncService.java
  29. 13 13
      java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionService.java
  30. 2 2
      java/src/main/java/com/payment/platform/module/payment/expense/quota/service/IssueBatchService.java
  31. 8 8
      java/src/main/java/com/payment/platform/module/payment/expense/quota/service/QuotaService.java
  32. 3 3
      java/src/main/java/com/payment/platform/module/payment/expense/rule/service/RuleService.java
  33. 4 4
      java/src/main/java/com/payment/platform/module/payment/facetoface/service/FacetofaceService.java
  34. 1 1
      java/src/main/java/com/payment/platform/module/payment/notification/handler/BillHandler.java
  35. 14 2
      java/src/main/java/com/payment/platform/module/payment/notification/handler/EnterpriseHandler.java
  36. 1 1
      java/src/main/java/com/payment/platform/module/payment/notification/handler/VoucherHandler.java
  37. 79 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/controller/ServiceProviderController.java
  38. 29 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/dto/ServiceProviderCreateDTO.java
  39. 13 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/dto/ServiceProviderUpdateDTO.java
  40. 25 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/dto/ServiceProviderVO.java
  41. 25 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/entity/ServiceProviderEntity.java
  42. 9 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/mapper/ServiceProviderMapper.java
  43. 112 0
      java/src/main/java/com/payment/platform/module/payment/serviceprovider/service/ServiceProviderService.java
  44. 5 9
      java/src/main/java/com/payment/platform/module/system/menu/mapper/MenuMapper.java
  45. 3 1
      java/src/main/java/com/payment/platform/module/system/position/mapper/PositionMapper.java
  46. 28 0
      java/src/main/java/com/payment/platform/module/system/role/dto/RoleExportVO.java
  47. 2 0
      java/src/main/java/com/payment/platform/module/system/role/mapper/RoleMapper.java
  48. 3 0
      java/src/main/java/com/payment/platform/module/system/user/dto/UserInfoVO.java
  49. 1 0
      java/src/main/java/com/payment/platform/module/system/user/service/UserService.java
  50. 4 4
      java/src/main/resources/application.yml

+ 16 - 0
.codegraph/.gitignore

@@ -0,0 +1,16 @@
+# CodeGraph data files
+# These are local to each machine and should not be committed
+
+# Database
+*.db
+*.db-wal
+*.db-shm
+
+# Cache
+cache/
+
+# Logs
+*.log
+
+# Hook markers
+.dirty

BIN
docs/api清单.docx


BIN
docs/调用接口代商家发起开通当面付申请.docx


+ 8 - 0
frontend/src/api/module_payment/employee.ts

@@ -51,6 +51,14 @@ const EmployeeAPI = {
       data: body,
     });
   },
+
+  updateEmployee(employeeId: string, body: Record<string, unknown>) {
+    return request<ApiResponse<EmployeeOperation>>({
+      url: `${API_PATH}/${employeeId}`,
+      method: "put",
+      data: body,
+    });
+  },
 };
 
 export default EmployeeAPI;

+ 74 - 0
frontend/src/api/module_system/service_provider.ts

@@ -0,0 +1,74 @@
+import request from "@/utils/request";
+
+export interface ServiceProviderTable {
+  id?: number;
+  name?: string;
+  scopeLabel?: string;
+  appId?: string;
+  serverUrl?: string;
+  signType?: string;
+  format?: string;
+  charset?: string;
+  providerStatus?: string;
+  description?: string;
+  appPrivateKeyHint?: string;
+  alipayPublicKeyHint?: string;
+  createdTime?: string;
+  updatedTime?: string;
+}
+
+export interface ServiceProviderListResult {
+  total: number;
+  items: ServiceProviderTable[];
+}
+
+export interface ServiceProviderCreateForm {
+  name: string;
+  scopeLabel: string;
+  appId: string;
+  appPrivateKey: string;
+  alipayPublicKey: string;
+  serverUrl?: string;
+  signType?: string;
+  format?: string;
+  charset?: string;
+  description?: string;
+}
+
+export interface ServiceProviderUpdateForm {
+  name?: string;
+  scopeLabel?: string;
+  appId?: string;
+  appPrivateKey?: string;
+  alipayPublicKey?: string;
+  serverUrl?: string;
+  signType?: string;
+  format?: string;
+  charset?: string;
+  description?: string;
+}
+
+export interface ServiceProviderOption {
+  id: number;
+  name: string;
+  scopeLabel: string;
+}
+
+const API = {
+  list: (params: any) =>
+    request.get("/system/service-provider/list", { params }),
+  detail: (id: number) =>
+    request.get(`/system/service-provider/detail/${id}`),
+  create: (data: ServiceProviderCreateForm) =>
+    request.post("/system/service-provider/create", data),
+  update: (id: number, data: ServiceProviderUpdateForm) =>
+    request.put(`/system/service-provider/update/${id}`, data),
+  delete: (ids: number[]) =>
+    request.delete("/system/service-provider/delete", { data: ids }),
+  toggle: (id: number) =>
+    request.patch(`/system/service-provider/toggle/${id}`),
+  options: () =>
+    request.get("/system/service-provider/options"),
+};
+
+export default API;

+ 1 - 0
frontend/src/api/module_system/user.ts

@@ -182,6 +182,7 @@ export interface UserInfo extends BaseType {
   gender?: string;
   password?: string;
   menus?: MenuTable[];
+  permissions?: string[];
   dept?: deptTreeType;
   dept_id?: deptTreeType["id"];
   dept_name?: deptTreeType["name"];

+ 6 - 0
frontend/src/store/modules/user.store.ts

@@ -94,6 +94,12 @@ export const useUserStore = defineStore("user", {
 
       // 收集所有权限
       collect(allMenus);
+
+      // 额外收集后端返回的权限标识列表(含 type=3 按钮权限)
+      if (this.basicInfo.permissions && Array.isArray(this.basicInfo.permissions)) {
+        this.basicInfo.permissions.forEach((p: string) => permissionSet.add(p));
+      }
+
       this.prems = Array.from(permissionSet);
     },
     setAvatar(avatar: string) {

+ 6 - 2
frontend/src/views/module_payment/employee/components/EmployeeForm.vue

@@ -263,9 +263,13 @@ async function handleSaveAndAddNext() {
 
     if (props.type === "create") {
       await EmployeeAPI.createEmployee(submitData as unknown as EmployeeForm);
+      resetForm();
+      emit("successAndAddNext");
+    } else if (props.type === "update") {
+      await EmployeeAPI.updateEmployee(props.employeeId!, submitData);
+      resetForm();
+      emit("success");
     }
-    resetForm();
-    emit("successAndAddNext");
   } catch (error) {
     console.error(error);
   }

+ 43 - 3
frontend/src/views/module_payment/enterprise/components/EnterpriseForm.vue

@@ -8,6 +8,26 @@
       label-width="auto"
       label-position="right"
     >
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="服务商" prop="service_provider_id">
+            <el-select
+              v-model="formData.service_provider_id"
+              placeholder="请选择服务商"
+              style="width: 100%"
+              @change="handleProviderChange"
+            >
+              <el-option
+                v-for="item in providerOptions"
+                :key="item.id"
+                :label="item.name + ' (' + item.scope_label + ')'"
+                :value="item.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
       <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="身份类型" prop="identity_type">
@@ -83,8 +103,9 @@ import {
   IDENTITY_TYPE_OPTIONS,
 } from "@/api/module_payment/enterprise";
 import EnterpriseAPI from "@/api/module_payment/enterprise";
+import ProviderAPI, { type ServiceProviderOption } from "@/api/module_system/service_provider";
 import { ElMessage } from "element-plus";
-import { computed, reactive, ref, watch } from "vue";
+import { computed, onMounted, reactive, ref, watch } from "vue";
 import { useLoadingAction } from "@/composables/useLoadingAction";
 
 interface Props {
@@ -101,17 +122,33 @@ const emit = defineEmits<{
 const dataFormRef = ref();
 const isSubmitting = ref(false);
 
+const providerOptions = ref<ServiceProviderOption[]>([]);
+
+onMounted(async () => {
+  try {
+    const res = await ProviderAPI.options();
+    providerOptions.value = res.data.data || [];
+  } catch { /* ignore */ }
+});
+
 const initialFormData = {
   identity_type: "ALIPAY_USER_ID",
-  alipay_id_type: "uid", // 默认选择uid
+  alipay_id_type: "uid",
   identity: undefined,
   identity_open_id: undefined,
+  service_provider_id: null as number | null,
+  scope_label: "",
 };
 
 const formData = reactive(
   JSON.parse(JSON.stringify(initialFormData))
 );
 
+function handleProviderChange(val: number) {
+  const provider = providerOptions.value.find(p => p.id === val);
+  formData.scope_label = provider?.scope_label || "";
+}
+
 // 身份类型变化处理
 function handleIdentityTypeChange() {
   if (formData.identity_type === 'ALIPAY_USER_ID') {
@@ -201,6 +238,7 @@ const validateIdentity = (_rule: unknown, value: string, callback: (error?: Erro
 const { pageLoading, loadingText, execute: loadingExecute } = useLoadingAction();
 
 const rules = reactive({
+  service_provider_id: [{ required: true, message: "请选择服务商", trigger: "change" }],
   identity_type: [{ required: true, message: "请选择身份类型", trigger: "change" }],
   alipay_id_type: [
     {
@@ -219,10 +257,12 @@ async function submitForm() {
   isSubmitting.value = true;
 
   try {
-    const submitData: { identity_type: string; identity?: string; identity_open_id?: string } = {
+    const submitData: { identity_type: string; identity?: string; identity_open_id?: string; service_provider_id?: number | null; scope_label?: string } = {
       identity_type: formData.identity_type,
       identity: undefined,
       identity_open_id: undefined,
+      service_provider_id: formData.service_provider_id,
+      scope_label: formData.scope_label,
     };
 
     // 根据ID类型设置相应的字段

+ 322 - 0
frontend/src/views/module_system/service_provider/index.vue

@@ -0,0 +1,322 @@
+<!-- 服务商管理 -->
+<template>
+  <div class="app-container">
+    <PageSearch
+      ref="searchRef"
+      :search-config="searchConfig"
+      @query-click="handleQueryClick"
+      @reset-click="handleResetClick"
+    />
+
+    <PageContent ref="contentRef" :content-config="contentConfig">
+      <template #toolbar="{ toolbarRight, onToolbar, removeIds, cols }">
+        <CrudToolbarLeft
+          :remove-ids="removeIds"
+          :perm-create="['module_system:service_provider:create']"
+          :perm-delete="['module_system:service_provider:delete']"
+          @add="handleOpenDialog('create')"
+          @delete="onToolbar('delete')"
+        />
+        <div class="data-table__toolbar--right">
+          <CrudToolbarRight :buttons="toolbarRight" :cols="cols" :on-toolbar="onToolbar" />
+        </div>
+      </template>
+
+      <template #table="{ data, loading, tableRef, onSelectionChange, pagination }">
+        <div class="data-table__content">
+          <el-table
+            :ref="tableRef as any"
+            v-loading="loading"
+            row-key="id"
+            :data="data"
+            height="100%"
+            border
+            stripe
+            @selection-change="onSelectionChange"
+          >
+            <template #empty>
+              <el-empty :image-size="80" description="暂无数据" />
+            </template>
+            <el-table-column type="selection" min-width="55" align="center" />
+            <el-table-column label="序号" min-width="60" align="center">
+              <template #default="scope">
+                {{ (pagination.currentPage - 1) * pagination.pageSize + scope.$index + 1 }}
+              </template>
+            </el-table-column>
+            <el-table-column label="服务商名称" prop="name" min-width="140" show-overflow-tooltip />
+            <el-table-column label="业务范围" prop="scope_label" min-width="100" align="center" show-overflow-tooltip />
+            <el-table-column label="AppId" prop="app_id" min-width="200" show-overflow-tooltip />
+            <el-table-column label="网关地址" prop="server_url" min-width="220" show-overflow-tooltip />
+            <el-table-column label="状态" prop="provider_status" min-width="80" align="center">
+              <template #default="scope">
+                <el-tag :type="scope.row.provider_status === 'ACTIVE' ? 'success' : 'danger'">
+                  {{ scope.row.provider_status === "ACTIVE" ? "启用" : "禁用" }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="创建时间" prop="created_time" min-width="180" show-overflow-tooltip />
+            <el-table-column fixed="right" label="操作" align="center" min-width="220">
+              <template #default="scope">
+                <el-button
+                  v-hasPerm="['module_system:service_provider:detail']"
+                  type="info" size="small" link icon="View"
+                  @click="handleOpenDialog('detail', scope.row.id)"
+                >详情</el-button>
+                <el-button
+                  v-hasPerm="['module_system:service_provider:update']"
+                  type="primary" size="small" link icon="edit"
+                  @click="handleOpenDialog('update', scope.row.id)"
+                >编辑</el-button>
+                <el-button
+                  v-hasPerm="['module_system:service_provider:update']"
+                  type="warning" size="small" link icon="SwitchButton"
+                  @click="handleToggle(scope.row.id, scope.row.provider_status)"
+                >{{ scope.row.provider_status === 'ACTIVE' ? '停用' : '启用' }}</el-button>
+                <el-button
+                  v-hasPerm="['module_system:service_provider:delete']"
+                  type="danger" size="small" link icon="delete"
+                  @click="handleRowDelete(scope.row.id)"
+                >删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </template>
+    </PageContent>
+
+    <EnhancedDialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
+      <!-- 详情 -->
+      <template v-if="dialogVisible.type === 'detail'">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="服务商名称">{{ detailData.name }}</el-descriptions-item>
+          <el-descriptions-item label="业务范围">{{ detailData.scope_label }}</el-descriptions-item>
+          <el-descriptions-item label="AppId">{{ detailData.app_id }}</el-descriptions-item>
+          <el-descriptions-item label="网关地址" :span="2">{{ detailData.server_url }}</el-descriptions-item>
+          <el-descriptions-item label="密钥片段(App)" :span="2">{{ detailData.app_private_key_hint || '****' }}</el-descriptions-item>
+          <el-descriptions-item label="密钥片段(支付宝)" :span="2">{{ detailData.alipay_public_key_hint || '****' }}</el-descriptions-item>
+          <el-descriptions-item label="签名方式">{{ detailData.sign_type }}</el-descriptions-item>
+          <el-descriptions-item label="格式">{{ detailData.format }}</el-descriptions-item>
+          <el-descriptions-item label="编码">{{ detailData.charset }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="detailData.provider_status === 'ACTIVE' ? 'success' : 'danger'">
+              {{ detailData.provider_status === "ACTIVE" ? "启用" : "禁用" }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="描述" :span="2">{{ detailData.description || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ detailData.created_time }}</el-descriptions-item>
+          <el-descriptions-item label="更新时间">{{ detailData.updated_time }}</el-descriptions-item>
+        </el-descriptions>
+      </template>
+
+      <!-- 新增 -->
+      <template v-else-if="dialogVisible.type === 'create'">
+        <el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
+          <el-form-item label="服务商名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入名称" :maxlength="64" />
+          </el-form-item>
+          <el-form-item label="业务范围标签" prop="scope_label">
+            <el-input v-model="formData.scope_label" placeholder="如 DOMESTIC / OVERSEAS" :maxlength="64" />
+          </el-form-item>
+          <el-form-item label="AppId" prop="app_id">
+            <el-input v-model="formData.app_id" placeholder="支付宝应用 ID" :maxlength="64" />
+          </el-form-item>
+          <el-form-item label="应用私钥" prop="app_private_key">
+            <el-input v-model="formData.app_private_key" type="textarea" :rows="4" placeholder="粘贴 RSA2 私钥" />
+          </el-form-item>
+          <el-form-item label="支付宝公钥" prop="alipay_public_key">
+            <el-input v-model="formData.alipay_public_key" type="textarea" :rows="4" placeholder="粘贴支付宝 RSA2 公钥" />
+          </el-form-item>
+          <el-form-item label="网关地址" prop="server_url">
+            <el-input v-model="formData.server_url" placeholder="https://openapi.alipay.com/gateway.do" :maxlength="256" />
+          </el-form-item>
+          <el-form-item label="签名方式" prop="sign_type">
+            <el-select v-model="formData.sign_type" style="width: 100%">
+              <el-option label="RSA2" value="RSA2" />
+              <el-option label="RSA" value="RSA" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="描述" prop="description">
+            <el-input v-model="formData.description" type="textarea" :rows="2" placeholder="可选" :maxlength="512" />
+          </el-form-item>
+        </el-form>
+      </template>
+
+      <!-- 编辑(仅名称 + 业务范围) -->
+      <template v-else-if="dialogVisible.type === 'update'">
+        <el-form ref="dataFormRef" :model="formData" :rules="updateRules" label-suffix=":" label-width="auto" label-position="right">
+          <el-form-item label="服务商名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入名称" :maxlength="64" />
+          </el-form-item>
+          <el-form-item label="业务范围标签" prop="scope_label">
+            <el-input v-model="formData.scope_label" placeholder="如 DOMESTIC / OVERSEAS" :maxlength="64" />
+          </el-form-item>
+        </el-form>
+      </template>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleCloseDialog">取消</el-button>
+          <el-button
+            v-if="dialogVisible.type !== 'detail'"
+            v-hasPerm="['module_system:service_provider:' + (dialogVisible.type === 'create' ? 'create' : 'update')]"
+            type="primary"
+            :loading="submitLoading"
+            @click="handleSubmit"
+          >确定</el-button>
+          <el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
+        </div>
+      </template>
+    </EnhancedDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from "vue";
+import ProviderAPI, {
+  type ServiceProviderTable,
+  type ServiceProviderCreateForm,
+  type ServiceProviderUpdateForm,
+} from "@/api/module_system/service_provider";
+import CrudToolbarLeft from "@/components/CURD/CrudToolbarLeft.vue";
+import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
+import PageSearch from "@/components/CURD/PageSearch.vue";
+import PageContent from "@/components/CURD/PageContent.vue";
+import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
+import { useCrudList } from "@/components/CURD/useCrudList";
+import type { IContentConfig, ISearchConfig } from "@/components/CURD/types";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+defineOptions({ name: "ServiceProvider", inheritAttrs: false });
+
+const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
+const dataFormRef = ref();
+const submitLoading = ref(false);
+
+const searchConfig = reactive<ISearchConfig>({
+  permPrefix: "module_system:service_provider",
+  colon: true,
+  isExpandable: true,
+  showNumber: 2,
+  form: { labelWidth: "auto" },
+  formItems: [
+    { prop: "name", label: "服务商名称", type: "input", attrs: { placeholder: "请输入名称", clearable: true } },
+    { prop: "scope_label", label: "业务范围", type: "input", attrs: { placeholder: "如 DOMESTIC", clearable: true } },
+    { prop: "status", label: "状态", type: "select",
+      attrs: { placeholder: "请选择", clearable: true, style: { width: "167px" },
+        options: [{ label: "启用", value: "ACTIVE" }, { label: "禁用", value: "DISABLED" }] } },
+  ],
+});
+
+const contentConfig = reactive<IContentConfig<any>>({
+  permPrefix: "module_system:service_provider",
+  pk: "id",
+  cols: [
+    { prop: "selection", label: "选择框", show: true },
+    { prop: "index", label: "序号", show: true },
+    { prop: "name", label: "名称", show: true },
+    { prop: "scope_label", label: "业务范围", show: true },
+    { prop: "app_id", label: "AppId", show: true },
+    { prop: "server_url", label: "网关地址", show: true },
+    { prop: "provider_status", label: "状态", show: true },
+    { prop: "created_time", label: "创建时间", show: true },
+    { prop: "operation", label: "操作", show: true },
+  ] as any,
+  hideColumnFilter: false,
+  toolbar: [],
+  defaultToolbar: [{ name: "refresh", perm: "refresh" }, "filter"],
+  pagination: { pageSize: 10, pageSizes: [10, 20, 30, 50] },
+  request: { page_no: "page_no", page_size: "page_size" },
+  indexAction: async (params) => {
+    const res = await ProviderAPI.list(params);
+    return { total: res.data.data.total, list: res.data.data.items };
+  },
+  deleteAction: async (ids) => {
+    const idList = ids.split(",").map((s: string) => Number(s.trim())).filter((n: number) => !Number.isNaN(n));
+    await ProviderAPI.delete(idList);
+  },
+  deleteConfirm: { title: "警告", message: "确认删除该服务商?", type: "warning" },
+});
+
+const detailData = ref<ServiceProviderTable>({});
+const currentEditId = ref<number | null>(null);
+
+const formData = reactive({
+  name: "", scope_label: "DOMESTIC", app_id: "", app_private_key: "", alipay_public_key: "",
+  server_url: "https://openapi.alipay.com/gateway.do", sign_type: "RSA2", format: "JSON", charset: "UTF-8", description: "",
+});
+
+const dialogVisible = reactive({ title: "", visible: false, type: "create" as "create" | "update" | "detail" });
+
+const rules = reactive({
+  name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+  scope_label: [{ required: true, message: "请输入业务范围", trigger: "blur" }],
+  app_id: [{ required: true, message: "请输入 AppId", trigger: "blur" }],
+  app_private_key: [{ required: true, message: "请输入应用私钥", trigger: "blur" }],
+  alipay_public_key: [{ required: true, message: "请输入支付宝公钥", trigger: "blur" }],
+});
+
+const updateRules = reactive({
+  name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+  scope_label: [{ required: true, message: "请输入业务范围", trigger: "blur" }],
+});
+
+function handleRowDelete(id: number) { contentRef.value?.handleDelete(id); }
+
+async function handleToggle(id: number, currentStatus: string) {
+  const action = currentStatus === "ACTIVE" ? "停用" : "启用";
+  try {
+    await ElMessageBox.confirm(`确认${action}该服务商?`, "提示", { confirmButtonText: action, cancelButtonText: "取消", type: "warning" });
+    const res = await ProviderAPI.toggle(id);
+    ElMessage.success(res.data.data?.provider_status === "ACTIVE" ? "已启用" : "已停用");
+    refreshList();
+  } catch { /* 用户取消或失败 */ }
+}
+
+async function handleCloseDialog() {
+  dialogVisible.visible = false;
+  dataFormRef.value?.resetFields();
+  dataFormRef.value?.clearValidate();
+  Object.assign(formData, { name: "", scope_label: "DOMESTIC", app_id: "", app_private_key: "", alipay_public_key: "", server_url: "https://openapi.alipay.com/gateway.do", sign_type: "RSA2", format: "JSON", charset: "UTF-8", description: "" });
+  currentEditId.value = null;
+}
+
+async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
+  dialogVisible.type = type;
+  if (id) {
+    const response = await ProviderAPI.detail(id);
+    if (type === "detail") {
+      dialogVisible.title = "服务商详情";
+      Object.assign(detailData.value, response.data.data);
+    } else if (type === "update") {
+      dialogVisible.title = "编辑服务商";
+      Object.assign(formData, response.data.data);
+      currentEditId.value = id;
+    }
+  } else {
+    dialogVisible.title = "新增服务商";
+  }
+  dialogVisible.visible = true;
+}
+
+async function handleSubmit() {
+  dataFormRef.value.validate(async (valid: boolean) => {
+    if (!valid) return;
+    submitLoading.value = true;
+    try {
+      const id = currentEditId.value;
+      if (id) {
+        await ProviderAPI.update(id, formData as ServiceProviderUpdateForm);
+      } else {
+        await ProviderAPI.create(formData);
+      }
+      dialogVisible.visible = false;
+      await handleCloseDialog();
+      refreshList();
+    } catch (e: any) {
+      ElMessage.error(e?.message || "操作失败");
+    } finally {
+      submitLoading.value = false;
+    }
+  });
+}
+</script>

+ 44 - 0
java/sql/002_service_provider.sql

@@ -0,0 +1,44 @@
+-- ============================================
+-- 支付宝多服务商架构 — DDL + 初始数据
+-- 执行前请确认所在数据库为 payment_platform_prod
+-- ============================================
+
+-- 1. 创建服务商表
+CREATE TABLE IF NOT EXISTS pay_service_provider (
+    id                BIGSERIAL PRIMARY KEY,
+    name              VARCHAR(64)  NOT NULL,
+    scope_label       VARCHAR(64)  DEFAULT 'DOMESTIC',
+    app_id            VARCHAR(64)  NOT NULL,
+    app_private_key   TEXT         NOT NULL,
+    alipay_public_key TEXT         NOT NULL,
+    server_url        VARCHAR(256) DEFAULT 'https://openapi.alipay.com/gateway.do',
+    sign_type         VARCHAR(16)  DEFAULT 'RSA2',
+    format            VARCHAR(16)  DEFAULT 'JSON',
+    charset           VARCHAR(16)  DEFAULT 'UTF-8',
+    provider_status   VARCHAR(32)  DEFAULT 'ACTIVE',
+    description       VARCHAR(512),
+    status            VARCHAR(1)   DEFAULT '0',    -- MyBatis-Plus @TableLogic 软删除
+    uuid              VARCHAR(64),
+    created_time      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_time      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 2. 插入当前服务商(密钥来自旧 application.yml)
+INSERT INTO pay_service_provider (name, scope_label, app_id, app_private_key, alipay_public_key, server_url, sign_type, format, charset, provider_status, status)
+VALUES (
+    '国内服务商',
+    'DOMESTIC',
+    '2021005122654283',
+    'MIIEogIBAAKCAQEAnhU7Ix3+klh7k+5+Vx10tmqS2MkWD3MIkB+GpwZm94SccEISoLjCatW56U1QJo7D9SP1rSwyJ3dtvbiEpje8f0D+iaHilX9tkdWZMnsI9XbQqmqLkjDPFdmII3sa4p9Mx1UUOtIGmPhKYmnR8jM85SEmf9ivuFOKZISny5LxDYam2o3czGKjRiLgb7oNs4LKf9isc1k4r/XeCEAhLPejFgilSiKWC8DQbiSG15DvOKrr0BxM1gb0YUngQr11diUSYx5LRXWjhUE7WkPaSGkLDVuZ+ZAHddH9DddPrfjoQpiq7ROJNTkxVuNo5Bqont5fvLnzIN9/9pR8bJVLNBDFlQIDAQABAoIBADbXhryYG0kKCVU41/vA7EycjHVIza5uafoV9dDcF7ym9N69DVlUv53wp56Yg8XcoX7aCtEZFA09EYVQDjTcATjkg2mcD89tdcWyJIOuy1zc62czr1f4Nt+Np/0nKByWxzwf9/SwCDnpaWTa8UrlG6sh5QlVUyDPWqOkodGuGJJoJoaC/yM2kXjovR/JbKvqevRRI/ZpqUU8OXU1MzBuYH3OEWmYjDFpydIJnEYRS9U4Ftgo3q8diRbTSb1rPR5cxyJh6ikoRNLnrWuEX/B2QW32Qag6vYrjq+LXtW7hzZYoNUAaEzf2c8WP9nrzw3IBvAF+sBBhiyH8wlxcSEndR8ECgYEA8Ka2sy4iTuLkf9Qxd22IRAxXgnkh7ITB71OM4OIU5dZULysAbnwTc/9B6URyuQ7o64VFQQHZtXLM/Ujt2l09RU2caUBkzRS495g6H53mZSY78k4F6nI9eEqTE0PPRiPZ/NrV1ZeUdOU4p1iaO6KxHJrMY5hCRMns5OX/jm4DfhsCgYEAqCpdfpTRqW03pMpM0MN3S43Z4xdthxeBDa0mGGo3CpQImj8WvVaAMiTNjnH/NBupXT/akn6lURQANo9IjR4EvoA8vgtYBYOrB+kchDRwbl4elAzdGYcUWixBnuKaz23yncOFXai0RwcnSZ57lF++fUlC778u2MUoLGk8JeozRg8CgYAJDSw4BxcxQmV3zWJi7JLAhHpSJP46qC+nMcxNtRM2Jd6au5JTlYUhyssO3A04mq/2E9gr/sbShVPifvi7SuCAF9A3QT3JXOssHOSqxcShr1N35KliTRO0z5FCmz1TlQug8BY053OlBe4glTuP3Kmuur/PSy3K6pFndqMrF0Y4WwKBgGbaopyNQT8zQdlUsm7tXsWjWSUAa0k5IwHOaYJ9VHTv3eMZuzrK8VW6FM+PAdxJHumd6H25YDua1BaKxIErXxN1M7G5Fnko8y6/cWWa/TlD78f1pjW63MPaVbhsOOut/7pmn+eNC9Z1lZ39hPBXLxdJ+9rPQEPtMokXRGacPAgZAoGATt/Rko7fkFST19JOiMorFyp26cZQ91ZLrPADRIZEN4H9ShZhHTeSBN32ZGUFYsOjLzM8g0Xui2w2NAQBrmeHCGNXrV+vFRX6+s5cccUtohtpjYK07mz4RbeScWL/oebNubDR1XsdiUKe+JQsHT5MMksKk/kmBbIfIJ3wDdeJLlk=',
+    'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqUX9WqZPwh57HqR2RiAEYe8GWrXl8Zz9YC7dz7dnfGY1k/ma8/w18TC7txausbCCHCEWl52836+gdD1uQumCxPYPtkoWcZy8984kCE3whaV9O2PoaCd6Q3/Ww0WpRAvJDGDpa032t3vuTPSUbbU0N8iYfPua9a8z1JjbD4hDiIVMN797PllUDQEuIfNV+C06usGZxL01e/zBdMiIun1HhrVqHQ+p+GjHQXZ58kqP1EIGOg4/1HPib5i4umXULnVsCYZ0dvvdyrSiJxCnqMZZmtVSVm9rA+TYaiEoVf1RlchZPOKBFhF1gMGRsYh3LPL9cU4lbqt6DpZiLggsudD4NQIDAQAB',
+    'https://openapi.alipay.com/gateway.do',
+    'RSA2', 'JSON', 'UTF-8',
+    'ACTIVE', '0'
+);
+
+-- 3. pay_enterprise 加字段
+ALTER TABLE pay_enterprise ADD COLUMN IF NOT EXISTS service_provider_id BIGINT;
+ALTER TABLE pay_enterprise ADD COLUMN IF NOT EXISTS scope_label VARCHAR(64);
+
+-- 4. 旧企业全部绑定到第一个服务商
+UPDATE pay_enterprise SET service_provider_id = 1, scope_label = 'DOMESTIC' WHERE service_provider_id IS NULL;

+ 19 - 0
java/sql/003_service_provider_menu.sql

@@ -0,0 +1,19 @@
+WITH system_menu AS (
+    SELECT id FROM sys_menu WHERE name = '系统管理' AND type = 1 AND status = '0' LIMIT 1
+),
+inserted AS (
+    INSERT INTO sys_menu (name, type, "order", permission, icon, route_name, route_path, component_path,
+                          redirect, hidden, keep_alive, always_show, affix, title, params,
+                          uuid, parent_id, status, created_time, updated_time)
+    SELECT '服务商管理', 2, 99,
+           'module_system:service_provider:list',
+           'provider', 'ServiceProvider', '/service-provider', 'views/module_system/service_provider/index.vue',
+           null, false, true, false, false, '服务商管理', null,
+           gen_random_uuid(),
+           system_menu.id, '0', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
+    FROM system_menu
+    WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE name = '服务商管理' AND status = '0')
+    RETURNING id
+)
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT 1, id FROM inserted;

+ 125 - 55
java/src/main/java/com/payment/platform/core/alipay/AlipayClientFactory.java

@@ -4,8 +4,12 @@ import com.alipay.api.AlipayApiException;
 import com.alipay.api.AlipayClient;
 import com.alipay.api.AlipayConfig;
 import com.alipay.api.DefaultAlipayClient;
+import com.payment.platform.module.payment.enterprise.entity.EnterpriseEntity;
+import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
 import com.payment.platform.module.payment.openapi.entity.OpenConfEntity;
 import com.payment.platform.module.payment.openapi.mapper.OpenConfMapper;
+import com.payment.platform.module.payment.serviceprovider.entity.ServiceProviderEntity;
+import com.payment.platform.module.payment.serviceprovider.mapper.ServiceProviderMapper;
 import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -15,14 +19,12 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
 /**
- * 支付宝客户端工厂 — 对应 Python AlipayClient.get_client()
+ * 支付宝客户端工厂 — 支持多服务商
  *
- * 多租户支持: 默认使用 application.yml 中的配置;当传入 enterpriseId 时,
- * 从 open_conf 表查找该企业/租户的配置并创建/缓存专属 AlipayClient。
- *
- * Python SDK → Java SDK 映射:
- *   alipay.aop.api.DefaultAlipayClient  → com.alipay.api.DefaultAlipayClient
- *   alipay.aop.api.AlipayClientConfig    → com.alipay.api.AlipayConfig
+ * 解析优先级:
+ *   enterprise.serviceProviderId → 服务商配置
+ *   tenant.openConf → 租户专属配置(旧逻辑,兼容)
+ *   yml alipay → 默认 fallback
  */
 @Slf4j
 @Component
@@ -31,87 +33,155 @@ public class AlipayClientFactory {
 
     private final com.payment.platform.core.alipay.AlipayConfig paymentAlipayConfig;
     private final OpenConfMapper openConfMapper;
+    private final EnterpriseMapper enterpriseMapper;
+    private final ServiceProviderMapper serviceProviderMapper;
 
-    private final ConcurrentMap<String, AlipayClient> clients = new ConcurrentHashMap<>();
+    private final ConcurrentMap<Long, AlipayClient> providerClients = new ConcurrentHashMap<>();
+    private final ConcurrentMap<String, AlipayClient> tenantClients = new ConcurrentHashMap<>();
     private AlipayClient defaultClient;
 
     @PostConstruct
     public void init() {
-        if (!paymentAlipayConfig.isValid()) {
-            log.warn("支付宝配置不完整,跳过默认客户端初始化");
-            return;
+        if (paymentAlipayConfig.isValid()) {
+            AlipayConfig config = buildSdkConfig(
+                    paymentAlipayConfig.getAppId(),
+                    paymentAlipayConfig.getAppPrivateKey(),
+                    paymentAlipayConfig.getAlipayPublicKey(),
+                    paymentAlipayConfig.getServerUrl(),
+                    paymentAlipayConfig.getFormat(),
+                    paymentAlipayConfig.getCharset(),
+                    paymentAlipayConfig.getSignType());
+            try {
+                this.defaultClient = new DefaultAlipayClient(config);
+                log.info("支付宝默认客户端初始化成功, appId={}", paymentAlipayConfig.getAppId());
+            } catch (AlipayApiException e) {
+                log.error("支付宝默认客户端初始化失败", e);
+            }
+        } else {
+            log.warn("" +
+                    "支付宝默认配置不完整,将仅使用服务商/租户专属客户端");
         }
+    }
 
-        AlipayConfig config = new AlipayConfig();
-        config.setAppId(paymentAlipayConfig.getAppId());
-        config.setPrivateKey(paymentAlipayConfig.getAppPrivateKey());
-        config.setAlipayPublicKey(paymentAlipayConfig.getAlipayPublicKey());
-        config.setServerUrl(paymentAlipayConfig.getServerUrl());
-        config.setFormat(paymentAlipayConfig.getFormat());
-        config.setCharset(paymentAlipayConfig.getCharset());
-        config.setSignType(paymentAlipayConfig.getSignType());
+    // ==================== 按 enterpriseId 获取(主入口) ====================
 
-        try {
-            this.defaultClient = new DefaultAlipayClient(config);
-            log.info("支付宝默认客户端初始化成功, appId={}", paymentAlipayConfig.getAppId());
-        } catch (AlipayApiException e) {
-            log.error("支付宝默认客户端初始化失败", e);
+    public AlipayClient getClient(String enterpriseId) {
+        if (enterpriseId == null || enterpriseId.isBlank()) return getClient();
+        // 查企业 → 取 serviceProviderId
+        EnterpriseEntity ent = enterpriseMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EnterpriseEntity>()
+                        .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
+        if (ent != null && ent.getServiceProviderId() != null) {
+            return getClientByProvider(ent.getServiceProviderId());
         }
+        // 回退: 找不到企业或无服务商绑定,用默认
+        log.debug("企业[{}]未绑定服务商,使用默认客户端", enterpriseId);
+        return getClient();
+    }
+
+    // ==================== 按 providerId 获取 ====================
+
+    public AlipayClient getClientByProvider(Long providerId) {
+        if (providerId == null) return getClient();
+        return providerClients.computeIfAbsent(providerId, this::createClientForProvider);
     }
 
     /**
-     * 获取默认支付宝客户端(使用 application.yml 全局配置)
+     * 强制刷新指定服务商客户端(配置修改后调用
      */
-    public AlipayClient getClient() {
-        if (defaultClient == null) {
-            throw new IllegalStateException("支付宝默认客户端未初始化,请检查 alipay 配置");
+    public void refreshClient(Long providerId) {
+        if (providerId == null) return;
+        AlipayClient removed = providerClients.remove(providerId);
+        if (removed != null) {
+            log.info("服务商[{}]客户端缓存已清除", providerId);
         }
-        return defaultClient;
     }
 
-    /**
-     * 获取指定租户的支付宝客户端 — 从 open_conf 表读取配置,懒加载创建并缓存
-     *
-     * @param tenantId 租户ID,为 null 时退化为 getClient()
-     */
+    // ==================== 按 tenantId 获取(兼容旧逻辑) ====================
+
     public AlipayClient getClient(Long tenantId) {
         if (tenantId == null) return getClient();
         String cacheKey = "tenant:" + tenantId;
-        return clients.computeIfAbsent(cacheKey, k -> createClientForTenant(tenantId));
+        return tenantClients.computeIfAbsent(cacheKey, k -> createClientForTenant(tenantId));
+    }
+
+    // ==================== 默认客户端 ====================
+
+    public AlipayClient getClient() {
+        if (defaultClient != null) return defaultClient;
+        // 尝试取 DB 中第一个 ACTIVE 服务商
+        ServiceProviderEntity sp = serviceProviderMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ServiceProviderEntity>()
+                        .eq(ServiceProviderEntity::getProviderStatus, "ACTIVE")
+                        .orderByAsc(ServiceProviderEntity::getId)
+                        .last("LIMIT 1"));
+        if (sp != null) {
+            return getClientByProvider(sp.getId());
+        }
+        throw new IllegalStateException("无可用支付宝客户端:请配置服务商或检查 alipay 配置");
+    }
+
+    public boolean isReady() {
+        return defaultClient != null || !providerClients.isEmpty();
+    }
+
+    // ==================== 内部方法 ====================
+
+    private AlipayClient createClientForProvider(Long providerId) {
+        ServiceProviderEntity sp = serviceProviderMapper.selectById(providerId);
+        if (sp == null) {
+            log.warn("服务商[{}]不存在,回退默认", providerId);
+            return getClient();
+        }
+        if (!"ACTIVE".equals(sp.getProviderStatus())) {
+            log.warn("服务商[{}]已停用,回退默认", providerId);
+            return getClient();
+        }
+        AlipayConfig config = buildSdkConfig(
+                sp.getAppId(), sp.getAppPrivateKey(), sp.getAlipayPublicKey(),
+                sp.getServerUrl(), sp.getFormat(), sp.getCharset(), sp.getSignType());
+        try {
+            AlipayClient client = new DefaultAlipayClient(config);
+            log.info("服务商[{}]客户端创建成功, appId={}, name={}", providerId, sp.getAppId(), sp.getName());
+            return client;
+        } catch (AlipayApiException e) {
+            log.error("服务商[{}]客户端创建失败", providerId, e);
+            return getClient();
+        }
     }
 
-    /**
-     * 根据 open_conf 表创建租户专属客户端
-     */
     private AlipayClient createClientForTenant(Long tenantId) {
         OpenConfEntity conf = openConfMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OpenConfEntity>()
                         .eq(OpenConfEntity::getTenantId, tenantId));
         if (conf == null) {
-            log.warn("租户[{}]未配置开放平台信息,使用默认客户端", tenantId);
+            log.debug("租户[{}]无专属配置,使用默认", tenantId);
             return getClient();
         }
-
-        AlipayConfig config = new AlipayConfig();
-        config.setAppId(conf.getAppId());
-        config.setPrivateKey(conf.getPrivateKey());
-        config.setAlipayPublicKey(conf.getPublicKey());
-        config.setServerUrl(conf.getGatewayUrl() != null ? conf.getGatewayUrl() : paymentAlipayConfig.getServerUrl());
-        config.setFormat(conf.getFormat() != null ? conf.getFormat() : paymentAlipayConfig.getFormat());
-        config.setCharset(conf.getCharset() != null ? conf.getCharset() : paymentAlipayConfig.getCharset());
-        config.setSignType(conf.getSignType() != null ? conf.getSignType() : paymentAlipayConfig.getSignType());
-
+        AlipayConfig config = buildSdkConfig(
+                conf.getAppId(), conf.getPrivateKey(), conf.getPublicKey(),
+                conf.getGatewayUrl() != null ? conf.getGatewayUrl() : paymentAlipayConfig.getServerUrl(),
+                conf.getFormat() != null ? conf.getFormat() : paymentAlipayConfig.getFormat(),
+                conf.getCharset() != null ? conf.getCharset() : paymentAlipayConfig.getCharset(),
+                conf.getSignType() != null ? conf.getSignType() : paymentAlipayConfig.getSignType());
         try {
-            AlipayClient client = new DefaultAlipayClient(config);
-            log.info("租户[{}]支付宝客户端创建成功, appId={}", tenantId, conf.getAppId());
-            return client;
+            return new DefaultAlipayClient(config);
         } catch (AlipayApiException e) {
-            log.error("租户[{}]支付宝客户端创建失败,回退默认客户端", tenantId, e);
+            log.error("租户[{}]客户端创建失败", tenantId, e);
             return getClient();
         }
     }
 
-    public boolean isReady() {
-        return defaultClient != null;
+    private AlipayConfig buildSdkConfig(String appId, String privateKey, String alipayPublicKey,
+                                         String serverUrl, String format, String charset, String signType) {
+        AlipayConfig config = new AlipayConfig();
+        config.setAppId(appId);
+        config.setPrivateKey(privateKey);
+        config.setAlipayPublicKey(alipayPublicKey);
+        config.setServerUrl(serverUrl != null ? serverUrl : "https://openapi.alipay.com/gateway.do");
+        config.setFormat(format != null ? format : "JSON");
+        config.setCharset(charset != null ? charset : "UTF-8");
+        config.setSignType(signType != null ? signType : "RSA2");
+        return config;
     }
 }

+ 38 - 0
java/src/main/java/com/payment/platform/core/scheduler/ScheduledTasks.java

@@ -0,0 +1,38 @@
+package com.payment.platform.core.scheduler;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * Spring @Scheduled 定时任务 — 替代 Quartz/APScheduler
+ *
+ * 格式: fixedRate(毫秒) / fixedDelay(毫秒) / cron 表达式
+ * cron: "秒 分 时 日 月 周"
+ */
+@Slf4j
+@Component
+@EnableScheduling
+@RequiredArgsConstructor
+public class ScheduledTasks {
+
+    // ==================== 定时任务示例 ====================
+
+    /** 每分钟 — 心跳 */
+    @Scheduled(fixedRate = 60_000)
+    public void heartbeat() {
+        log.debug("[定时任务] heartbeat");
+    }
+
+    // ==================== 业务任务(按需添加) ====================
+
+    // 示例: 每天凌晨 2 点清理过期 Token
+    // @Scheduled(cron = "0 0 2 * * ?")
+    // public void cleanExpiredTokens() { ... }
+
+    // 示例: 每 5 分钟同步支付状态
+    // @Scheduled(fixedDelay = 300_000)
+    // public void syncPaymentStatus() { ... }
+}

+ 23 - 0
java/src/main/java/com/payment/platform/core/sms/AliyunSmsConfig.java

@@ -0,0 +1,23 @@
+package com.payment.platform.core.sms;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 阿里云短信配置 — 对应 Python AliyunConfig
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "alibaba-cloud")
+public class AliyunSmsConfig {
+
+    private String accessKeyId;
+    private String accessKeySecret;
+
+    /** 短信签名 */
+    private String signName = "湖南钱程似锦技术服务";
+
+    /** endpoint */
+    private String endpoint = "dysmsapi.aliyuncs.com";
+}

+ 71 - 0
java/src/main/java/com/payment/platform/core/sms/AliyunSmsSender.java

@@ -0,0 +1,71 @@
+package com.payment.platform.core.sms;
+
+import com.aliyun.dysmsapi20170525.Client;
+import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
+import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
+import com.aliyun.teaopenapi.models.Config;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 阿里云短信发送器 — 对应 Python SmsSender
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AliyunSmsSender {
+
+    private final AliyunSmsConfig smsConfig;
+
+    private volatile Client client;
+
+    private Client getClient() throws Exception {
+        if (client == null) {
+            synchronized (this) {
+                if (client == null) {
+                    Config config = new Config()
+                            .setAccessKeyId(smsConfig.getAccessKeyId())
+                            .setAccessKeySecret(smsConfig.getAccessKeySecret())
+                            .setEndpoint(smsConfig.getEndpoint());
+                    client = new Client(config);
+                }
+            }
+        }
+        return client;
+    }
+
+    /**
+     * 发送短信验证码 — 对应 Python send_sms
+     *
+     * @param phoneNumbers 手机号(多个用逗号分隔)
+     * @param templateCode 短信模板 Code
+     * @param templateParam 模板变量 JSON 字符串,如 {"code":"123456"}
+     * @return true 发送成功
+     */
+    public boolean send(String phoneNumbers, String templateCode, String templateParam) {
+        try {
+            SendSmsRequest request = new SendSmsRequest()
+                    .setPhoneNumbers(phoneNumbers)
+                    .setSignName(smsConfig.getSignName())
+                    .setTemplateCode(templateCode)
+                    .setTemplateParam(templateParam);
+
+            SendSmsResponse response = getClient().sendSms(request);
+
+            if ("OK".equals(response.getBody().getCode())) {
+                log.info("短信发送成功: phone={}, bizId={}, requestId={}",
+                        phoneNumbers, response.getBody().getBizId(), response.getBody().getRequestId());
+                return true;
+            }
+
+            log.warn("短信发送失败: phone={}, code={}, message={}",
+                    phoneNumbers, response.getBody().getCode(), response.getBody().getMessage());
+            return false;
+
+        } catch (Exception e) {
+            log.error("短信发送异常: phone={}, error={}", phoneNumbers, e.getMessage(), e);
+            return false;
+        }
+    }
+}

+ 3 - 1
java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java

@@ -35,7 +35,9 @@ public class TenantInnerInterceptor extends TenantLineInnerInterceptor {
             "sys_log",            // 操作日志
             "sys_role_menus",     // 角色菜单关联表(无 tenant_id 列)
             "sys_user_roles",     // 用户角色关联表(无 tenant_id 列)
-            "sys_user_social"     // 用户第三方登录(无 tenant_id 列)
+            "sys_user_positions", // 用户岗位关联表(无 tenant_id 列)
+            "sys_user_social",    // 用户第三方登录(无 tenant_id 列)
+            "pay_service_provider" // 服务商配置(无 tenant_id 列)
     );
 
     public TenantInnerInterceptor() {

+ 3 - 3
java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java

@@ -156,7 +156,7 @@ public class AccountService {
             request.setBizModel(model);
 
             AlipayCommerceEcConsumeDetailQueryResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
             if (response == null) throw new BusinessException(400, "账单详情查询失败: 无响应");
             if (!response.isSuccess()) {
                 log.error("支付宝账单详情查询失败: code={}, msg={}, subMsg={}",
@@ -322,7 +322,7 @@ public class AccountService {
             var request = new com.alipay.api.request.AlipayCommerceEcTransReceiptApplyRequest();
             request.setBizModel(model);
 
-            var response = alipayClientFactory.getClient().execute(request);
+            var response = alipayClientFactory.getClient(enterpriseId).execute(request);
             if (response == null) throw new BusinessException(400, "申请回单失败: 无响应");
             if (!response.isSuccess()) {
                 redisTemplate.delete(cacheKey);
@@ -354,7 +354,7 @@ public class AccountService {
             var request = new com.alipay.api.request.AlipayCommerceEcTransReceiptQueryRequest();
             request.setBizModel(model);
 
-            var response = alipayClientFactory.getClient().execute(request);
+            var response = alipayClientFactory.getClient(enterpriseId).execute(request);
             if (response == null) throw new BusinessException(400, "查询回单失败: 无响应");
             if (!response.isSuccess())
                 throw new BusinessException(400, "查询回单失败: " +

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

@@ -97,7 +97,7 @@ public class AlipayTransferService {
             request.setBizModel(model);
 
             AlipayCommerceEcTransAuthorizeApplyResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "申请转账授权失败: " + response.getMsg());
@@ -121,7 +121,7 @@ public class AlipayTransferService {
             request.setBizModel(model);
 
             AlipayCommerceEcTransAccountCreateResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "开通资金专户失败: " + response.getMsg());
@@ -156,7 +156,7 @@ public class AlipayTransferService {
             request.setBizModel(model);
 
             AlipayCommerceEcTransAccountDepositResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "充值失败: " + response.getMsg());
@@ -254,7 +254,7 @@ public class AlipayTransferService {
             request.setBizModel(model);
 
             AlipayCommerceEcTransAccountTransferResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             String subCode = response.getSubCode() != null ? response.getSubCode() : "";
             String subMsg = response.getSubMsg() != null ? response.getSubMsg() : "";
@@ -352,7 +352,7 @@ public class AlipayTransferService {
             request.setBizModel(model);
 
             AlipayCommerceEcTransAccountWithdrawResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "提现失败: " + response.getMsg());
@@ -387,7 +387,7 @@ public class AlipayTransferService {
             request.setBizModel(model);
 
             AlipayCommerceEcTransAccountQueryResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "查询资金专户失败: " + response.getMsg());
@@ -625,7 +625,7 @@ public class AlipayTransferService {
             Object request = requestClass.getDeclaredConstructor().newInstance();
             requestClass.getMethod("setBizModel", modelClass).invoke(request, model);
 
-            Object response = alipayClientFactory.getClient().execute(
+            Object response = alipayClientFactory.getClient(enterpriseId).execute(
                     (com.alipay.api.AlipayRequest) request);
 
             if (response == null) return null;

+ 3 - 3
java/src/main/java/com/payment/platform/module/payment/department/service/AlipayDepartmentService.java

@@ -41,7 +41,7 @@ public class AlipayDepartmentService {
             AlipayCommerceEcDepartmentCreateRequest request = new AlipayCommerceEcDepartmentCreateRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcDepartmentCreateResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcDepartmentCreateResponse response = alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400,
@@ -76,7 +76,7 @@ public class AlipayDepartmentService {
             AlipayCommerceEcDepartmentInfoModifyRequest request = new AlipayCommerceEcDepartmentInfoModifyRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcDepartmentInfoModifyResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcDepartmentInfoModifyResponse response = alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400,
@@ -99,7 +99,7 @@ public class AlipayDepartmentService {
             AlipayCommerceEcDepartmentDeleteRequest request = new AlipayCommerceEcDepartmentDeleteRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcDepartmentDeleteResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcDepartmentDeleteResponse response = alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400,

+ 26 - 0
java/src/main/java/com/payment/platform/module/payment/employee/controller/EmployeeController.java

@@ -163,6 +163,32 @@ public class EmployeeController {
         return Result.ok(alipayEmployeeService.addEmployee(b));
     }
 
+    @PutMapping("/{employee_id}")
+    public Result<Map<String, Object>> update(@PathVariable(name = "employee_id") String employeeId,
+            @RequestBody Map<String, Object> body) {
+        EmployeeEntity emp = employeeMapper.selectOne(
+                new LambdaQueryWrapper<EmployeeEntity>().eq(EmployeeEntity::getEmployeeId, employeeId));
+        if (emp == null) throw new com.payment.platform.common.exception.BusinessException(404, "员工不存在");
+
+        // 1. 调用支付宝 SDK 修改员工信息
+        alipayEmployeeService.modifyEmployee(emp.getEnterpriseId(), employeeId, body);
+
+        // 2. 更新本地记录
+        if (body.containsKey("employee_name")) emp.setEmployeeName((String) body.get("employee_name"));
+        if (body.containsKey("employee_no")) emp.setEmployeeNo((String) body.get("employee_no"));
+        if (body.containsKey("employee_mobile")) emp.setEmployeeMobile((String) body.get("employee_mobile"));
+        if (body.containsKey("employee_email")) emp.setEmployeeEmail((String) body.get("employee_email"));
+        if (body.containsKey("department_ids")) {
+            try { emp.setDepartmentIds(objectMapper.writeValueAsString(body.get("department_ids"))); }
+            catch (JsonProcessingException ignored) {}
+        }
+        employeeMapper.updateById(emp);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("employee_id", emp.getEmployeeId());
+        result.put("employee_name", emp.getEmployeeName());
+        return Result.ok(result);
+    }
+
     @DeleteMapping("/{employee_id}")
     public Result<Map<String, String>> delete(@PathVariable(name = "employee_id") String employeeId,
             @RequestParam(name = "enterprise_id") String enterpriseId) {

+ 28 - 4
java/src/main/java/com/payment/platform/module/payment/employee/service/AlipayEmployeeService.java

@@ -64,7 +64,7 @@ public class AlipayEmployeeService {
             AlipayCommerceEcEmployeeAddRequest request = new AlipayCommerceEcEmployeeAddRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcEmployeeAddResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcEmployeeAddResponse response = alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "添加员工失败: " + (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
@@ -141,7 +141,7 @@ public class AlipayEmployeeService {
             AlipayCommerceEcEmployeeInfoQueryRequest request = new AlipayCommerceEcEmployeeInfoQueryRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcEmployeeInfoQueryResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcEmployeeInfoQueryResponse response = alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "查询员工详情失败: " + response.getMsg());
@@ -207,7 +207,7 @@ public class AlipayEmployeeService {
             AlipayCommerceEcEmployeeInviteQueryRequest request = new AlipayCommerceEcEmployeeInviteQueryRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcEmployeeInviteQueryResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcEmployeeInviteQueryResponse response = alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "获取签约链接失败: " + response.getSubMsg());
@@ -223,6 +223,30 @@ public class AlipayEmployeeService {
         }
     }
 
+    /** 对应 Python modify_employee_service — 修改员工信息(调支付宝API) */
+    public void modifyEmployee(String enterpriseId, String employeeId, Map<String, Object> data) {
+        try {
+            AlipayCommerceEcEmployeeInfoModifyModel model = new AlipayCommerceEcEmployeeInfoModifyModel();
+            model.setEnterpriseId(enterpriseId);
+            model.setEmployeeId(employeeId);
+            if (data.containsKey("employee_name")) model.setEmployeeName((String) data.get("employee_name"));
+            if (data.containsKey("employee_no")) model.setEmployeeNo((String) data.get("employee_no"));
+            if (data.containsKey("employee_mobile")) model.setEmployeeMobile((String) data.get("employee_mobile"));
+            if (data.containsKey("employee_email")) model.setEmployeeEmail((String) data.get("employee_email"));
+
+            AlipayCommerceEcEmployeeInfoModifyRequest request = new AlipayCommerceEcEmployeeInfoModifyRequest();
+            request.setBizModel(model);
+            AlipayCommerceEcEmployeeInfoModifyResponse response =
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
+            if (!response.isSuccess()) {
+                throw new BusinessException(400, "修改员工信息失败: " + response.getMsg());
+            }
+            log.info("支付宝员工信息修改成功: employee_id={}, enterprise_id={}", employeeId, enterpriseId);
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "修改员工信息失败: " + e.getMessage());
+        }
+    }
+
     /** 对应 Python delete_employee_service — 删除员工(调支付宝+解约联动+删用户+删员工记录) */
     @Transactional
     public Map<String, String> deleteEmployee(String enterpriseId, String employeeId) {
@@ -234,7 +258,7 @@ public class AlipayEmployeeService {
             AlipayCommerceEcEmployeeDeleteRequest request = new AlipayCommerceEcEmployeeDeleteRequest();
             request.setBizModel(model);
 
-            AlipayCommerceEcEmployeeDeleteResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayCommerceEcEmployeeDeleteResponse response = alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess())
                 throw new BusinessException(400, "删除员工失败: " + (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));

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

@@ -20,6 +20,12 @@ public class EnterpriseCreateDTO {
     @Schema(description = "状态")
     private String status;
 
+    @Schema(description = "服务商ID")
+    private Long serviceProviderId;
+
+    @Schema(description = "业务范围标签")
+    private String scopeLabel;
+
     @Schema(description = "描述")
     private String description;
 }

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

@@ -66,6 +66,12 @@ public class EnterpriseVO {
     @Schema(description = "状态")
     private String status;
 
+    @Schema(description = "服务商ID")
+    private Long serviceProviderId;
+
+    @Schema(description = "业务范围")
+    private String scopeLabel;
+
     @Schema(description = "创建时间")
     private OffsetDateTime createdTime;
 

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

@@ -30,6 +30,8 @@ public class EnterpriseEntity extends PaymentTenantBaseEntity {
     private String baseInfo;
     private String profiles;
     private String remark;
+    private Long serviceProviderId;
+    private String scopeLabel;
 
     @JsonRawValue
     public String getBaseInfo() { return baseInfo; }

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

@@ -38,17 +38,33 @@ public class AlipayEnterpriseService {
             request.setBizModel(model);
 
             AlipayCommerceEcEnterpriseRegisterinviteCreateResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 log.error("支付宝邀请码申请失败: {} - {}", response.getCode(), response.getMsg());
                 throw new BusinessException(400, "申请邀请码失败: " + response.getMsg());
             }
 
+            // 保存入驻预登记(含服务商绑定)
+            Long serviceProviderId = data.get("service_provider_id") instanceof Number
+                    ? ((Number) data.get("service_provider_id")).longValue() : null;
+            String scopeLabel = (String) data.get("scope_label");
+            EnterpriseEntity pending = new EnterpriseEntity();
+            pending.setOutBizNo(model.getOutBizNo());
+            pending.setIdentityType((String) data.get("identity_type"));
+            pending.setIdentity((String) data.get("identity"));
+            pending.setIdentityOpenId((String) data.get("identity_open_id"));
+            pending.setRegisterMode("INVITE");
+            pending.setStatus("PENDING");
+            pending.setServiceProviderId(serviceProviderId);
+            pending.setScopeLabel(scopeLabel);
+            enterpriseMapper.insert(pending);
+
             Map<String, Object> result = new LinkedHashMap<>();
             result.put("pc_invite_url", response.getPcInviteUrl() != null ? response.getPcInviteUrl() : "");
             result.put("invite_time", new java.util.Date());
             result.put("expire_time", response.getExpireTime());
+            result.put("out_biz_no", model.getOutBizNo());
             return result;
         } catch (AlipayApiException e) {
             throw new BusinessException(400, "申请邀请码失败: " + e.getMessage());
@@ -65,7 +81,7 @@ public class AlipayEnterpriseService {
             request.setBizModel(model);
 
             AlipayCommerceEcEnterpriseInfoQueryResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 log.error("支付宝企业信息查询失败: {} - {}", response.getCode(), response.getMsg());
@@ -104,7 +120,7 @@ public class AlipayEnterpriseService {
             request.setBizModel(model);
 
             AlipayCommerceEcEnterpriseUnsignResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 log.error("支付宝企业解约失败: {} - {}", response.getCode(), response.getMsg());
@@ -137,7 +153,7 @@ public class AlipayEnterpriseService {
             request.setBizModel(model);
 
             AlipayCommerceEcEnterpriseDeleteResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 log.error("支付宝企业注销失败: {} - {}", response.getCode(), response.getMsg());

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

@@ -73,6 +73,8 @@ public class EnterpriseService {
         e.setSignFundWay(dto.getSignFundWay());
         e.setStatus(dto.getStatus());
         e.setDescription(dto.getDescription());
+        e.setServiceProviderId(dto.getServiceProviderId());
+        e.setScopeLabel(dto.getScopeLabel());
         enterpriseMapper.insert(e);
         return toVO(enterpriseMapper.selectById(e.getId()));
     }
@@ -118,6 +120,8 @@ public class EnterpriseService {
         vo.setShortName(e.getShortName());
         vo.setStatus(e.getStatus());
         vo.setCreatedTime(e.getCreatedTime());
+        vo.setServiceProviderId(e.getServiceProviderId());
+        vo.setScopeLabel(e.getScopeLabel());
         return vo;
     }
 
@@ -145,6 +149,8 @@ public class EnterpriseService {
         vo.setRemark(e.getRemark());
         vo.setCreatedTime(e.getCreatedTime());
         vo.setUpdatedTime(e.getUpdatedTime());
+        vo.setServiceProviderId(e.getServiceProviderId());
+        vo.setScopeLabel(e.getScopeLabel());
         return vo;
     }
 }

+ 2 - 2
java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionScopeSyncService.java

@@ -150,7 +150,7 @@ public class InstitutionScopeSyncService {
                             new AlipayEbppInvoiceInstitutionScopeModifyRequest();
                     scopeRequest.setBizModel(scopeModel);
                     AlipayEbppInvoiceInstitutionScopeModifyResponse scopeResponse =
-                            alipayClientFactory.getClient().execute(scopeRequest);
+                            alipayClientFactory.getClient(enterpriseId).execute(scopeRequest);
                     if (!scopeResponse.isSuccess()) {
                         log.warn("scope.modify 移除部门失败: {}",
                                 scopeResponse.getSubMsg() != null ? scopeResponse.getSubMsg() : scopeResponse.getMsg());
@@ -295,7 +295,7 @@ public class InstitutionScopeSyncService {
                                 new AlipayEbppInvoiceInstitutionScopeModifyRequest();
                         scopeRequest.setBizModel(scopeModel);
                         AlipayEbppInvoiceInstitutionScopeModifyResponse scopeResponse =
-                                alipayClientFactory.getClient().execute(scopeRequest);
+                                alipayClientFactory.getClient(enterpriseId).execute(scopeRequest);
                         if (!scopeResponse.isSuccess()) {
                             log.warn("scope.modify 移除员工失败: {}",
                                     scopeResponse.getSubMsg() != null ? scopeResponse.getSubMsg() : scopeResponse.getMsg());

+ 13 - 13
java/src/main/java/com/payment/platform/module/payment/expense/institution/service/InstitutionService.java

@@ -101,7 +101,7 @@ public class InstitutionService {
                         new AlipayEbppInvoiceInstitutionDetailinfoQueryRequest();
                 queryRequest.setBizModel(queryModel);
                 AlipayEbppInvoiceInstitutionDetailinfoQueryResponse queryResponse =
-                        alipayClientFactory.getClient().execute(queryRequest);
+                        alipayClientFactory.getClient(enterpriseId).execute(queryRequest);
                 if (queryResponse.isSuccess()) {
                     InstitutionVO vo = new InstitutionVO();
                     vo.setInstitutionId(queryResponse.getInstitutionId());
@@ -355,7 +355,7 @@ public class InstitutionService {
         Map<String, String> standardIdMapping = new java.util.HashMap<>();
         try {
             AlipayEbppInvoiceInstitutionCreateResponse createResponse =
-                    alipayClientFactory.getClient().execute(createRequest);
+                    alipayClientFactory.getClient(enterpriseId).execute(createRequest);
             if (!createResponse.isSuccess()) {
                 throw new BusinessException(400, "创建费控制度失败: " +
                         (createResponse.getSubMsg() != null ? createResponse.getSubMsg() : createResponse.getMsg()));
@@ -399,7 +399,7 @@ public class InstitutionService {
                         new AlipayEbppInvoiceInstitutionScopeModifyRequest();
                 scopeReq.setBizModel(scopeModel);
                 AlipayEbppInvoiceInstitutionScopeModifyResponse scopeResp =
-                        alipayClientFactory.getClient().execute(scopeReq);
+                        alipayClientFactory.getClient(enterpriseId).execute(scopeReq);
                 if (!scopeResp.isSuccess()) {
                     throw new BusinessException(400, "设置适用范围失败: " +
                             (scopeResp.getSubMsg() != null ? scopeResp.getSubMsg() : scopeResp.getMsg()));
@@ -416,7 +416,7 @@ public class InstitutionService {
                 AlipayEbppInvoiceInstitutionDeleteRequest rollbackReq =
                         new AlipayEbppInvoiceInstitutionDeleteRequest();
                 rollbackReq.setBizModel(rollbackModel);
-                alipayClientFactory.getClient().execute(rollbackReq);
+                alipayClientFactory.getClient(enterpriseId).execute(rollbackReq);
             } catch (Exception ignored) {
                 log.warn("回滚删除支付宝制度失败: institutionId={}", institutionId);
             }
@@ -460,7 +460,7 @@ public class InstitutionService {
                         new AlipayEbppInvoiceIssueruleCreateRequest();
                 irReq.setBizModel(irModel);
                 AlipayEbppInvoiceIssueruleCreateResponse irResp =
-                        alipayClientFactory.getClient().execute(irReq);
+                        alipayClientFactory.getClient(enterpriseId).execute(irReq);
                 if (!irResp.isSuccess()) {
                     throw new BusinessException(400, "创建发放规则失败: " +
                             (irResp.getSubMsg() != null ? irResp.getSubMsg() : irResp.getMsg()));
@@ -478,7 +478,7 @@ public class InstitutionService {
                     AlipayEbppInvoiceInstitutionDeleteRequest rollbackReq =
                             new AlipayEbppInvoiceInstitutionDeleteRequest();
                     rollbackReq.setBizModel(rollbackModel);
-                    alipayClientFactory.getClient().execute(rollbackReq);
+                    alipayClientFactory.getClient(enterpriseId).execute(rollbackReq);
                 } catch (Exception ignored) {
                     log.warn("回滚删除支付宝制度失败: institutionId={}", institutionId);
                 }
@@ -714,7 +714,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceInstitutionModifyRequest();
             modifyRequest.setBizModel(modifyModel);
             AlipayEbppInvoiceInstitutionModifyResponse modifyResponse =
-                    alipayClientFactory.getClient().execute(modifyRequest);
+                    alipayClientFactory.getClient(existing.getEnterpriseId()).execute(modifyRequest);
             if (!modifyResponse.isSuccess()) {
                 log.warn("支付宝 institution.modify 失败: {}",
                         modifyResponse.getSubMsg() != null ? modifyResponse.getSubMsg() : modifyResponse.getMsg());
@@ -785,7 +785,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceInstitutionDeleteRequest();
             deleteRequest.setBizModel(deleteModel);
             AlipayEbppInvoiceInstitutionDeleteResponse deleteResponse =
-                    alipayClientFactory.getClient().execute(deleteRequest);
+                    alipayClientFactory.getClient(enterpriseId).execute(deleteRequest);
             if (!deleteResponse.isSuccess()) {
                 log.warn("支付宝删除失败(可能已删): {}",
                         deleteResponse.getSubMsg() != null ? deleteResponse.getSubMsg() : deleteResponse.getMsg());
@@ -851,7 +851,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceInstitutionScopepageinfoQueryRequest();
             queryRequest.setBizModel(queryModel);
             AlipayEbppInvoiceInstitutionScopepageinfoQueryResponse queryResponse =
-                    alipayClientFactory.getClient().execute(queryRequest);
+                    alipayClientFactory.getClient(enterpriseId).execute(queryRequest);
             if (!queryResponse.isSuccess()) {
                 throw new BusinessException(400, "查询适用范围失败: " +
                         (queryResponse.getSubMsg() != null ? queryResponse.getSubMsg() : queryResponse.getMsg()));
@@ -1045,7 +1045,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceInstitutionScopeModifyRequest();
             scopeRequest.setBizModel(scopeModel);
             AlipayEbppInvoiceInstitutionScopeModifyResponse scopeResponse =
-                    alipayClientFactory.getClient().execute(scopeRequest);
+                    alipayClientFactory.getClient(enterpriseId).execute(scopeRequest);
             if (!scopeResponse.isSuccess()) {
                 throw new BusinessException(400, "设置适用范围失败: " +
                         (scopeResponse.getSubMsg() != null ? scopeResponse.getSubMsg() : scopeResponse.getMsg()));
@@ -1149,7 +1149,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceIssueruleCreateRequest();
             request.setBizModel(model);
             AlipayEbppInvoiceIssueruleCreateResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) data.get("enterprise_id")).execute(request);
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "创建发放规则失败: " +
                         (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
@@ -1196,7 +1196,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceIssueruleDeleteRequest();
             request.setBizModel(model);
             AlipayEbppInvoiceIssueruleDeleteResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "删除发放规则失败: " +
                         (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
@@ -1270,7 +1270,7 @@ public class InstitutionService {
                     new AlipayEbppInvoiceIssueruleModifyRequest();
             request.setBizModel(model);
             AlipayEbppInvoiceIssueruleModifyResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) data.get("enterprise_id")).execute(request);
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "修改发放规则失败: " +
                         (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));

+ 2 - 2
java/src/main/java/com/payment/platform/module/payment/expense/quota/service/IssueBatchService.java

@@ -138,7 +138,7 @@ public class IssueBatchService {
             AlipayEbppInvoiceExpensecontrolIssuebatchCreateRequest request = new AlipayEbppInvoiceExpensecontrolIssuebatchCreateRequest();
             request.setBizModel(model);
 
-            AlipayEbppInvoiceExpensecontrolIssuebatchCreateResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayEbppInvoiceExpensecontrolIssuebatchCreateResponse response = alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "批量发放失败: " + (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));
@@ -175,7 +175,7 @@ public class IssueBatchService {
             AlipayEbppInvoiceExpensecontrolIssuebatchCancelRequest request = new AlipayEbppInvoiceExpensecontrolIssuebatchCancelRequest();
             request.setBizModel(model);
 
-            AlipayEbppInvoiceExpensecontrolIssuebatchCancelResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayEbppInvoiceExpensecontrolIssuebatchCancelResponse response = alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "作废批次失败: " + (response.getSubMsg() != null ? response.getSubMsg() : response.getMsg()));

+ 8 - 8
java/src/main/java/com/payment/platform/module/payment/expense/quota/service/QuotaService.java

@@ -141,7 +141,7 @@ public class QuotaService {
                 request.setBizModel(model);
 
                 AlipayEbppInvoiceExpensecontrolQuotaModifyResponse response =
-                        alipayClientFactory.getClient().execute(request);
+                        alipayClientFactory.getClient((String) null).execute(request);
 
                 if (response == null) {
                     throw new BusinessException(400, "支付宝更新额度失败: 无响应");
@@ -268,7 +268,7 @@ public class QuotaService {
                 request.setBizModel(model);
 
                 AlipayEbppInvoiceExpensecontrolQuotaModifyResponse response =
-                        alipayClientFactory.getClient().execute(request);
+                        alipayClientFactory.getClient((String) null).execute(request);
 
                 if (!response.isSuccess()) {
                     String subMsg = response.getSubMsg() != null ? response.getSubMsg() : response.getMsg();
@@ -375,7 +375,7 @@ public class QuotaService {
             AlipayEbppInvoiceExpensecontrolQuotaCreateRequest request = new AlipayEbppInvoiceExpensecontrolQuotaCreateRequest();
             request.setBizModel(model);
 
-            AlipayEbppInvoiceExpensecontrolQuotaCreateResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayEbppInvoiceExpensecontrolQuotaCreateResponse response = alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "支付宝创建费用报销失败: "
@@ -407,7 +407,7 @@ public class QuotaService {
             AlipayEbppInvoiceExpensecontrolQuotaQueryRequest request = new AlipayEbppInvoiceExpensecontrolQuotaQueryRequest();
             request.setBizModel(model);
 
-            AlipayEbppInvoiceExpensecontrolQuotaQueryResponse response = alipayClientFactory.getClient().execute(request);
+            AlipayEbppInvoiceExpensecontrolQuotaQueryResponse response = alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "支付宝查询费用报销失败: "
@@ -488,7 +488,7 @@ public class QuotaService {
             request.setBizModel(model);
 
             AlipayEbppInvoiceExpensecontrolQuotaModifyResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) null).execute(request);
 
             if (response == null) {
                 throw new BusinessException(400, "修改额度失败: 支付宝无响应");
@@ -538,9 +538,9 @@ public class QuotaService {
             Object request = deleteRequestClass.getDeclaredConstructor().newInstance();
             deleteRequestClass.getMethod("setBizModel", Object.class).invoke(request, model);
 
-            Object response = alipayClientFactory.getClient().getClass()
+            Object response = alipayClientFactory.getClient((String) null).getClass()
                     .getMethod("execute", com.alipay.api.AlipayRequest.class)
-                    .invoke(alipayClientFactory.getClient(), request);
+                    .invoke(alipayClientFactory.getClient((String) null), request);
             if (response != null) {
                 java.lang.reflect.Method isSuccessMethod = response.getClass().getMethod("isSuccess");
                 if (Boolean.TRUE.equals(isSuccessMethod.invoke(response))) {
@@ -640,7 +640,7 @@ public class QuotaService {
             request.setBizModel(model);
 
             AlipayEbppInvoiceExpensecomsueOutsourceNotifyResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient(enterpriseId).execute(request);
 
             if (!response.isSuccess()) {
                 log.error("外部消费额度同步失败: code={}, msg={}, subCode={}, subMsg={}",

+ 3 - 3
java/src/main/java/com/payment/platform/module/payment/expense/rule/service/RuleService.java

@@ -168,7 +168,7 @@ public class RuleService {
             request.setBizModel(model);
 
             AlipayEbppInvoiceInstitutionExpenseruleCreateResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "创建使用规则失败: " +
@@ -219,7 +219,7 @@ public class RuleService {
             request.setBizModel(model);
 
             AlipayEbppInvoiceInstitutionExpenseruleModifyResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "修改使用规则失败: " +
@@ -265,7 +265,7 @@ public class RuleService {
             request.setBizModel(model);
 
             AlipayEbppInvoiceInstitutionExpenseruleDeleteResponse response =
-                    alipayClientFactory.getClient().execute(request);
+                    alipayClientFactory.getClient((String) null).execute(request);
 
             if (!response.isSuccess()) {
                 throw new BusinessException(400, "删除使用规则失败: " +

+ 4 - 4
java/src/main/java/com/payment/platform/module/payment/facetoface/service/FacetofaceService.java

@@ -120,7 +120,7 @@ public class FacetofaceService {
             AlipayOpenAgentCreateRequest createRequest = new AlipayOpenAgentCreateRequest();
             createRequest.setBizModel(createModel);
 
-            AlipayOpenAgentCreateResponse createResponse = alipayClientFactory.getClient().execute(createRequest);
+            AlipayOpenAgentCreateResponse createResponse = alipayClientFactory.getClient(dto.getEnterpriseId()).execute(createRequest);
             if (!createResponse.isSuccess())
                 throw new BusinessException(400, "创建应用事务失败: " +
                         (createResponse.getSubMsg() != null ? createResponse.getSubMsg() : createResponse.getMsg()));
@@ -141,7 +141,7 @@ public class FacetofaceService {
                 signRequest.setRate(dto.getRate());
             }
 
-            AlipayOpenAgentFacetofaceSignResponse signResponse = alipayClientFactory.getClient().execute(signRequest);
+            AlipayOpenAgentFacetofaceSignResponse signResponse = alipayClientFactory.getClient(dto.getEnterpriseId()).execute(signRequest);
             if (!signResponse.isSuccess())
                 throw new BusinessException(400, "提交当面付签约失败: " +
                         (signResponse.getSubMsg() != null ? signResponse.getSubMsg() : signResponse.getMsg()));
@@ -154,7 +154,7 @@ public class FacetofaceService {
             AlipayOpenAgentConfirmRequest confirmRequest = new AlipayOpenAgentConfirmRequest();
             confirmRequest.setBizModel(confirmModel);
 
-            AlipayOpenAgentConfirmResponse confirmResponse = alipayClientFactory.getClient().execute(confirmRequest);
+            AlipayOpenAgentConfirmResponse confirmResponse = alipayClientFactory.getClient(dto.getEnterpriseId()).execute(confirmRequest);
             if (!confirmResponse.isSuccess())
                 throw new BusinessException(400, "确认提交事务失败: " +
                         (confirmResponse.getSubMsg() != null ? confirmResponse.getSubMsg() : confirmResponse.getMsg()));
@@ -229,7 +229,7 @@ public class FacetofaceService {
             AlipayOpenAgentOrderQueryRequest queryRequest = new AlipayOpenAgentOrderQueryRequest();
             queryRequest.setBizModel(queryModel);
 
-            AlipayOpenAgentOrderQueryResponse queryResponse = alipayClientFactory.getClient().execute(queryRequest);
+            AlipayOpenAgentOrderQueryResponse queryResponse = alipayClientFactory.getClient(e.getEnterpriseId()).execute(queryRequest);
 
             if (queryResponse.isSuccess()) {
                 OffsetDateTime now = OffsetDateTime.now();

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/notification/handler/BillHandler.java

@@ -243,7 +243,7 @@ public class BillHandler extends BaseNotifyHandler {
 
         AlipayCommerceEcConsumeDetailQueryResponse response;
         try {
-            response = alipayClientFactory.getClient().execute(request);
+            response = alipayClientFactory.getClient(enterpriseId).execute(request);
         } catch (AlipayApiException e) {
             throw new RuntimeException("账单详情查询异常: " + e.getMessage(), e);
         }

+ 14 - 2
java/src/main/java/com/payment/platform/module/payment/notification/handler/EnterpriseHandler.java

@@ -99,15 +99,27 @@ public class EnterpriseHandler extends BaseNotifyHandler {
                         .eq(EnterpriseEntity::getEnterpriseId, enterpriseId));
 
         if (entity == null) {
-            // 通知到达时本地尚未创建 → 新建记录
+            // 通知到达时本地尚未创建 → 先查预登记记录(applyInvite 创建的 PENDING 记录)
+            EnterpriseEntity pending = null;
+            if (StrUtil.isNotBlank(outBizNo)) {
+                pending = enterpriseMapper.selectOne(
+                        new LambdaQueryWrapper<EnterpriseEntity>()
+                                .eq(EnterpriseEntity::getOutBizNo, outBizNo));
+            }
             entity = new EnterpriseEntity();
             entity.setEnterpriseId(enterpriseId);
             entity.setOutBizNo(outBizNo);
             entity.setAccountId(accountId);
             entity.setStatus(status);
             entity.setRemark(remark);
+            if (pending != null) {
+                entity.setServiceProviderId(pending.getServiceProviderId());
+                entity.setScopeLabel(pending.getScopeLabel());
+                entity.setName(pending.getName());
+                enterpriseMapper.deleteById(pending);
+            }
             enterpriseMapper.insert(entity);
-            log.info("通知创建企业记录: enterprise_id={}, status={}", enterpriseId, status);
+            log.info("通知创建企业记录: enterprise_id={}, status={}, providerId={}", enterpriseId, status, entity.getServiceProviderId());
         } else {
             // 已存在 → 更新状态
             entity.setStatus(status);

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/notification/handler/VoucherHandler.java

@@ -128,7 +128,7 @@ public class VoucherHandler extends BaseNotifyHandler {
 
         AlipayCommerceEcConsumeDetailQueryResponse response;
         try {
-            response = alipayClientFactory.getClient().execute(request);
+            response = alipayClientFactory.getClient(enterpriseId).execute(request);
         } catch (AlipayApiException e) {
             log.warn("查询账单详情失败(不影响凭证处理): pay_no={}, error={}", payNo, e.getMessage());
             return;

+ 79 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/controller/ServiceProviderController.java

@@ -0,0 +1,79 @@
+package com.payment.platform.module.payment.serviceprovider.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.payment.serviceprovider.dto.ServiceProviderCreateDTO;
+import com.payment.platform.module.payment.serviceprovider.dto.ServiceProviderUpdateDTO;
+import com.payment.platform.module.payment.serviceprovider.dto.ServiceProviderVO;
+import com.payment.platform.module.payment.serviceprovider.service.ServiceProviderService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "服务商管理")
+@RestController
+@RequestMapping("/system/service-provider")
+@RequiredArgsConstructor
+public class ServiceProviderController {
+
+    private final ServiceProviderService service;
+
+    @GetMapping("/list")
+    @PreAuthorize("@perm.hasAny('module_system:service_provider:list')")
+    @Operation(summary = "服务商分页列表")
+    public Result<PageResult<ServiceProviderVO>> list(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+            @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String scopeLabel,
+            @RequestParam(required = false) String status) {
+        return Result.ok(service.page(pageNo, pageSize, name, scopeLabel, status));
+    }
+
+    @GetMapping("/detail/{id}")
+    @PreAuthorize("@perm.hasAny('module_system:service_provider:detail')")
+    @Operation(summary = "服务商详情")
+    public Result<ServiceProviderVO> detail(@PathVariable Long id) {
+        return Result.ok(service.detail(id));
+    }
+
+    @PostMapping("/create")
+    @PreAuthorize("@perm.hasAny('module_system:service_provider:create')")
+    @Operation(summary = "新增服务商")
+    public Result<ServiceProviderVO> create(@Valid @RequestBody ServiceProviderCreateDTO dto) {
+        return Result.ok(service.create(dto));
+    }
+
+    @PutMapping("/update/{id}")
+    @PreAuthorize("@perm.hasAny('module_system:service_provider:update')")
+    @Operation(summary = "修改服务商")
+    public Result<ServiceProviderVO> update(@PathVariable Long id, @RequestBody ServiceProviderUpdateDTO dto) {
+        return Result.ok(service.update(id, dto));
+    }
+
+    @DeleteMapping("/delete")
+    @PreAuthorize("@perm.hasAny('module_system:service_provider:delete')")
+    @Operation(summary = "删除服务商")
+    public Result<Void> delete(@RequestBody List<Long> ids) {
+        service.delete(ids);
+        return Result.ok();
+    }
+
+    @PatchMapping("/toggle/{id}")
+    @PreAuthorize("@perm.hasAny('module_system:service_provider:update')")
+    @Operation(summary = "启用/停用服务商")
+    public Result<ServiceProviderVO> toggle(@PathVariable Long id) {
+        return Result.ok(service.toggle(id));
+    }
+
+    @GetMapping("/options")
+    @Operation(summary = "服务商下拉选项(仅 ACTIVE,无需登录权限)")
+    public Result<List<ServiceProviderVO>> options() {
+        return Result.ok(service.options());
+    }
+}

+ 29 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/dto/ServiceProviderCreateDTO.java

@@ -0,0 +1,29 @@
+package com.payment.platform.module.payment.serviceprovider.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class ServiceProviderCreateDTO {
+
+    @NotBlank(message = "服务商名称不能为空")
+    private String name;
+
+    @NotBlank(message = "业务范围标签不能为空")
+    private String scopeLabel;
+
+    @NotBlank(message = "支付宝 AppId 不能为空")
+    private String appId;
+
+    @NotBlank(message = "应用私钥不能为空")
+    private String appPrivateKey;
+
+    @NotBlank(message = "支付宝公钥不能为空")
+    private String alipayPublicKey;
+
+    private String serverUrl = "https://openapi.alipay.com/gateway.do";
+    private String signType = "RSA2";
+    private String format = "JSON";
+    private String charset = "UTF-8";
+    private String description;
+}

+ 13 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/dto/ServiceProviderUpdateDTO.java

@@ -0,0 +1,13 @@
+package com.payment.platform.module.payment.serviceprovider.dto;
+
+import lombok.Data;
+
+/**
+ * 编辑仅允许名称和业务范围 — 密钥等敏感配置仅允许 DB 维护
+ */
+@Data
+public class ServiceProviderUpdateDTO {
+
+    private String name;
+    private String scopeLabel;
+}

+ 25 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/dto/ServiceProviderVO.java

@@ -0,0 +1,25 @@
+package com.payment.platform.module.payment.serviceprovider.dto;
+
+import lombok.Data;
+import java.time.OffsetDateTime;
+
+@Data
+public class ServiceProviderVO {
+
+    private Long id;
+    private String name;
+    private String scopeLabel;
+    private String appId;
+    private String serverUrl;
+    private String signType;
+    private String format;
+    private String charset;
+    private String providerStatus;
+    private String description;
+    private OffsetDateTime createdTime;
+    private OffsetDateTime updatedTime;
+
+    /** 不返回完整密钥,仅返回脱敏片段供确认 */
+    private String appPrivateKeyHint;
+    private String alipayPublicKeyHint;
+}

+ 25 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/entity/ServiceProviderEntity.java

@@ -0,0 +1,25 @@
+package com.payment.platform.module.payment.serviceprovider.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("pay_service_provider")
+public class ServiceProviderEntity extends BaseEntity {
+
+    private String name;
+    private String scopeLabel;
+    private String appId;
+    private String appPrivateKey;
+    private String alipayPublicKey;
+    private String serverUrl;
+    private String signType;
+    private String format;
+    private String charset;
+    @com.baomidou.mybatisplus.annotation.TableField("provider_status")
+    private String providerStatus;
+    private String description;
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/mapper/ServiceProviderMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.payment.serviceprovider.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.payment.serviceprovider.entity.ServiceProviderEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ServiceProviderMapper extends BaseMapper<ServiceProviderEntity> {
+}

+ 112 - 0
java/src/main/java/com/payment/platform/module/payment/serviceprovider/service/ServiceProviderService.java

@@ -0,0 +1,112 @@
+package com.payment.platform.module.payment.serviceprovider.service;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.core.alipay.AlipayClientFactory;
+import com.payment.platform.module.payment.serviceprovider.dto.ServiceProviderCreateDTO;
+import com.payment.platform.module.payment.serviceprovider.dto.ServiceProviderUpdateDTO;
+import com.payment.platform.module.payment.serviceprovider.dto.ServiceProviderVO;
+import com.payment.platform.module.payment.serviceprovider.entity.ServiceProviderEntity;
+import com.payment.platform.module.payment.serviceprovider.mapper.ServiceProviderMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class ServiceProviderService {
+
+    private final ServiceProviderMapper mapper;
+    private final AlipayClientFactory alipayClientFactory;
+
+    public PageResult<ServiceProviderVO> page(int pageNo, int pageSize, String name, String scopeLabel, String status) {
+        LambdaQueryWrapper<ServiceProviderEntity> w = new LambdaQueryWrapper<>();
+        if (name != null && !name.isBlank()) w.like(ServiceProviderEntity::getName, name);
+        if (scopeLabel != null && !scopeLabel.isBlank()) w.eq(ServiceProviderEntity::getScopeLabel, scopeLabel);
+        if (status != null && !status.isBlank()) w.eq(ServiceProviderEntity::getProviderStatus, status);
+        w.orderByDesc(ServiceProviderEntity::getUpdatedTime);
+        Page<ServiceProviderEntity> page = new Page<>(pageNo, pageSize);
+        Page<ServiceProviderEntity> result = mapper.selectPage(page, w);
+        return PageResult.of(pageNo, pageSize, result.getTotal(),
+                result.getRecords().stream().map(this::toVO).collect(Collectors.toList()));
+    }
+
+    public ServiceProviderVO detail(Long id) {
+        ServiceProviderEntity e = getOrThrow(id);
+        return toVO(e);
+    }
+
+    @Transactional
+    public ServiceProviderVO create(ServiceProviderCreateDTO dto) {
+        ServiceProviderEntity e = BeanUtil.copyProperties(dto, ServiceProviderEntity.class);
+        e.setProviderStatus("ACTIVE");
+        e.setUuid(java.util.UUID.randomUUID().toString());
+        mapper.insert(e);
+        return toVO(e);
+    }
+
+    @Transactional
+    public ServiceProviderVO update(Long id, ServiceProviderUpdateDTO dto) {
+        ServiceProviderEntity e = getOrThrow(id);
+        if (dto.getName() != null) e.setName(dto.getName());
+        if (dto.getScopeLabel() != null) e.setScopeLabel(dto.getScopeLabel());
+        // 密钥等敏感字段仅允许通过数据库直接维护
+        mapper.updateById(e);
+        return toVO(e);
+    }
+
+    @Transactional
+    public void delete(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) throw new BusinessException(400, "请选择要删除的服务商");
+        mapper.deleteBatchIds(ids);
+        ids.forEach(alipayClientFactory::refreshClient);
+    }
+
+    @Transactional
+    public ServiceProviderVO toggle(Long id) {
+        ServiceProviderEntity e = getOrThrow(id);
+        e.setProviderStatus("ACTIVE".equals(e.getProviderStatus()) ? "DISABLED" : "ACTIVE");
+        mapper.updateById(e);
+        alipayClientFactory.refreshClient(id);
+        return toVO(e);
+    }
+
+    public List<ServiceProviderVO> options() {
+        return mapper.selectList(
+                new LambdaQueryWrapper<ServiceProviderEntity>()
+                        .eq(ServiceProviderEntity::getProviderStatus, "ACTIVE")
+                        .orderByDesc(ServiceProviderEntity::getUpdatedTime))
+                .stream().map(e -> {
+                    ServiceProviderVO vo = new ServiceProviderVO();
+                    vo.setId(e.getId());
+                    vo.setName(e.getName());
+                    vo.setScopeLabel(e.getScopeLabel());
+                    return vo;
+                }).collect(Collectors.toList());
+    }
+
+    public ServiceProviderEntity getById(Long id) {
+        return id != null ? mapper.selectById(id) : null;
+    }
+
+    private ServiceProviderEntity getOrThrow(Long id) {
+        ServiceProviderEntity e = mapper.selectById(id);
+        if (e == null) throw new BusinessException(404, "服务商不存在");
+        return e;
+    }
+
+    private ServiceProviderVO toVO(ServiceProviderEntity e) {
+        ServiceProviderVO vo = BeanUtil.copyProperties(e, ServiceProviderVO.class);
+        String pk = e.getAppPrivateKey();
+        if (pk != null && pk.length() > 16) vo.setAppPrivateKeyHint(pk.substring(0, 8) + "****" + pk.substring(pk.length() - 8));
+        String pubk = e.getAlipayPublicKey();
+        if (pubk != null && pubk.length() > 16) vo.setAlipayPublicKeyHint(pubk.substring(0, 8) + "****" + pubk.substring(pubk.length() - 8));
+        return vo;
+    }
+}

+ 5 - 9
java/src/main/java/com/payment/platform/module/system/menu/mapper/MenuMapper.java

@@ -1,5 +1,6 @@
 package com.payment.platform.module.system.menu.mapper;
 
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.payment.platform.module.system.menu.entity.MenuEntity;
 import org.apache.ibatis.annotations.Mapper;
@@ -10,20 +11,15 @@ import java.util.List;
 @Mapper
 public interface MenuMapper extends BaseMapper<MenuEntity> {
 
-    /**
-     * 根据用户ID查询有权限的菜单ID列表
-     * 对应 Python: 从 auth.user.roles 中收集 role.menus 的 id
-     */
+    /** 查询用户有权限的菜单ID — 跳过租户拦截(sys_role_menus/sys_user_roles 无 tenant_id) */
+    @InterceptorIgnore(tenantLine = "true")
     @Select("SELECT DISTINCT rm.menu_id FROM sys_role_menus rm " +
             "INNER JOIN sys_user_roles ur ON ur.role_id = rm.role_id " +
             "WHERE ur.user_id = #{userId}")
     List<Long> selectMenuIdsByUserId(Long userId);
 
-    /**
-     * 根据用户ID查询权限标识列表(用于 Spring Security GrantedAuthority)
-     * 对应 Python dependencies.py AuthPermission: user_permissions = {menu.permission for role in auth.user.roles for menu in role.menus}
-     * 格式: "module_payment:account:transfer"
-     */
+    /** 查询用户权限标识 — 跳过租户拦截(三表 JOIN 无 tenant_id) */
+    @InterceptorIgnore(tenantLine = "true")
     @Select("SELECT DISTINCT m.permission FROM sys_menu m " +
             "INNER JOIN sys_role_menus rm ON rm.menu_id = m.id " +
             "INNER JOIN sys_user_roles ur ON ur.role_id = rm.role_id " +

+ 3 - 1
java/src/main/java/com/payment/platform/module/system/position/mapper/PositionMapper.java

@@ -1,5 +1,6 @@
 package com.payment.platform.module.system.position.mapper;
 
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.payment.platform.module.system.position.entity.PositionEntity;
 import org.apache.ibatis.annotations.Mapper;
@@ -10,7 +11,8 @@ import java.util.List;
 @Mapper
 public interface PositionMapper extends BaseMapper<PositionEntity> {
 
-    /** 查询用户关联的岗位 */
+    /** 查询用户关联的岗位(跳过租户拦截 — sys_user_positions 无 tenant_id 列) */
+    @InterceptorIgnore(tenantLine = "true")
     @Select("SELECT p.* FROM sys_position p INNER JOIN sys_user_positions up ON up.position_id = p.id WHERE up.user_id = #{userId}")
     List<PositionEntity> selectByUserId(Long userId);
 }

+ 28 - 0
java/src/main/java/com/payment/platform/module/system/role/dto/RoleExportVO.java

@@ -0,0 +1,28 @@
+package com.payment.platform.module.system.role.dto;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+
+@Data
+public class RoleExportVO {
+
+    @ExcelProperty("ID")
+    private Long id;
+
+    @ExcelProperty("角色名称")
+    private String name;
+
+    @ExcelProperty("角色编码")
+    private String code;
+
+    @ExcelProperty("状态")
+    private String status;
+
+    @ExcelProperty("描述")
+    private String description;
+
+    @ExcelProperty("创建时间")
+    private OffsetDateTime createdTime;
+}

+ 2 - 0
java/src/main/java/com/payment/platform/module/system/role/mapper/RoleMapper.java

@@ -1,5 +1,6 @@
 package com.payment.platform.module.system.role.mapper;
 
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.payment.platform.module.system.role.entity.RoleEntity;
 import org.apache.ibatis.annotations.Delete;
@@ -13,6 +14,7 @@ import java.util.List;
 public interface RoleMapper extends BaseMapper<RoleEntity> {
 
     /** 查询用户关联的角色 */
+    @InterceptorIgnore(tenantLine = "true")
     @Select("SELECT r.* FROM sys_role r INNER JOIN sys_user_roles ur ON ur.role_id = r.id WHERE ur.user_id = #{userId}")
     List<RoleEntity> selectByUserId(Long userId);
 

+ 3 - 0
java/src/main/java/com/payment/platform/module/system/user/dto/UserInfoVO.java

@@ -69,6 +69,9 @@ public class UserInfoVO extends BaseVO {
     @Schema(description = "更新者")
     private SimpleUserInfo updatedBy;
 
+    @Schema(description = "权限标识列表")
+    private List<String> permissions;
+
     @Schema(description = "菜单树")
     private List<MenuNode> menus;
 

+ 1 - 0
java/src/main/java/com/payment/platform/module/system/user/service/UserService.java

@@ -51,6 +51,7 @@ public class UserService {
         UserEntity user = userMapper.selectById(loginUser.getUserId());
         if (user == null) throw new BusinessException(404, "用户不存在");
         UserInfoVO vo = toUserInfoVO(user);
+        vo.setPermissions(menuMapper.selectPermissionsByUserId(user.getId()));
         List<UserInfoVO.MenuNode> menus;
         if (loginUser.getIsSuperuser() != null && loginUser.getIsSuperuser()) {
             menus = getAllMenusTree();

+ 4 - 4
java/src/main/resources/application.yml

@@ -74,11 +74,11 @@ knife4j:
   setting:
     language: zh_cn
 
-# Alipay
+# Alipay(仅作 fallback — 正常运行时使用 pay_service_provider 表配置)
 alipay:
-  app-id: "2021005122654283"
-  app-private-key: "MIIEogIBAAKCAQEAnhU7Ix3+klh7k+5+Vx10tmqS2MkWD3MIkB+GpwZm94SccEISoLjCatW56U1QJo7D9SP1rSwyJ3dtvbiEpje8f0D+iaHilX9tkdWZMnsI9XbQqmqLkjDPFdmII3sa4p9Mx1UUOtIGmPhKYmnR8jM85SEmf9ivuFOKZISny5LxDYam2o3czGKjRiLgb7oNs4LKf9isc1k4r/XeCEAhLPejFgilSiKWC8DQbiSG15DvOKrr0BxM1gb0YUngQr11diUSYx5LRXWjhUE7WkPaSGkLDVuZ+ZAHddH9DddPrfjoQpiq7ROJNTkxVuNo5Bqont5fvLnzIN9/9pR8bJVLNBDFlQIDAQABAoIBADbXhryYG0kKCVU41/vA7EycjHVIza5uafoV9dDcF7ym9N69DVlUv53wp56Yg8XcoX7aCtEZFA09EYVQDjTcATjkg2mcD89tdcWyJIOuy1zc62czr1f4Nt+Np/0nKByWxzwf9/SwCDnpaWTa8UrlG6sh5QlVUyDPWqOkodGuGJJoJoaC/yM2kXjovR/JbKvqevRRI/ZpqUU8OXU1MzBuYH3OEWmYjDFpydIJnEYRS9U4Ftgo3q8diRbTSb1rPR5cxyJh6ikoRNLnrWuEX/B2QW32Qag6vYrjq+LXtW7hzZYoNUAaEzf2c8WP9nrzw3IBvAF+sBBhiyH8wlxcSEndR8ECgYEA8Ka2sy4iTuLkf9Qxd22IRAxXgnkh7ITB71OM4OIU5dZULysAbnwTc/9B6URyuQ7o64VFQQHZtXLM/Ujt2l09RU2caUBkzRS495g6H53mZSY78k4F6nI9eEqTE0PPRiPZ/NrV1ZeUdOU4p1iaO6KxHJrMY5hCRMns5OX/jm4DfhsCgYEAqCpdfpTRqW03pMpM0MN3S43Z4xdthxeBDa0mGGo3CpQImj8WvVaAMiTNjnH/NBupXT/akn6lURQANo9IjR4EvoA8vgtYBYOrB+kchDRwbl4elAzdGYcUWixBnuKaz23yncOFXai0RwcnSZ57lF++fUlC778u2MUoLGk8JeozRg8CgYAJDSw4BxcxQmV3zWJi7JLAhHpSJP46qC+nMcxNtRM2Jd6au5JTlYUhyssO3A04mq/2E9gr/sbShVPifvi7SuCAF9A3QT3JXOssHOSqxcShr1N35KliTRO0z5FCmz1TlQug8BY053OlBe4glTuP3Kmuur/PSy3K6pFndqMrF0Y4WwKBgGbaopyNQT8zQdlUsm7tXsWjWSUAa0k5IwHOaYJ9VHTv3eMZuzrK8VW6FM+PAdxJHumd6H25YDua1BaKxIErXxN1M7G5Fnko8y6/cWWa/TlD78f1pjW63MPaVbhsOOut/7pmn+eNC9Z1lZ39hPBXLxdJ+9rPQEPtMokXRGacPAgZAoGATt/Rko7fkFST19JOiMorFyp26cZQ91ZLrPADRIZEN4H9ShZhHTeSBN32ZGUFYsOjLzM8g0Xui2w2NAQBrmeHCGNXrV+vFRX6+s5cccUtohtpjYK07mz4RbeScWL/oebNubDR1XsdiUKe+JQsHT5MMksKk/kmBbIfIJ3wDdeJLlk="
-  alipay-public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqUX9WqZPwh57HqR2RiAEYe8GWrXl8Zz9YC7dz7dnfGY1k/ma8/w18TC7txausbCCHCEWl52836+gdD1uQumCxPYPtkoWcZy8984kCE3whaV9O2PoaCd6Q3/Ww0WpRAvJDGDpa032t3vuTPSUbbU0N8iYfPua9a8z1JjbD4hDiIVMN797PllUDQEuIfNV+C06usGZxL01e/zBdMiIun1HhrVqHQ+p+GjHQXZ58kqP1EIGOg4/1HPib5i4umXULnVsCYZ0dvvdyrSiJxCnqMZZmtVSVm9rA+TYaiEoVf1RlchZPOKBFhF1gMGRsYh3LPL9cU4lbqt6DpZiLggsudD4NQIDAQAB"
+  app-id: ""
+  app-private-key: ""
+  alipay-public-key: ""
   server-url: https://openapi.alipay.com/gateway.do
   format: JSON
   charset: UTF-8