From d8eaa16682cf180e9563d6be05fb43057daf6c07 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 12 十月 2026 10:34:14 +0800
Subject: [PATCH] feat(merchant): 后台纳管外部进件商户并支持解绑

---
 talent-card-admin/src/views/MerchantSettlementView.vue |  258 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 256 insertions(+), 2 deletions(-)

diff --git a/talent-card-admin/src/views/MerchantSettlementView.vue b/talent-card-admin/src/views/MerchantSettlementView.vue
index 4ba1a12..8705464 100644
--- a/talent-card-admin/src/views/MerchantSettlementView.vue
+++ b/talent-card-admin/src/views/MerchantSettlementView.vue
@@ -1,8 +1,17 @@
 <script setup lang="ts">
 import { computed, onMounted, reactive, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
-import { Refresh, Search } from '@element-plus/icons-vue'
-import { getMerchantSettlement, getMerchantSettlementMerchantTypes, getMerchantSettlements } from '../api/admin'
+import { Plus, Refresh, Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import {
+  adoptManagedOnboarding,
+  getAdminMerchants,
+  getMerchantSettlement,
+  getMerchantSettlementMerchantTypes,
+  getMerchantSettlements,
+  previewManagedOnboarding,
+} from '../api/admin'
+import { hasAdminPermission } from '../utils/permissions'
 
 type Kind = 'onboardings' | 'bank-cards' | 'settlements'
 
@@ -202,6 +211,108 @@
   }
 }
 
