|
|
@@ -1,10 +1,17 @@
|
|
|
<template>
|
|
|
<div class="app-container">
|
|
|
+ <!-- ★ 文案分两种身份(2026-09-30):录入商户号已经改成**超管代录**,
|
|
|
+ 租户看不到新增/编辑/删除按钮,所以对他们说「请联系平台运营」才是能照做的指引;
|
|
|
+ 只留超管那句会让租户在页面上找一个不存在的按钮。 -->
|
|
|
<el-alert
|
|
|
type="info"
|
|
|
:closable="false"
|
|
|
style="margin-bottom: 12px"
|
|
|
- title="手工登记商户号。收款前必须先在这里登记一行 —— 下单时提示「未配置商户号」就是这里还没登记。企业 ID 留空 = 整个租户共用这个号;填了 = 只有这家企业用。"
|
|
|
+ :title="
|
|
|
+ isSuperAdmin
|
|
|
+ ? '手工登记商户号。收款前必须先在这里登记一行 —— 下单时提示「未配置商户号」就是这里还没登记。新增时要选择归属租户;企业 ID 留空 = 整个租户共用这个号;填了 = 只有这家企业用。'
|
|
|
+ : '这里显示的是本租户已登记的商户号(只读)。商户号由平台运营录入,如需新增或修改请联系平台运营。'
|
|
|
+ "
|
|
|
/>
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
@@ -20,13 +27,20 @@
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
|
|
- <el-button type="success" @click="openForm(null)">新增</el-button>
|
|
|
+ <!-- 判据是 is_superuser(与后端 @perm.hasRole('SUPERUSER') 同一个来源),
|
|
|
+ **不是** v-hasPerm —— 后者判的是角色/菜单授权,与端点实际校验的身份不是一回事。
|
|
|
+ 见后端 LeshouyingMerchantController 的类 javadoc。 -->
|
|
|
+ <el-button v-if="isSuperAdmin" type="success" @click="openForm(null)">新增</el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
</el-card>
|
|
|
|
|
|
<el-card shadow="never" style="margin-top: 12px">
|
|
|
<el-table v-loading="loading" :data="list" border stripe>
|
|
|
+ <!-- 租户列只对超管有意义:非超管的查询被后端按租户钉死,整列会是同一个值 -->
|
|
|
+ <el-table-column v-if="isSuperAdmin" label="租户" min-width="140">
|
|
|
+ <template #default="{ row }">{{ tenantName(row.tenant_id) }}</template>
|
|
|
+ </el-table-column>
|
|
|
<!-- ★ 留空的 enterprise_id 必须显示成「租户级」而不是空白格:
|
|
|
空单元格与「这一列没数据」无法区分,管理员看不出哪一行是租户级兜底映射。 -->
|
|
|
<el-table-column label="企业 ID" min-width="180">
|
|
|
@@ -49,8 +63,15 @@
|
|
|
<el-table-column prop="created_time" label="创建时间" width="180" />
|
|
|
<el-table-column label="操作" width="140" fixed="right">
|
|
|
<template #default="{ row }">
|
|
|
- <el-button link type="primary" @click="openForm(row)">编辑</el-button>
|
|
|
- <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
|
|
+ <!-- 两个按钮都挂 isSuperAdmin:后端 save/delete 已经只认超管身份,
|
|
|
+ 非超管点下去必 403 —— 显示一个点了必然失败的入口比不显示更糟。 -->
|
|
|
+ <el-button v-if="isSuperAdmin" link type="primary" @click="openForm(row)">
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isSuperAdmin" link type="danger" @click="handleDelete(row)">
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ <span v-if="!isSuperAdmin" class="readonly-hint">只读</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
@@ -68,6 +89,27 @@
|
|
|
|
|
|
<el-dialog v-model="formVisible" :title="form.id ? '编辑商户号' : '登记商户号'" width="560px">
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
|
|
+ <!-- ★ 归属(2026-09-30):新增时**必填** —— 超管 bypass 了租户拦截器,
|
|
|
+ 不选租户的后果不是报错,而是这一行静默落到超管自己那个租户上,
|
|
|
+ 而它决定钱进哪个乐收赢商户号。
|
|
|
+ ★ 编辑态置灰:归属改不了(后端编辑分支的 SET 列表里没有 tenant_id,
|
|
|
+ 改归属等于让同一笔业务横跨两个租户)。置灰是第二道,不是唯一那道。 -->
|
|
|
+ <el-form-item label="租户" prop="tenant_id">
|
|
|
+ <el-select
|
|
|
+ v-model="form.tenant_id"
|
|
|
+ placeholder="选择该商户号归属的租户"
|
|
|
+ filterable
|
|
|
+ :disabled="!!form.id"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="t in tenantList"
|
|
|
+ :key="t.id"
|
|
|
+ :label="t.name || String(t.id)"
|
|
|
+ :value="t.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
<!-- ★ 选填(2026-09-22 去企业化):留空 ⇒ 这一行是**租户级**映射,
|
|
|
下单时不带 enterprise_id 的请求都会落到它上面。 -->
|
|
|
<el-form-item label="企业 ID" prop="enterprise_id">
|
|
|
@@ -108,13 +150,45 @@ defineOptions({
|
|
|
inheritAttrs: false,
|
|
|
});
|
|
|
|
|
|
-import { onMounted, reactive, ref } from "vue";
|
|
|
-import { ElMessageBox } from "element-plus";
|
|
|
+import { computed, onMounted, reactive, ref } from "vue";
|
|
|
+import { ElMessageBox, type FormRules } from "element-plus";
|
|
|
import {
|
|
|
getLsyMerchantPage,
|
|
|
saveLsyMerchant,
|
|
|
deleteLsyMerchant,
|
|
|
} from "@/api/module_payment/collection";
|
|
|
+import TenantAPI, { type TenantTable } from "@/api/module_system/tenant";
|
|
|
+import { useUserStore } from "@/store";
|
|
|
+
|
|
|
+const userStore = useUserStore();
|
|
|
+
|
|
|
+/**
|
|
|
+ * 超管判据 = `sys_user.is_superuser`,与后端 `@perm.hasRole('SUPERUSER')` 同一个来源。
|
|
|
+ *
|
|
|
+ * ★ 不要换成 `v-hasPerm`:那读的是角色/菜单权限串,与端点实际校验的**身份**不是一回事
|
|
|
+ * (后端 save/delete 已从 `@perm.hasAny('module_payment:leshouying:merchant:list')`
|
|
|
+ * 改成 `@perm.hasRole('SUPERUSER')`,权限串对这两个端点不再起作用)。
|
|
|
+ * 同款判据与注释见汇安融商户页(`module_payment/har/merchant/index.vue`)。
|
|
|
+ */
|
|
|
+const isSuperAdmin = computed(() => userStore.basicInfo.is_superuser === true);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 租户下拉数据。只有超管会用到,非超管不请求(那个接口对他们也未必有权限)。
|
|
|
+ *
|
|
|
+ * ★ 类型把 `id` 收窄成**必有**(`TenantTable.id` 是可选的 `number | undefined`):
|
|
|
+ * `el-option` 的 `value` 不收 `undefined`(vue-tsc 报 TS2322),
|
|
|
+ * 而 `sys_tenant.id` 是主键、本来就不可能为空 —— 收窄不为绕过类型检查,
|
|
|
+ * 是让「下拉里出现一个选不中的租户」在赋值那一刻就不成立(见下面 onMounted 里的 filter)。
|
|
|
+ */
|
|
|
+const tenantList = ref<(TenantTable & { id: number })[]>([]);
|
|
|
+
|
|
|
+const tenantName = (id?: number) => {
|
|
|
+ if (id === undefined || id === null) {
|
|
|
+ return "-";
|
|
|
+ }
|
|
|
+ const t = tenantList.value.find((x) => x.id === id);
|
|
|
+ return t ? t.name || String(id) : String(id);
|
|
|
+};
|
|
|
|
|
|
/** 查询参数 camelCase / 响应与请求体字段 snake_case —— 见 `api/module_payment/collection.ts` 文件头 ★ */
|
|
|
const query = reactive({ pageNo: 1, pageSize: 20, keyword: "" });
|
|
|
@@ -125,11 +199,17 @@ const formVisible = ref(false);
|
|
|
const formRef = ref();
|
|
|
|
|
|
/**
|
|
|
- * 表单**只认**这 6 个键(`id` + 5 个业务字段)。其余字段(含服务端将来新增的)一律不得进入表单模型 ——
|
|
|
+ * 表单**只认**这 7 个键(`id` + `tenant_id` + 5 个业务字段)。其余字段(含服务端将来新增的)一律不得进入表单模型 ——
|
|
|
* `buildForm` 是按这张白名单**逐键取值**的,不是把服务端整行摊进来。
|
|
|
+ *
|
|
|
+ * ★ `tenant_id` 是 2026-09-30 加进来的(超管代录要选归属)。加了它就会**在编辑态被回传**,
|
|
|
+ * 而后端编辑分支一律忽略这个值(改归属在结构上不成立,见 `LsyMerchantService#save` 的
|
|
|
+ * javadoc)—— 那不是「多传了一个没用的键」:不放进白名单的话,`buildForm` 会把它丢掉,
|
|
|
+ * 下拉框在编辑态就是空的。
|
|
|
*/
|
|
|
const FORM_FIELDS = [
|
|
|
"id",
|
|
|
+ "tenant_id",
|
|
|
"enterprise_id",
|
|
|
"mch_no",
|
|
|
"shop_no",
|
|
|
@@ -152,6 +232,10 @@ const FORM_FIELDS = [
|
|
|
function emptyForm(): Record<string, any> {
|
|
|
return {
|
|
|
id: null,
|
|
|
+ // 同样给 null 而不是 "":后端判的是 `tenantId == null`("请选择租户"),
|
|
|
+ // 空串会绕过那道判断并以 "" 进 INSERT —— 而 tenant_id 是 bigint,""
|
|
|
+ // 在 PG 上直接是类型错误(报错位置离现场很远)。
|
|
|
+ tenant_id: null,
|
|
|
enterprise_id: null,
|
|
|
mch_no: "",
|
|
|
shop_no: null,
|
|
|
@@ -195,9 +279,16 @@ const form = ref<Record<string, any>>(emptyForm());
|
|
|
// ★ `enterprise_id` 没有规则 —— 它是选填的(留空 = 租户级)。
|
|
|
// 不要加回 required:那会把「配租户级映射」这条正常路径堵死在表单校验上,
|
|
|
// 而服务端早就不认这个前提了(LsyMerchantService.save 只校验 mch_no)。
|
|
|
-const rules = {
|
|
|
- mch_no: [{ required: true, message: "商户号必填", trigger: "blur" }],
|
|
|
-};
|
|
|
+//
|
|
|
+// ★ `tenant_id` 只在**新增**时必填(与后端 `save` 的新增分支同一条件)。
|
|
|
+// 编辑态不加规则:归属改不了(下拉已置灰),而给旧数据卡一条必填会让超管
|
|
|
+// 连备注都改不了 —— 那道校验挡不住任何东西,只会挡住正常操作。
|
|
|
+const rules = computed<FormRules>(() => ({
|
|
|
+ tenant_id: form.value.id
|
|
|
+ ? []
|
|
|
+ : [{ required: true, message: "请选择租户", trigger: "change" as const }],
|
|
|
+ mch_no: [{ required: true, message: "商户号必填", trigger: "blur" as const }],
|
|
|
+}));
|
|
|
|
|
|
async function loadData() {
|
|
|
loading.value = true;
|
|
|
@@ -249,5 +340,37 @@ async function handleDelete(row: any) {
|
|
|
loadData();
|
|
|
}
|
|
|
|
|
|
-onMounted(loadData);
|
|
|
+onMounted(async () => {
|
|
|
+ loadData();
|
|
|
+ if (isSuperAdmin.value) {
|
|
|
+ try {
|
|
|
+ const res: any = await TenantAPI.listTenant({ page_no: 1, page_size: 100 });
|
|
|
+ // ★ 两个键都读:后端 `PageResult` 的字段名是 `list`,而全局 TS 声明写的是 `items`
|
|
|
+ // (同款实测记在 `har/merchant/index.vue`)—— 只读 `items` 会**静默**拿到空数组,
|
|
|
+ // 表现为超管的租户下拉是空的,于是根本建不了号。
|
|
|
+ const page = res.data.data as
|
|
|
+ | { list?: TenantTable[]; items?: TenantTable[] }
|
|
|
+ | undefined;
|
|
|
+ // ★ filter 掉没有 id 的行:租户 id 是主键,缺它只可能是响应被截断/字段被改名的**已经坏了**的数据,
|
|
|
+ // 而这种行一旦进下拉就是个「选中了却什么也提交不上去」的选项(`tenant_id: undefined`
|
|
|
+ // 会被 JSON.stringify 整个丢掉 ⇒ 后端报「请选择租户」,而用户明明选了)。
|
|
|
+ tenantList.value = (page?.list || page?.items || []).filter(
|
|
|
+ (t): t is TenantTable & { id: number } => typeof t.id === "number"
|
|
|
+ );
|
|
|
+ } catch {
|
|
|
+ // 拉不到租户列表不阻塞列表查询:那一列会退化成显示 tenant_id 数字。
|
|
|
+ // ★ 但新增会因此建不出来(下拉空 ⇒ 必填校验过不去),失败是响亮的(提交时红字提示),
|
|
|
+ // 不是静默写错租户 —— 这个方向是可接受的。
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+/* 非超管的操作列:给一句明确的「只读」,而不是留一个空白的操作格
|
|
|
+ (空白格与「这一行没有操作」无法区分,租户会以为是自己没权限点) */
|
|
|
+.readonly-hint {
|
|
|
+ color: var(--el-text-color-placeholder);
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+</style>
|