15 Commits 8adab022f0 ... e68143718c

Autore SHA1 Messaggio Data
  alphaH e68143718c fix bug 1 mese fa
  alphaH e2aabf45e5 fix: 银行卡蛇形字段转驼峰 — setAccount_type→setAccountType 1 mese fa
  alphaH 0e0c3fce4b fix: 对公账户标红required + 银行名校验 + 提醒提示 1 mese fa
  alphaH bc420a9288 fix: account_type空值时兜底为2而非过滤掉 1 mese fa
  alphaH 06cfc64cc1 fix: 银行卡信息过滤空字符串避免传空值到支付宝 1 mese fa
  alphaH d2fed67a2a fix: 备注改为必填+场景联动placeholder提示, 后端强制校验 1 mese fa
  alphaH dbb677d4f2 fix: transfer_scene_report_infos按场景自动填写 1 mese fa
  alphaH 10bd4408ce feat: 转账表单加场景选择: 佣金报酬/业务结算/二手回收 1 mese fa
  alphaH f8a8f7f559 fix: transferSceneName默认佣金报酬,SDK4.40已有该字段 1 mese fa
  alphaH 4cf56b20ec fix: JDBC URL加stringtype=unspecified让PG自动推断类型,解决json列报错 1 mese fa
  alphaH c4b0b6119b fix: insertTransfer改用XML mapper避免注解冲突 1 mese fa
  alphaH f40eadbb09 fix: insertTransfer独立方法避免与BaseMapper.insert冲突 1 mese fa
  alphaH 57873e8208 fix: TransferMapper insert加jsonb转换 + 收款方增加ALIPAY_LOGON_ID 1 mese fa
  alphaH 64a0a01d93 fix: admin不受transferEnabled限制,hasTransferPermission加is_platform_user判断 1 mese fa
  alphaH 4949a03046 fix: EnterpriseSwitcher仅单企业时自动选中,多企业admin需手动选 1 mese fa

BIN
frontend/dist.zip


+ 1 - 0
frontend/src/api/module_payment/account.ts