+/**
+ * 纳管外部进件商户。
+ *
+ * 流程刻意分两步:先「核验通道商户」做一次只读查询,把通道侧的商户注册名称、商户简称与主体类型
+ * 展示出来,运营与所选商家核对后**显式勾选确认**才能提交。不做「名称是否对得上」的程序判定——
+ * 我们这边只有一个商家名(常是简称或服务点名),通道那边是工商全称,形态不对应,规则化匹配必然误判。
+ */
+const adoptVisible = ref(false)
+const adoptPreviewing = ref(false)
+const adoptSubmitting = ref(false)
+const adoptConfirmed = ref(false)
+const adoptPreview = ref<Record<string, any>>()
+const adoptForm = reactive({ merchantId: undefined as number | string | undefined, providerHuifuId: '', providerApplyNo: '' })
+const merchantOptions = ref<Array<{ id: number | string; merchantName: string }>>([])
+const merchantLoading = ref(false)
+
+async function searchMerchants(keyword: string) {
+  merchantLoading.value = true
+  try {
+    const result = await getAdminMerchants({ page: 1, size: 20, keyword: keyword?.trim() || undefined })
+    merchantOptions.value = result.records || []
+  } catch {
+    merchantOptions.value = []
+  } finally {
+    merchantLoading.value = false
+  }
+}
+
+function openAdopt() {
+  adoptForm.merchantId = undefined
+  adoptForm.providerHuifuId = ''
+  adoptForm.providerApplyNo = ''
+  adoptPreview.value = undefined
+  adoptConfirmed.value = false
+  adoptVisible.value = true
+  if (merchantOptions.value.length === 0) searchMerchants('')
+}
+
+/** 换了商家或商户号,之前那次核验的对象就不是现在填的了,确认必须重新勾。 */
+function invalidateAdoptPreview() {
+  adoptPreview.value = undefined
+  adoptConfirmed.value = false
+}
+
+async function runAdoptPreview() {
+  if (!adoptForm.merchantId) {
+    ElMessage.warning('请先选择我方商家')
+    return
+  }
+  if (!adoptForm.providerHuifuId.trim()) {
+    ElMessage.warning('请先填写汇付商户号')
+    return
+  }
+  adoptPreviewing.value = true
+  try {
+    adoptPreview.value = await previewManagedOnboarding({
+      merchantId: adoptForm.merchantId,
+      providerHuifuId: adoptForm.providerHuifuId.trim(),
+    })
+    adoptConfirmed.value = false
+  } catch (error: any) {
+    adoptPreview.value = undefined
+    adoptConfirmed.value = false
+    ElMessage.error(error?.message || '核验通道商户失败,请确认商户号后重试')
+  } finally {
+    adoptPreviewing.value = false
+  }
+}
+
+async function submitAdopt() {
+  if (!adoptPreview.value) {
+    ElMessage.warning('请先核验通道商户')
+    return
+  }
+  if (!adoptForm.providerApplyNo.trim()) {
+    ElMessage.warning('请填写汇付侧的入驻申请单号')
+    return
+  }
+  if (!adoptConfirmed.value) {
+    ElMessage.warning('请先核对通道商户名称并勾选确认')
+    return
+  }
+  adoptSubmitting.value = true
+  try {
+    const result = await adoptManagedOnboarding({
+      merchantId: adoptForm.merchantId as number | string,
+      providerHuifuId: adoptForm.providerHuifuId.trim(),
+      providerApplyNo: adoptForm.providerApplyNo.trim(),
+      confirmed: true,
+    })
+    adoptVisible.value = false
+    ElMessage.success('纳管成功,已触发一次进度刷新')
+    await load()
+    // 纳管产出的就是一条进件记录,直接进详情看分阶段进度与待办理项。
+    if (result?.id) router.push({ name: 'merchant-onboarding-detail', params: { id: String(result.id) } })
+  } catch (error: any) {
+    ElMessage.error(error?.message || '纳管失败,请稍后重试')
+  } finally {
+    adoptSubmitting.value = false
+  }
+}
+
 watch(() => route.query.kind, () => {
   const next = routeKind()
   // changeKind 自己写回地址栏也会走到这里,那一次已经查过了,别重复请求。
@@ -265,6 +376,18 @@
           <el-button :icon="Refresh" @click="reset">重置</el-button>
         </el-form-item>
       </el-form>
+      <!-- 纳管入口只放在「商户进件」页签:纳管产出的就是一条进件记录,这个页签正是进件记录的列表,
+           纳管结果当场可见。位置沿用「商家查询」页「新建」按钮的既有习惯。 -->
+      <div v-if="kind === 'onboardings'" class="settlement-table-tools">
+        <el-button
+          v-if="hasAdminPermission('admin.merchant-settlement.edit')"
+          type="primary"
+          :icon="Plus"
+          @click="openAdopt"
+        >
+          纳管外部进件商户
+        </el-button>
+      </div>
     </section>
 
     <section class="table-panel">
@@ -289,6 +412,12 @@
           <el-table-column prop="merchantType" label="商家类型" min-width="110" />
           <el-table-column prop="subjectName" label="进件主体" min-width="150" show-overflow-tooltip />
           <el-table-column label="主体类型" min-width="105"><template #default="{ row }">{{ subjectTypeLabel(row.subjectType) }}</template></el-table-column>
+          <!-- 来源标记:区分两条进件路径,纳管进来的要一眼看出来。 -->
+          <el-table-column label="来源" min-width="100" align="center">
+            <template #default="{ row }">
+              <el-tag :type="row.source === 'managed' ? 'success' : 'info'" effect="plain">{{ row.source === 'managed' ? '纳管' : '自助进件' }}</el-tag>
+            </template>
+          </el-table-column>
         </template>
         <template v-else>
           <el-table-column prop="id" label="申请id" min-width="100" />
@@ -336,6 +465,125 @@
         />
       </div>
     </section>
+
+    <el-dialog
+      v-model="adoptVisible"
+      title="纳管外部进件商户"
+      width="640px"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <p class="adopt-hint">
+        用于把<b>已在汇付侧完成进件</b>的商户接到我方某个商家上。纳管全程只查询、不下发:不会补开微信、
+        不会绑定人才端、不会改动支付场景与延迟入账。
+      </p>
+      <el-form label-width="120px" class="adopt-form">
+        <el-form-item label="我方商家" required>
+          <el-select
+            v-model="adoptForm.merchantId"
+            filterable
+            remote
+            reserve-keyword
+            clearable
+            placeholder="输入商家名称搜索"
+            :remote-method="searchMerchants"
+            :loading="merchantLoading"
+            @change="invalidateAdoptPreview"
+          >
+            <el-option
+              v-for="item in merchantOptions"
+              :key="item.id"
+              :label="item.merchantName"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="汇付商户号" required>
+          <el-input v-model="adoptForm.providerHuifuId" clearable placeholder="斗拱侧该商户的 huifu_id" @input="invalidateAdoptPreview" />
+        </el-form-item>
+        <el-form-item label="入驻申请单号" required>
+          <el-input v-model="adoptForm.providerApplyNo" clearable placeholder="汇付控台入驻申请列表里的申请单号" />
+        </el-form-item>
+        <el-form-item label=" ">
+          <el-button type="primary" plain :loading="adoptPreviewing" @click="runAdoptPreview">核验通道商户</el-button>
+          <span class="adopt-tip">卡号不用填,从通道查回来。</span>
+        </el-form-item>
+      </el-form>
+
+      <template v-if="adoptPreview">
+        <el-descriptions :column="1" border size="small" class="adopt-preview">
+          <el-descriptions-item label="我方商家">
+            {{ adoptPreview.merchantName }}<span class="muted">(门店:{{ adoptPreview.storeName }})</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="通道商户注册名称">{{ adoptPreview.channelMerchantName }}</el-descriptions-item>
+          <el-descriptions-item label="通道商户简称">{{ adoptPreview.channelShortName }}</el-descriptions-item>
+          <el-descriptions-item label="通道主体类型">
+            {{ adoptPreview.channelSubjectType === 'enterprise' ? '企业' : '个人' }}
+            <span v-if="adoptPreview.channelCompanyType" class="muted">({{ adoptPreview.channelCompanyType }})</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="结算银行卡">
+            {{ adoptPreview.settlementCardMasked || '-' }}
+            <span v-if="adoptPreview.settlementBankName" class="muted">({{ adoptPreview.settlementBankName }})</span>
+          </el-descriptions-item>
+        </el-descriptions>
+        <!-- 只提示不拦截:连锁各门店共用一个收款主体是允许的,这里列出已绑的商家让运营自己确认。 -->
+        <el-alert
+          v-if="adoptPreview.alreadyManaged"
+          class="adopt-alert"
+          type="success"
+          :closable="false"
+          title="该商家已纳管过这个汇付商户号"
+          description="重复纳管是幂等的,提交会直接返回原来那条进件记录,不会产生第二条。"
+        />
+        <el-alert
+          v-if="adoptPreview.boundMerchants?.length"
+          class="adopt-alert"
+          type="info"
+          :closable="false"
+          :title="`该汇付商户号已绑给:${adoptPreview.boundMerchants.join('、')}`"
+          description="可以继续纳管(连锁各门店共用一个收款主体)。"
+        />
+        <el-alert
+          v-if="adoptPreview.nameWarning"
+          class="adopt-alert"
+          type="warning"
+          :closable="false"
+          title="通道名称与所选商家名完全没有重合字符"
+          :description="adoptPreview.nameWarning"
+        />
+        <el-alert
+          v-if="adoptPreview.loginNotice"
+          class="adopt-alert"
+          type="warning"
+          :closable="false"
+          title="商户端登录"
+          :description="adoptPreview.loginNotice"
+        />
+        <el-alert
+          v-for="blocker in adoptPreview.blockers || []"
+          :key="blocker"
+          class="adopt-alert"
+          type="error"
+          :closable="false"
+          :title="blocker"
+        />
+        <el-checkbox v-model="adoptConfirmed" :disabled="(adoptPreview.blockers || []).length > 0">
+          我已把通道侧的商户注册名称与简称与所选商家核对一致(纳管后该商家的收款将进入这个收款主体)
+        </el-checkbox>
+      </template>
+
+      <template #footer>
+        <el-button @click="adoptVisible = false">取消</el-button>
+        <el-button
+          type="primary"
+          :loading="adoptSubmitting"
+          :disabled="!adoptPreview || !adoptConfirmed || (adoptPreview.blockers || []).length > 0"
+          @click="submitAdopt"
+        >
+          提交纳管
+        </el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -352,6 +600,12 @@
 .query-form :deep(.el-input), .query-form :deep(.el-select), .query-form :deep(.el-date-editor) { width: 100%; }
 .query-actions { justify-content: flex-end; }
 .query-actions :deep(.el-form-item__content) { justify-content: flex-end; }
+.settlement-table-tools { margin: 0 0 12px; }
+.adopt-hint { margin: 0 0 16px; color: #606266; font-size: 13px; line-height: 20px; }
+.adopt-form :deep(.el-select) { width: 100%; }
+.adopt-tip { margin-left: 12px; color: #909399; font-size: 12px; }
+.adopt-preview { margin-bottom: 14px; }
+.adopt-alert { margin-bottom: 12px; }
 .table-panel { margin-top: 16px; padding: 0 20px 12px; }
 .settlement-table { width: 100%; }
 .settlement-table :deep(th.el-table__cell) { color: #303133; font-weight: 600; background: #f7f8fa; }

--
Gitblit v1.7.1