feat(tenant): per-tenant brand and question limit overrides

Add nullable brand_limit / question_limit columns on tenants so ops
admins can override the plan-derived brand-library quotas per account.
When set, these take precedence over config.yml plan defaults across the
brand-library summary, question materialization, and the daily
monitoring worker's primary-client plan resolution.

Ops side exposes them on admin-user create plus a new
PUT /admin-users/:id/limits endpoint, invalidating the brand-library
summary cache on change. The ops-web AdminUsers view gains matching
inputs on the create and edit modals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 09:04:06 +08:00
parent d69510274c
commit 348f2e3451
16 changed files with 413 additions and 63 deletions
+57 -1
View File
@@ -201,6 +201,24 @@
<a-form-item label="初始套餐" required>
<a-select v-model:value="createForm.plan_code" :options="planOptions" />
</a-form-item>
<a-form-item label="品牌数量上限">
<a-input-number
v-model:value="createForm.brand_limit"
class="admin-users-limit-input"
:min="1"
:precision="0"
placeholder="留空使用 config.yml 默认值"
/>
</a-form-item>
<a-form-item label="问题数量上限">
<a-input-number
v-model:value="createForm.question_limit"
class="admin-users-limit-input"
:min="1"
:precision="0"
placeholder="留空使用 config.yml 默认值"
/>
</a-form-item>
</a-form>
</a-modal>
@@ -240,6 +258,24 @@
:allow-clear="false"
/>
</a-form-item>
<a-form-item label="品牌数量上限">
<a-input-number
v-model:value="editForm.brand_limit"
class="admin-users-limit-input"
:min="1"
:precision="0"
placeholder="留空使用 config.yml 默认值"
/>
</a-form-item>
<a-form-item label="问题数量上限">
<a-input-number
v-model:value="editForm.question_limit"
class="admin-users-limit-input"
:min="1"
:precision="0"
placeholder="留空使用 config.yml 默认值"
/>
</a-form-item>
</a-form>
</a-modal>
@@ -320,6 +356,8 @@ interface AdminUserRow {
plan_name: string | null
subscription_status: string | null
subscription_end_at: string | null
brand_limit: number | null
question_limit: number | null
kol_profile_id: number | null
kol_display_name: string | null
kol_status: string | null
@@ -481,6 +519,8 @@ const createForm = reactive({
password: '',
role: 'tenant_admin',
plan_code: '',
brand_limit: null as number | null,
question_limit: null as number | null,
})
function resetCreate() {
@@ -490,6 +530,8 @@ function resetCreate() {
createForm.password = ''
createForm.role = 'tenant_admin'
createForm.plan_code = defaultPlanCode()
createForm.brand_limit = null
createForm.question_limit = null
createOpen.value = false
}
@@ -514,13 +556,15 @@ async function submitCreate() {
}
createLoading.value = true
try {
await http.post('/admin-users', {
await http.post<AdminUserRow>('/admin-users', {
phone,
name: createForm.name.trim(),
email: createForm.email.trim(),
password: createForm.password,
role: createForm.role,
plan_code: createForm.plan_code,
brand_limit: createForm.brand_limit,
question_limit: createForm.question_limit,
})
message.success('新建成功')
resetCreate()
@@ -542,6 +586,8 @@ const editForm = reactive({
role: 'tenant_admin',
plan_code: '',
subscription_end_date: '',
brand_limit: null as number | null,
question_limit: null as number | null,
})
function openEdit(row: AdminUserRow) {
@@ -551,6 +597,8 @@ function openEdit(row: AdminUserRow) {
editForm.email = row.email ?? ''
editForm.role = row.tenant_role ?? 'tenant_admin'
editForm.plan_code = row.plan_code ?? defaultPlanCode()
editForm.brand_limit = row.brand_limit
editForm.question_limit = row.question_limit
editForm.subscription_end_date = row.subscription_end_at
? dayjs(row.subscription_end_at).format('YYYY-MM-DD')
: estimatePlanEndDate(editForm.plan_code)
@@ -604,6 +652,10 @@ async function submitEdit() {
end_at: subscriptionEndAt,
})
}
await http.put(`/admin-users/${editTarget.value.id}/limits`, {
brand_limit: editForm.brand_limit,
question_limit: editForm.question_limit,
})
message.success('用户设置已更新')
editOpen.value = false
void reload()
@@ -818,6 +870,10 @@ onMounted(() => {
width: 100%;
}
.admin-users-limit-input {
width: 100%;
}
.admin-users-toolbar__spacer {
flex: 1;
}