@@ -149,6 +149,7 @@ export const AccountAPI = {
     amount: number;
     order_title?: string;
     remark?: string;
+    transfer_scene_name?: string;
     payee_info: {
       identity_type: string;
       name: string;

+ 2 - 2
frontend/src/layouts/components/NavBar/components/EnterpriseSwitcher.vue

@@ -51,9 +51,9 @@ const currentEnterpriseName = computed(() => {
   return `${currentEnterprise.value.name} (${currentEnterprise.value.enterprise_id.slice(-4)})`;
 });
 
-// 在组件挂载或依赖变化时处理
+// 仅单个企业时自动选中,多个企业不自动选(admin 需手动选择)
 onMounted(() => {
-  if (!currentEnterprise.value && hasEnterprise.value && enterpriseList.value.length > 0) {
+  if (!currentEnterprise.value && hasEnterprise.value && enterpriseList.value.length === 1) {
     enterpriseStore.setCurrentEnterprise(enterpriseList.value[0].enterprise_id);
   }
 });

+ 53 - 10
frontend/src/views/module_payment/account/index.vue

@@ -210,9 +210,15 @@
               <el-form-item label="转账标题" prop="order_title">
                 <el-input v-model="transferForm.order_title" placeholder="请输入转账标题" />
               </el-form-item>
+              <el-form-item label="转账场景" prop="transfer_scene_name">
+                <el-select v-model="transferForm.transfer_scene_name" placeholder="请选择转账场景">
+                  <el-option v-for="s in TRANSFER_SCENE_OPTIONS" :key="s.value" :label="s.label" :value="s.value" />
+                </el-select>
+              </el-form-item>
               <el-form-item label="收款方类型" prop="payee_info.identity_type">
                 <el-select v-model="transferForm.payee_info.identity_type" placeholder="请选择收款方类型">
-                  <el-option label="支付宝账户" value="ALIPAY_USER_ID" />
+                  <el-option label="支付宝账户(UID)" value="ALIPAY_USER_ID" />
+                  <el-option label="支付宝账户(手机号/邮箱)" value="ALIPAY_LOGON_ID" />
                   <el-option label="银行卡" value="BANKCARD_ACCOUNT" />
                   <!-- <el-option label="资金专户" value="book" /> -->
                 </el-select>
@@ -230,25 +236,30 @@
                     <el-option label="个人账户" value="2" />
                   </el-select>
                 </el-form-item>
-                <el-form-item label="银行名称" prop="payee_info.bankcard_ext_info.inst_name">
+                <el-form-item label="银行名称" prop="payee_info.bankcard_ext_info.inst_name" :required="isCorporate">
                   <el-input v-model="transferForm.payee_info.bankcard_ext_info!.inst_name" placeholder="请输入银行名称" />
                 </el-form-item>
-                <el-form-item label="开户省份" prop="payee_info.bankcard_ext_info.inst_province">
+                <template v-if="isCorporate">
+                  <el-alert type="warning" :closable="false" show-icon style="margin-bottom:12px">
+                    对公账户必须填写省市支行信息 或 联行号
+                  </el-alert>
+                </template>
+                <el-form-item label="开户省份" prop="payee_info.bankcard_ext_info.inst_province" :required="isCorporate">
                   <el-input v-model="transferForm.payee_info.bankcard_ext_info!.inst_province" placeholder="请输入开户省份" />
                 </el-form-item>
-                <el-form-item label="开户城市" prop="payee_info.bankcard_ext_info.inst_city">
+                <el-form-item label="开户城市" prop="payee_info.bankcard_ext_info.inst_city" :required="isCorporate">
                   <el-input v-model="transferForm.payee_info.bankcard_ext_info!.inst_city" placeholder="请输入开户城市" />
                 </el-form-item>
-                <el-form-item label="支行名称" prop="payee_info.bankcard_ext_info.inst_branch_name">
+                <el-form-item label="支行名称" prop="payee_info.bankcard_ext_info.inst_branch_name" :required="isCorporate">
                   <el-input v-model="transferForm.payee_info.bankcard_ext_info!.inst_branch_name"
                     placeholder="请输入支行名称" />
                 </el-form-item>
-                <el-form-item label="联行号" prop="payee_info.bankcard_ext_info.bank_code">
+                <el-form-item label="联行号" prop="payee_info.bankcard_ext_info.bank_code" :required="isCorporate">
                   <el-input v-model="transferForm.payee_info.bankcard_ext_info!.bank_code" placeholder="请输入联行号" />
                 </el-form-item>
               </template>
-              <el-form-item label="备注" prop="remark">
-                <el-input v-model="transferForm.remark" type="textarea" placeholder="请输入备注" />
+              <el-form-item label="备注" prop="remark" required>
+                <el-input v-model="transferForm.remark" type="textarea" :placeholder="remarkPlaceholder" />
               </el-form-item>
               <el-form-item>
                 <el-button v-hasPerm="['module_payment:account:transfer']" type="primary" :loading="transferLoading"
@@ -802,7 +813,9 @@ const consumeFormRef = ref<FormInstance>();
 const overviewRef = ref<InstanceType<typeof AccountOverview>>();
 
 const currentEnterpriseId = computed(() => enterpriseStore.getCurrentEnterprise?.enterprise_id);
-const hasTransferPermission = computed(() => enterpriseStore.getCurrentEnterprise?.transfer_enabled === true);
+const hasTransferPermission = computed(() =>
+  userStore.is_platform_user || enterpriseStore.getCurrentEnterprise?.transfer_enabled === true
+);
 const cureentEnterprise = computed(() => enterpriseStore.getCurrentEnterprise);
 
 const accountInfo = ref<{
@@ -1055,6 +1068,7 @@ const transferForm = reactive({
   amount: 0,
   order_title: "",
   remark: "",
+  transfer_scene_name: "佣金报酬",
   payee_info: {
     identity_type: "ALIPAY_USER_ID",
     name: "",
@@ -1077,8 +1091,36 @@ const transferRules: FormRules = {
   "payee_info.identity_type": [{ required: true, message: "请选择收款方类型", trigger: "change" }],
   "payee_info.name": [{ required: true, message: "请输入收款方姓名", trigger: "blur" }],
   "payee_info.identity": [{ required: true, message: "请输入收款方账号", trigger: "blur" }],
+  remark: [{ required: true, message: "请填写备注,说明转账事由", trigger: "blur" }],
+  "payee_info.bankcard_ext_info.inst_name": [{
+    validator: (_r: any, _v: any, cb: any) => {
+      if (isCorporate.value && !transferForm.payee_info.bankcard_ext_info?.inst_name)
+        cb(new Error("对公账户银行名称不能为空"));
+      else cb();
+    }, trigger: "blur"
+  }],
 };
 
+const isCorporate = computed(() =>
+  transferForm.payee_info.identity_type === "BANKCARD_ACCOUNT"
+  && transferForm.payee_info.bankcard_ext_info?.account_type === "1"
+);
+
+const TRANSFER_SCENE_OPTIONS = [
+  { value: "佣金报酬", label: "佣金报酬" },
+  { value: "业务结算", label: "业务结算" },
+  { value: "二手回收", label: "二手回收" },
+];
+
+const remarkPlaceholder = computed(() => {
+  const map: Record<string, string> = {
+    "佣金报酬": "请填写佣金报酬说明,如:8月家政服务报酬",
+    "业务结算": "请填写结算款项名称,如:材料货款",
+    "二手回收": "请填写回收商品名称,如:旧衣服",
+  };
+  return map[transferForm.transfer_scene_name] || "请填写转账事由";
+});
+
 const isAlipayPayee = computed(() =>
   transferForm.payee_info.identity_type === "ALIPAY_USER_ID"
   || transferForm.payee_info.identity_type === "ALIPAY_LOGON_ID"
@@ -1605,12 +1647,13 @@ async function handleTransfer() {
           amount: transferForm.amount,
           order_title: transferForm.order_title,
           remark: transferForm.remark,
+          transfer_scene_name: transferForm.transfer_scene_name,
           payee_info: {
             identity_type: transferForm.payee_info.identity_type,
             name: transferForm.payee_info.name,
             identity: transferForm.payee_info.identity,
             bankcard_ext_info:
-              transferForm.payee_info.identity_type === "bank"
+              transferForm.payee_info.identity_type === "BANKCARD_ACCOUNT"
                 ? transferForm.payee_info.bankcard_ext_info
                 : undefined,
           },

+ 3 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java

@@ -58,6 +58,9 @@ public class TransferCreateDTO {
     @Schema(description = "租户ID(内部使用,由 Controller 从认证上下文注入,对应 Python auth.tenant_id)")
     private Long tenantId;
 
+    @Schema(description = "转账场景名称")
+    private String transferSceneName;
+
     @AssertTrue(message = "收款方信息不完整:identity_type 和 name 为必填项")
     public boolean isPayeeInfoValid() {
         if (payeeInfo == null) return false;

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

@@ -76,13 +76,12 @@ public class AccountService {
         for (AccountVO vo : vos) {
             if (vo.getAccountBookId() != null && !vo.getAccountBookId().isBlank()) {
                 try {
-                    /*var alipayAccounts = alipayTransferService.queryAccounts(enterpriseId);
+                    var alipayAccounts = alipayTransferService.queryAccounts(enterpriseId);
                     for (var m : alipayAccounts) {
                         if (vo.getAccountBookId().equals(m.get("account_book_id"))) {
                             vo.setAvailableAmount((String) m.get("available_amount"));
                         }
-                    }*/
-                    vo.setAvailableAmount("1");
+                    }
                 } catch (Exception ignored) {}
             }
         }

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

@@ -525,11 +525,10 @@ public class AlipayTransferService {
             model.setBizScene(BIZ_SCENE_TRANSFER);
             model.setProductCode(PRODUCT_CODE);
             model.setOrderTitle(orderTitle);
-            // 接入指南 §4.2: 单笔金额超过50000时 remark 必传
-            if (amount.compareTo(new BigDecimal("50000")) > 0
-                    && (remark == null || remark.isBlank()))
-                throw new BusinessException(400, "单笔转账超过50000元必须填写备注");
-            if (remark != null && !remark.isBlank()) model.setRemark(remark);
+            // 备注必填
+            if (remark == null || remark.isBlank())
+                throw new BusinessException(400, "备注不能为空,请填写转账事由");
+            model.setRemark(remark);
 
             // 必填: 付款方(记账本)
             Participant payer = new Participant();
@@ -554,15 +553,44 @@ public class AlipayTransferService {
                 BankcardExtInfo bce = new BankcardExtInfo();
                 Class<?> bceClass = bce.getClass();
                 for (var entry : bankcardInfo.entrySet()) {
-                    String setter = "set" + Character.toUpperCase(entry.getKey().charAt(0))
-                            + entry.getKey().substring(1);
-                    try { bceClass.getMethod(setter, String.class).invoke(bce, entry.getValue()); }
+                    Object val = entry.getValue();
+                    // account_type 为必填项,空值时兜底为 "2"(个人账户)
+                    if ("account_type".equals(entry.getKey())
+                            && (val == null || (val instanceof String s && s.isBlank())))
+                        val = "2";
+                    if (val == null || (val instanceof String s && s.isBlank())) continue;
+                    // 蛇形转驼峰: account_type -> setAccountType
+                    String key = entry.getKey();
+                    StringBuilder camel = new StringBuilder();
+                    boolean cap = true;
+                    for (char c : key.toCharArray()) {
+                        if (c == '_') { cap = true; }
+                        else { camel.append(cap ? Character.toUpperCase(c) : c); cap = false; }
+                    }
+                    String setter = "set" + camel;
+                    try { bceClass.getMethod(setter, String.class).invoke(bce, val.toString()); }
                     catch (Exception ignored) {}
                 }
                 payee.setBankcardExtInfo(bce);
             }
             model.setPayeeInfo(payee);
 
+            // 转账场景 + 场景信息
+            String sceneName = dto.getTransferSceneName() != null
+                    ? dto.getTransferSceneName() : "佣金报酬";
+            model.setTransferSceneName(sceneName);
+            var sceneReportInfos = new java.util.ArrayList<com.alipay.api.domain.TransferSceneReportInfo>();
+            com.alipay.api.domain.TransferSceneReportInfo info = new com.alipay.api.domain.TransferSceneReportInfo();
+            info.setInfoType(switch (sceneName) {
+                case "佣金报酬" -> "佣金报酬说明";
+                case "业务结算" -> "结算款项说明";
+                case "二手回收" -> "回收商品说明";
+                default -> "佣金报酬说明";
+            });
+            info.setInfoContent(remark != null && !remark.isBlank() ? remark : "转账");
+            sceneReportInfos.add(info);
+            model.setTransferSceneReportInfos(sceneReportInfos);
+
             // 选填: 转账场景信息(通过 businessParams JSON 透传)
             if (extInfo != null && (extInfo.containsKey("transfer_scene_name")
                     || extInfo.containsKey("transfer_scene_report_infos"))) {

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

@@ -24,7 +24,7 @@ spring:
     type: com.alibaba.druid.pool.DruidDataSource
     druid:
       driver-class-name: org.postgresql.Driver
-      url: jdbc:postgresql://localhost:5432/payment_platform_java?currentSchema=public
+      url: jdbc:postgresql://localhost:5432/payment_platform_java?currentSchema=public&stringtype=unspecified
       username: admin
       password: 'xjz#123321'
       initial-size: 5

BIN
logs/payment-platform-error.2026-07-16.0.log.gz


File diff suppressed because it is too large
+ 730 - 1
logs/payment-platform-error.log


File diff suppressed because it is too large
+ 85 - 0
logs/payment-platform.log


Some files were not shown because too many files changed in this diff