Browse Source

侧边栏添加收款信息,支持添加银行卡

liuxiaole 3 weeks ago
parent
commit
46edcff068

+ 3 - 1
.env.development

@@ -24,7 +24,9 @@ VITE_API_URL_PLATFORM = /api/alienStorePlatform
 
 # 开发环境跨域代理,支持配置多个
 # VITE_PROXY = [["/api","https://api.ailien.shop"]] #生产环境
-VITE_PROXY = [["/api","http://120.26.186.130:8000"]] # 邹建宇
+# VITE_PROXY = [["/api","http://120.26.186.130:8000"]] # 邹建宇
+VITE_PROXY = [["/api","http://192.168.10.80:8000"]] # 邹建宇
+
 
 # WebSocket 基础地址(分享等能力,与商家端一致)
 VITE_WS_BASE = http://120.26.186.130:8000/alienStore/socket/

+ 17 - 0
src/api/modules/newLoginApi.ts

@@ -341,6 +341,11 @@ export const deleteDynamicsById = (params: any) => {
   return httpLogin.get(`/alienStore/userDynamics/deleteDynamicsById`, params);
 };
 
+/** 数据字典(如银行名称 type: bankName) */
+export const getDict = (params: { type: string }) => {
+  return httpLogin.get(`/alienStore/dict/getDict`, params);
+};
+
 //入驻   根据店铺用户ID查询支付配置(设置收款账号)
 export const getPaymentStoreUserId = (params: any) => {
   return httpLogin.get(`/alienStore/store/payment/config/getByStoreUserId`, params);
@@ -389,6 +394,18 @@ export const saveAlipayWithFiles = (
   return httpLogin.post(`/alienStore/store/payment/config/saveAlipayByStoreUserId`, fd);
 };
 
+/**
+ * 以 multipart/form-data 保存银行账户收款配置
+ * 字段:storeUserId、bankName、bankCardNo(与后端 @RequestParam / Multipart 对齐时可按需改名)
+ */
+export const saveBankAccountConfig = (params: { storeUserId: string | number; bankName: string; bankCardNo: string }) => {
+  const fd = new FormData();
+  fd.append("storeUserId", String(params.storeUserId));
+  fd.append("bankName", params.bankName);
+  fd.append("bankCardNo", params.bankCardNo);
+  return httpLogin.post(`/alienStore/store/payment/config/saveBankByStoreUserId`, fd);
+};
+
 // 根据手机号获取用户ID
 export const getUserByPhone = (params: {
   phone: string; // 手机号

+ 14 - 0
src/assets/json/authMenuList.json

@@ -349,6 +349,20 @@
           }
         },
         {
+          "path": "/storeDecoration/receivingAccount",
+          "name": "receivingAccount",
+          "component": "/storeDecoration/receivingAccount/index",
+          "meta": {
+            "icon": "Wallet",
+            "title": "收款账号",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
           "path": "/storeDecoration/businessHours",
           "name": "businessHours",
           "component": "/storeDecoration/businessHours/index",

+ 1 - 0
src/stores/modules/auth.ts

@@ -81,6 +81,7 @@ export const useAuthStore = defineStore({
               case "门店基础信息":
               case "门店头图":
               case "官方相册":
+              case "收款账号":
                 // 所有业务类型都显示
                 menu.meta.isHide = false;
                 break;

+ 0 - 12
src/views/storeDecoration/basicStoreInformation/index.vue

@@ -200,9 +200,6 @@
           <el-form-item label="经营类目" prop="businessCategoryName">
             <el-input v-model="formData.businessCategoryName" placeholder="请输入经营类目" clearable maxlength="50" />
           </el-form-item>
-          <el-form-item label="设置收款账号">
-            <div style="color: #6c8ff8; cursor: pointer" @click="handleViewPayAccount">查看</div>
-          </el-form-item>
           <el-form-item label="营业时间">
             <div style="color: #6c8ff8; cursor: pointer" @click="handleViewBusinessHours">查看</div>
           </el-form-item>
@@ -235,8 +232,6 @@
         <el-button type="primary" size="large" :loading="loading" @click="handleSubmit"> 保存 </el-button>
       </div>
     </el-form>
-    <!-- 收款账号弹窗(查看/编辑) -->
-    <GoReceivingAccount v-model="payAccountDialogVisible" />
     <!-- 营业时间弹窗(查看/编辑) -->
     <el-dialog
       v-model="businessHoursDialogVisible"
@@ -394,13 +389,6 @@ import {
 import { getInputPrompt } from "@/api/modules/newLoginApi";
 import { useRoute } from "vue-router";
 import { localGet } from "@/utils";
-import GoReceivingAccount from "@/views/home/components/go-receivingAccount.vue";
-
-// 收款账号弹窗显隐
-const payAccountDialogVisible = ref(false);
-const handleViewPayAccount = () => {
-  payAccountDialogVisible.value = true;
-};
 
 // ========== 营业时间弹窗(参考 1.vue + businessHours 页) ==========
 const weekDays = [

+ 479 - 0
src/views/storeDecoration/receivingAccount/index.vue

@@ -0,0 +1,479 @@
+<template>
+  <div class="receiving-account-page">
+    <h2 class="page-title">收款账号</h2>
+    <p class="page-tip">
+      请选择收款方式并填写相关信息;配置微信/支付宝商户参数或绑定借记卡银行账户,带 * 为必填项。修改后请保存。
+    </p>
+
+    <el-card shadow="never" class="form-card">
+      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" label-position="top">
+        <el-form-item label="账号类型" prop="accountType">
+          <el-radio-group v-model="form.accountType">
+            <el-radio value="wechat"> 微信 </el-radio>
+            <el-radio value="alipay"> 支付宝 </el-radio>
+            <el-radio value="bank"> 银行账号 </el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <template v-if="form.accountType === 'wechat'">
+          <el-form-item label="appid" prop="wechatAppid">
+            <el-input v-model="form.wechatAppid" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item label="mchld" prop="wechatMchld">
+            <el-input v-model="form.wechatMchld" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item label="API证书序列号" prop="wechatApiCertSerialNo">
+            <el-input v-model="form.wechatApiCertSerialNo" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item label="APIv3 Key" prop="wechatApiV3Key">
+            <el-input v-model="form.wechatApiV3Key" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item label="支付公钥ID" prop="wechatPayPublicKeyId">
+            <el-input v-model="form.wechatPayPublicKeyId" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item label="私钥证书" prop="wechatPrivateKeyFile">
+            <el-upload
+              v-model:file-list="form.wechatPrivateKeyFile"
+              class="cert-upload"
+              drag
+              :auto-upload="false"
+              :limit="1"
+              accept=".pem"
+              :on-exceed="() => ElMessage.warning('最多上传1个文件')"
+            >
+              <div class="upload-inner">
+                <el-icon :size="32" color="#6c8ff8">
+                  <UploadFilled />
+                </el-icon>
+                <p>点击或拖拽 .pem 至此处</p>
+                <span>不超过 2MB</span>
+              </div>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label="公钥证书" prop="wechatPayPublicKeyFile">
+            <el-upload
+              v-model:file-list="form.wechatPayPublicKeyFile"
+              class="cert-upload"
+              drag
+              :auto-upload="false"
+              :limit="1"
+              accept=".pem"
+              :on-exceed="() => ElMessage.warning('最多上传1个文件')"
+            >
+              <div class="upload-inner">
+                <el-icon :size="32" color="#6c8ff8">
+                  <UploadFilled />
+                </el-icon>
+                <p>点击或拖拽 .pem 至此处</p>
+                <span>不超过 2MB</span>
+              </div>
+            </el-upload>
+          </el-form-item>
+        </template>
+
+        <template v-else-if="form.accountType === 'alipay'">
+          <el-form-item label="应用ID" prop="aliAppid">
+            <el-input v-model="form.aliAppid" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item label="应用私钥" prop="aliPrivateId">
+            <el-input v-model="form.aliPrivateId" type="textarea" :rows="4" placeholder="请输入应用私钥" />
+          </el-form-item>
+          <el-form-item label="应用公钥证书" prop="appPublicCertFile">
+            <el-upload
+              v-model:file-list="form.appPublicCertFile"
+              class="cert-upload"
+              drag
+              :auto-upload="false"
+              :limit="1"
+              accept=".crt,.pem"
+              :on-exceed="() => ElMessage.warning('最多上传1个文件')"
+            >
+              <div class="upload-inner">
+                <el-icon :size="32" color="#6c8ff8">
+                  <UploadFilled />
+                </el-icon>
+                <p>应用公钥证书</p>
+              </div>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label="支付宝公钥证书" prop="alipayPublicCertFile">
+            <el-upload
+              v-model:file-list="form.alipayPublicCertFile"
+              class="cert-upload"
+              drag
+              :auto-upload="false"
+              :limit="1"
+              accept=".crt,.pem"
+              :on-exceed="() => ElMessage.warning('最多上传1个文件')"
+            >
+              <div class="upload-inner">
+                <el-icon :size="32" color="#6c8ff8">
+                  <UploadFilled />
+                </el-icon>
+                <p>支付宝公钥证书</p>
+              </div>
+            </el-upload>
+          </el-form-item>
+          <el-form-item label="支付宝根证书" prop="alipayRootCertFile">
+            <el-upload
+              v-model:file-list="form.alipayRootCertFile"
+              class="cert-upload"
+              drag
+              :auto-upload="false"
+              :limit="1"
+              accept=".crt,.pem"
+              :on-exceed="() => ElMessage.warning('最多上传1个文件')"
+            >
+              <div class="upload-inner">
+                <el-icon :size="32" color="#6c8ff8">
+                  <UploadFilled />
+                </el-icon>
+                <p>支付宝根证书</p>
+              </div>
+            </el-upload>
+          </el-form-item>
+        </template>
+
+        <template v-else-if="form.accountType === 'bank'">
+          <el-form-item label="银行名称" prop="bankName">
+            <el-select v-model="form.bankName" filterable clearable placeholder="请选择" style="width: 100%">
+              <el-option v-for="b in bankSelectOptions" :key="b.value" :label="b.label" :value="b.value" />
+            </el-select>
+            <div class="form-item-tip">请选择开户银行的全称</div>
+          </el-form-item>
+          <el-form-item label="银行账号" prop="bankAccountNo">
+            <el-input v-model="form.bankAccountNo" placeholder="请输入" clearable maxlength="23" />
+            <div class="form-item-tip">请输入完整的银行卡号,仅支持借记卡,不支持信用卡</div>
+          </el-form-item>
+        </template>
+
+        <el-form-item>
+          <el-button type="primary" :loading="saving" @click="submit"> 保存 </el-button>
+          <el-button @click="fetchPayAccountConfig"> 取消 </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, watch } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
+import { UploadFilled } from "@element-plus/icons-vue";
+import {
+  getDict,
+  getPaymentStoreUserId,
+  saveWechatWithFiles,
+  saveAlipayWithFiles,
+  saveBankAccountConfig
+} from "@/api/modules/newLoginApi";
+import { localGet } from "@/utils/index";
+
+/** 银行名称下拉:仅展示 getDict 返回的数据 */
+const bankSelectOptions = ref<{ label: string; value: string }[]>([]);
+
+const normalizeDictToOptions = (raw: unknown): { label: string; value: string }[] => {
+  const list = Array.isArray(raw) ? raw : ((raw as any)?.records ?? (raw as any)?.list ?? []);
+  if (!Array.isArray(list)) return [];
+  return list
+    .map((item: any) => {
+      const label = item?.dictDetail ?? item?.dictLabel ?? item?.label ?? item?.name ?? item?.text ?? item?.bankName ?? "";
+      const value = item?.dictValue ?? item?.value ?? item?.code ?? item?.dictDetail ?? item?.dictLabel ?? label;
+      return { label: String(label).trim(), value: String(value).trim() };
+    })
+    .filter((x: { label: string; value: string }) => x.label && x.value);
+};
+
+const loadBankNameDict = async () => {
+  try {
+    const res: any = await getDict({ dictType: "bankName" });
+    if (res?.code !== 200 && res?.code !== "200") {
+      bankSelectOptions.value = [];
+      return;
+    }
+    bankSelectOptions.value = normalizeDictToOptions(res.data);
+  } catch {
+    bankSelectOptions.value = [];
+  }
+};
+
+const formRef = ref<FormInstance>();
+const saving = ref(false);
+
+const form = reactive({
+  accountType: "wechat",
+  wechatAppid: "",
+  wechatMchld: "",
+  wechatApiCertSerialNo: "",
+  wechatApiV3Key: "",
+  wechatPayPublicKeyId: "" as string | null,
+  wechatPrivateKeyFile: [] as UploadUserFile[],
+  wechatPayPublicKeyFile: [] as UploadUserFile[],
+  aliAppid: "",
+  aliPrivateId: "",
+  alipayRootCertFile: [] as UploadUserFile[],
+  alipayPublicCertFile: [] as UploadUserFile[],
+  appPublicCertFile: [] as UploadUserFile[],
+  bankName: "",
+  bankAccountNo: ""
+});
+
+const wechatRules: FormRules = {
+  accountType: [{ required: true, message: "请选择账号类型", trigger: "change" }],
+  wechatAppid: [{ required: true, message: "请输入appid", trigger: "blur" }],
+  wechatMchld: [{ required: true, message: "请输入mchId", trigger: "blur" }],
+  wechatApiCertSerialNo: [{ required: true, message: "请输入API证书序列号", trigger: "blur" }],
+  wechatApiV3Key: [{ required: true, message: "请输入APIv3 Key", trigger: "blur" }],
+  wechatPayPublicKeyId: [{ required: true, message: "请输入支付公钥ID", trigger: "blur" }],
+  wechatPrivateKeyFile: [{ required: true, type: "array", min: 1, message: "请上传私钥证书", trigger: "change" }],
+  wechatPayPublicKeyFile: [{ required: true, type: "array", min: 1, message: "请上传公钥证书", trigger: "change" }]
+};
+
+const alipayRules: FormRules = {
+  accountType: [{ required: true, message: "请选择账号类型", trigger: "change" }],
+  aliAppid: [{ required: true, message: "请输入应用ID", trigger: "blur" }],
+  aliPrivateId: [{ required: true, message: "请输入应用私钥", trigger: "blur" }]
+};
+
+const bankRules: FormRules = {
+  accountType: [{ required: true, message: "请选择账号类型", trigger: "change" }],
+  bankName: [{ required: true, message: "请选择银行名称", trigger: "change" }],
+  bankAccountNo: [
+    { required: true, message: "请输入银行账号", trigger: "blur" },
+    {
+      validator: (_rule, value, callback) => {
+        const s = String(value || "").replace(/\s/g, "");
+        if (!/^\d{16,19}$/.test(s)) {
+          callback(new Error("请输入16~19位数字借记卡卡号"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "blur"
+    }
+  ]
+};
+
+const formRules = computed<FormRules>(() => {
+  if (form.accountType === "alipay") return alipayRules;
+  if (form.accountType === "bank") return bankRules;
+  return wechatRules;
+});
+
+watch(
+  () => form.accountType,
+  () => {
+    formRef.value?.clearValidate();
+  }
+);
+
+const base64ToFile = (base64Str: string, fileName: string): File => {
+  const bstr = atob(base64Str);
+  const u8arr = new Uint8Array(bstr.length);
+  for (let i = 0; i < bstr.length; i++) u8arr[i] = bstr.charCodeAt(i);
+  return new File([u8arr], fileName, { type: "application/x-pem-file" });
+};
+
+const fetchPayAccountConfig = async () => {
+  const storeUserId = localGet("geeker-user")?.userInfo?.id;
+  if (!storeUserId) {
+    ElMessage.warning("请先登录");
+    return;
+  }
+  const res: any = await getPaymentStoreUserId({ storeUserId });
+  if (res?.code !== 200 || !res.data) {
+    return;
+  }
+  const data = res.data;
+
+  form.wechatAppid = "";
+  form.wechatMchld = "";
+  form.wechatApiCertSerialNo = "";
+  form.wechatApiV3Key = "";
+  form.wechatPayPublicKeyId = null;
+  form.wechatPrivateKeyFile = [];
+  form.wechatPayPublicKeyFile = [];
+  form.aliAppid = "";
+  form.aliPrivateId = "";
+  form.appPublicCertFile = [];
+  form.alipayPublicCertFile = [];
+  form.alipayRootCertFile = [];
+  form.bankName = "";
+  form.bankAccountNo = "";
+
+  if (data.wechatAppId) {
+    form.accountType = "wechat";
+    form.wechatAppid = data.wechatAppId ?? "";
+    form.wechatMchld = data.wechatMchId ?? "";
+    form.wechatApiCertSerialNo = data.merchantSerialNumber ?? "";
+    form.wechatApiV3Key = data.apiV3Key ?? "";
+    form.wechatPayPublicKeyId = data.wechatPayPublicKeyId ?? null;
+    if (data.wechatPrivateKeyFile && data.wechatPrivateKeyName) {
+      const file = base64ToFile(data.wechatPrivateKeyFile, data.wechatPrivateKeyName);
+      form.wechatPrivateKeyFile = [{ name: file.name, raw: file, status: "success", uid: Date.now() } as UploadUserFile];
+    }
+    if (data.wechatPayPublicKeyFile && data.wechatPayPublicKeyFileName) {
+      const f = base64ToFile(data.wechatPayPublicKeyFile, data.wechatPayPublicKeyFileName);
+      form.wechatPayPublicKeyFile = [{ name: f.name, raw: f, status: "success", uid: Date.now() + 1 } as UploadUserFile];
+    }
+  }
+
+  if (data.appId) {
+    form.aliAppid = data.appId ?? "";
+    form.aliPrivateId = data.appSecretCert ?? "";
+    if (data.appPublicCert && data.appPublicCertName) {
+      const file = base64ToFile(data.appPublicCert, data.appPublicCertName);
+      form.appPublicCertFile = [{ name: file.name, raw: file, status: "success", uid: Date.now() } as UploadUserFile];
+    }
+    if (data.alipayPublicCert && data.alipayPublicCertName) {
+      const file = base64ToFile(data.alipayPublicCert, data.alipayPublicCertName);
+      form.alipayPublicCertFile = [{ name: file.name, raw: file, status: "success", uid: Date.now() + 1 } as UploadUserFile];
+    }
+    if (data.alipayRootCert && data.alipayRootCertName) {
+      const file = base64ToFile(data.alipayRootCert, data.alipayRootCertName);
+      form.alipayRootCertFile = [{ name: file.name, raw: file, status: "success", uid: Date.now() + 2 } as UploadUserFile];
+    }
+    if (!data.wechatAppId) form.accountType = "alipay";
+  }
+
+  const bankNo = data.bankAccountNo ?? data.bankCardNo ?? data.settlementAccount;
+  const bankNm = data.bankName ?? data.openingBankName ?? data.openingBank;
+  if (!data.wechatAppId && !data.appId && bankNo) {
+    form.accountType = "bank";
+    form.bankAccountNo = String(bankNo).replace(/\s/g, "");
+    form.bankName = bankNm ? String(bankNm) : "";
+  }
+};
+
+const submit = async () => {
+  if (!formRef.value) return;
+  await formRef.value.validate(async valid => {
+    if (!valid) return;
+
+    const storeUserId = localGet("geeker-user")?.userInfo?.id;
+    if (!storeUserId) {
+      ElMessage.warning("请先登录");
+      return;
+    }
+
+    saving.value = true;
+    try {
+      if (form.accountType === "bank") {
+        const res: any = await saveBankAccountConfig({
+          storeUserId,
+          bankName: form.bankName.trim(),
+          bankCardNo: form.bankAccountNo.replace(/\s/g, "")
+        });
+        if (res?.code === 200 || res?.code === "200") ElMessage.success(res?.msg || "保存成功");
+        else ElMessage.error(res?.msg || "保存失败");
+        return;
+      }
+
+      if (form.accountType === "alipay") {
+        const appPublicCertFile = (form.appPublicCertFile as UploadUserFile[])[0]?.raw as File | undefined;
+        const alipayPublicCertFile = (form.alipayPublicCertFile as UploadUserFile[])[0]?.raw as File | undefined;
+        const alipayRootCertFile = (form.alipayRootCertFile as UploadUserFile[])[0]?.raw as File | undefined;
+
+        if (!appPublicCertFile || !alipayPublicCertFile || !alipayRootCertFile) {
+          ElMessage.warning("请上传应用公钥证书、支付宝公钥证书和支付宝根证书");
+          return;
+        }
+
+        const res: any = await saveAlipayWithFiles(
+          {
+            storeUserId: String(storeUserId),
+            appId: form.aliAppid.trim(),
+            appSecretCert: form.aliPrivateId.trim()
+          },
+          appPublicCertFile,
+          alipayPublicCertFile,
+          alipayRootCertFile
+        );
+        if (res?.code === 200) ElMessage.success("保存成功");
+        else ElMessage.error(res?.msg || "保存失败");
+        return;
+      }
+
+      const privateKeyUpload = (form.wechatPrivateKeyFile as UploadUserFile[])[0];
+      const publicKeyUpload = (form.wechatPayPublicKeyFile as UploadUserFile[])[0];
+      const privateKeyFile = privateKeyUpload?.raw as File | undefined;
+      const publicKeyFile = publicKeyUpload?.raw as File | undefined;
+
+      if (!privateKeyFile || !publicKeyFile) {
+        ElMessage.warning("请上传私钥证书和公钥证书");
+        return;
+      }
+
+      const res: any = await saveWechatWithFiles(
+        {
+          storeUserId: String(storeUserId),
+          apiV3Key: form.wechatApiV3Key || "",
+          merchantSerialNumber: form.wechatApiCertSerialNo || "",
+          wechatAppId: form.wechatAppid || "",
+          wechatMchId: form.wechatMchld || "",
+          wechatMiniAppId: "",
+          wechatPayPublicKeyId: form.wechatPayPublicKeyId || "",
+          accountType: "wechat"
+        },
+        privateKeyFile,
+        publicKeyFile
+      );
+      if (res?.code === 200) ElMessage.success("保存成功");
+      else ElMessage.error(res?.msg || "保存失败");
+    } catch (e: any) {
+      ElMessage.error(e?.message || "保存失败");
+    } finally {
+      saving.value = false;
+    }
+  });
+};
+
+onMounted(async () => {
+  await loadBankNameDict();
+  await fetchPayAccountConfig();
+});
+</script>
+
+<style scoped lang="scss">
+.receiving-account-page {
+  min-height: 100%;
+  padding: 20px;
+  background: #ffffff;
+  .page-title {
+    margin: 0 0 8px;
+    font-size: 18px;
+    font-weight: 600;
+    color: #303133;
+  }
+  .page-tip {
+    margin: 0 0 20px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #909399;
+  }
+  .form-card {
+    max-width: 720px;
+  }
+}
+.form-item-tip {
+  margin-top: 6px;
+  font-size: 12px;
+  line-height: 1.5;
+  color: #909399;
+}
+.cert-upload :deep(.el-upload-dragger) {
+  padding: 20px;
+}
+.upload-inner {
+  color: var(--el-text-color-secondary);
+  text-align: center;
+  p {
+    margin: 8px 0 4px;
+    font-size: 14px;
+  }
+  span {
+    font-size: 12px;
+  }
+}
+</style>