This commit is contained in:
张成
2026-03-26 10:55:13 +08:00
parent 7f3e6eb943
commit 656ecb6bc9
10 changed files with 23560 additions and 60 deletions

9028
_docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7125
_docs、.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,10 +19,19 @@ class UserServer {
return window.framework.http.get(`/biz_user/detail?id=${encodeURIComponent(id)}`, {}); return window.framework.http.get(`/biz_user/detail?id=${encodeURIComponent(id)}`, {});
} }
/** 下拉:全部业务用户(与 GET /biz_user/all 对应) */
async all() {
return window.framework.http.get('/biz_user/all', {});
}
async disable(row) { async disable(row) {
return window.framework.http.post("/biz_user/disable", row); return window.framework.http.post("/biz_user/disable", row);
} }
async enable(row) {
return window.framework.http.post("/biz_user/enable", row);
}
async exportRows(row) { async exportRows(row) {
return window.framework.http.post("/biz_user/export", row); return window.framework.http.post("/biz_user/export", row);
} }

View File

@@ -0,0 +1,45 @@
import userServer from '@/api/subscription/user_server.js'
import planServer from '@/api/subscription/plan_server.js'
/**
* 业务用户 / 套餐关联下拉:依赖后端 GET /biz_user/all、GET /biz_plan/all
*/
export default {
data() {
return {
bizUserOptions: [],
bizPlanOptions: [],
}
},
methods: {
async loadBizRelationOptions() {
try {
const [u, p] = await Promise.all([userServer.all(), planServer.all()])
if (u && u.code === 0) {
this.bizUserOptions = Array.isArray(u.data) ? u.data : []
} else {
this.bizUserOptions = []
}
if (p && p.code === 0) {
this.bizPlanOptions = Array.isArray(p.data) ? p.data : []
} else {
this.bizPlanOptions = []
}
} catch (e) {
this.bizUserOptions = []
this.bizPlanOptions = []
}
},
bizUserLabel(row) {
if (!row || row.id == null) return ''
const name = row.name || ''
const mobile = row.mobile ? ` ${row.mobile}` : ''
return `#${row.id} ${name}${mobile}`.trim()
},
bizPlanLabel(row) {
if (!row || row.id == null) return ''
const name = row.plan_name || row.plan_code || ''
return `#${row.id} ${name}`.trim()
},
},
}

View File

@@ -6,15 +6,27 @@
</div> </div>
<Form ref="formInline" :model="param.seachOption" inline :label-width="80"> <Form ref="formInline" :model="param.seachOption" inline :label-width="80">
<FormItem label="用户ID"> <FormItem label="筛选">
<Input v-model="param.seachOption.value" style="width: 140px" placeholder="筛选 user_id" class="ml10" /> <Select v-model="param.seachOption.key" style="width: 120px" @on-change="onSearchKeyChange">
</FormItem> <Option value="user_id">用户</Option>
<FormItem>
<Select v-model="param.seachOption.key" style="width: 120px">
<Option value="user_id">用户ID</Option>
<Option value="status">状态</Option> <Option value="status">状态</Option>
</Select> </Select>
</FormItem> </FormItem>
<FormItem v-if="param.seachOption.key === 'user_id'">
<Select
v-model="param.seachOption.value"
filterable
clearable
placeholder="选择用户"
style="width: 260px"
class="ml10"
>
<Option v-for="u in bizUserOptions" :key="u.id" :value="String(u.id)">{{ bizUserLabel(u) }}</Option>
</Select>
</FormItem>
<FormItem v-else>
<Input v-model="param.seachOption.value" style="width: 160px" placeholder="pending / active …" class="ml10" />
</FormItem>
<FormItem> <FormItem>
<Button type="primary" @click="load(1)">查询</Button> <Button type="primary" @click="load(1)">查询</Button>
<Button type="default" @click="resetQuery" class="ml10">重置</Button> <Button type="default" @click="resetQuery" class="ml10">重置</Button>
@@ -26,21 +38,23 @@
<div class="table-body"> <div class="table-body">
<Table :columns="columns" :data="rows" border stripe /> <Table :columns="columns" :data="rows" border stripe />
<div class="table-page-bar"> <div class="table-page-bar">
<Page <Page :total="total" :current="param.pageOption.page" :page-size="param.pageOption.pageSize" show-total
:total="total" @on-change="onPage" @on-page-size-change="onSize" />
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div> </div>
</div> </div>
<Modal v-model="openModal" title="开通订阅" width="640" :loading="saving" @on-ok="submitOpen"> <Modal v-model="openModal" title="开通订阅" width="640" :loading="saving" @on-ok="submitOpen">
<Form :label-width="110"> <Form :label-width="110">
<FormItem label="用户ID"><Input v-model="openForm.user_id" type="number" /></FormItem> <FormItem label="用户">
<FormItem label="套餐ID"><Input v-model="openForm.plan_id" type="number" /></FormItem> <Select v-model="openForm.user_id" filterable clearable placeholder="请选择" style="width: 100%">
<Option v-for="u in bizUserOptions" :key="u.id" :value="u.id">{{ bizUserLabel(u) }}</Option>
</Select>
</FormItem>
<FormItem label="套餐">
<Select v-model="openForm.plan_id" filterable clearable placeholder="请选择" style="width: 100%">
<Option v-for="p in bizPlanOptions" :key="p.id" :value="p.id">{{ bizPlanLabel(p) }}</Option>
</Select>
</FormItem>
<FormItem label="开始时间"><Input v-model="openForm.start_time" placeholder="2025-01-01 00:00:00" /></FormItem> <FormItem label="开始时间"><Input v-model="openForm.start_time" placeholder="2025-01-01 00:00:00" /></FormItem>
<FormItem label="结束时间"><Input v-model="openForm.end_time" placeholder="2025-12-31 23:59:59" /></FormItem> <FormItem label="结束时间"><Input v-model="openForm.end_time" placeholder="2025-12-31 23:59:59" /></FormItem>
<FormItem label="状态"> <FormItem label="状态">
@@ -73,7 +87,11 @@
<Modal v-model="upgradeModal" title="升级套餐" :loading="saving" @on-ok="submitUpgrade"> <Modal v-model="upgradeModal" title="升级套餐" :loading="saving" @on-ok="submitUpgrade">
<Form :label-width="100"> <Form :label-width="100">
<FormItem label="新套餐ID"><Input v-model="upgradeForm.new_plan_id" type="number" /></FormItem> <FormItem label="新套餐">
<Select v-model="upgradeForm.new_plan_id" filterable clearable placeholder="请选择" style="width: 100%">
<Option v-for="p in bizPlanOptions" :key="p.id" :value="p.id">{{ bizPlanLabel(p) }}</Option>
</Select>
</FormItem>
<FormItem label="开始"><Input v-model="upgradeForm.start_time" /></FormItem> <FormItem label="开始"><Input v-model="upgradeForm.start_time" /></FormItem>
<FormItem label="结束"><Input v-model="upgradeForm.end_time" /></FormItem> <FormItem label="结束"><Input v-model="upgradeForm.end_time" /></FormItem>
</Form> </Form>
@@ -84,9 +102,11 @@
<script> <script>
import subscriptionsServer from '@/api/subscription/subscriptions_server.js' import subscriptionsServer from '@/api/subscription/subscriptions_server.js'
import { downloadCsvFromRows } from '@/utils/csvExport.js' import { downloadCsvFromRows } from '@/utils/csvExport.js'
import subscriptionRelations from '@/mixins/subscriptionRelations.js'
export default { export default {
name: 'SubscriptionRecords', name: 'SubscriptionRecords',
mixins: [subscriptionRelations],
data() { data() {
return { return {
rows: [], rows: [],
@@ -102,7 +122,7 @@ export default {
currentRow: null, currentRow: null,
openForm: {}, openForm: {},
renewForm: { end_time: '' }, renewForm: { end_time: '' },
upgradeForm: { new_plan_id: '', start_time: '', end_time: '' }, upgradeForm: { new_plan_id: undefined, start_time: '', end_time: '' },
} }
}, },
computed: { computed: {
@@ -128,10 +148,14 @@ export default {
] ]
}, },
}, },
mounted() { async mounted() {
await this.loadBizRelationOptions()
this.load(1) this.load(1)
}, },
methods: { methods: {
onSearchKeyChange() {
this.param.seachOption.value = ''
},
async load(page) { async load(page) {
if (page) this.param.pageOption.page = page if (page) this.param.pageOption.page = page
const res = await subscriptionsServer.page({ param: this.param }) const res = await subscriptionsServer.page({ param: this.param })
@@ -159,8 +183,8 @@ export default {
d.getHours() d.getHours()
).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:00` ).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:00`
this.openForm = { this.openForm = {
user_id: '', user_id: undefined,
plan_id: '', plan_id: undefined,
start_time: fmt(now), start_time: fmt(now),
end_time: fmt(end), end_time: fmt(end),
status: 'pending', status: 'pending',
@@ -170,12 +194,28 @@ export default {
} }
this.openModal = true this.openModal = true
}, },
async submitOpen() { submitOpen() {
const uid = this.openForm.user_id
const pid = this.openForm.plan_id
if (uid === undefined || uid === null || uid === '') {
this.$Message.warning('请选择用户')
return false
}
if (pid === undefined || pid === null || pid === '') {
this.$Message.warning('请选择套餐')
return false
}
this.saving = true this.saving = true
this._submitOpen()
return false
},
async _submitOpen() {
const uid = this.openForm.user_id
const pid = this.openForm.plan_id
try { try {
const body = { const body = {
user_id: Number(this.openForm.user_id), user_id: Number(uid),
plan_id: Number(this.openForm.plan_id), plan_id: Number(pid),
start_time: this.openForm.start_time, start_time: this.openForm.start_time,
end_time: this.openForm.end_time, end_time: this.openForm.end_time,
status: this.openForm.status, status: this.openForm.status,
@@ -221,16 +261,30 @@ export default {
}, },
openUpgrade(row) { openUpgrade(row) {
this.currentRow = row this.currentRow = row
this.upgradeForm = { new_plan_id: row.plan_id, start_time: '', end_time: '' } this.upgradeForm = { new_plan_id: row.plan_id != null ? Number(row.plan_id) : undefined, start_time: '', end_time: '' }
this.upgradeModal = true this.upgradeModal = true
}, },
async submitUpgrade() { submitUpgrade() {
if (!this.currentRow) return if (!this.currentRow) return false
const np = this.upgradeForm.new_plan_id
if (np === undefined || np === null || np === '') {
this.$Message.warning('请选择新套餐')
return false
}
this.saving = true this.saving = true
this._submitUpgrade()
return false
},
async _submitUpgrade() {
if (!this.currentRow) {
this.saving = false
return
}
const np = this.upgradeForm.new_plan_id
try { try {
const res = await subscriptionsServer.upgrade({ const res = await subscriptionsServer.upgrade({
subscription_id: this.currentRow.id, subscription_id: this.currentRow.id,
new_plan_id: Number(this.upgradeForm.new_plan_id), new_plan_id: Number(np),
start_time: this.upgradeForm.start_time || undefined, start_time: this.upgradeForm.start_time || undefined,
end_time: this.upgradeForm.end_time || undefined, end_time: this.upgradeForm.end_time || undefined,
}) })

View File

@@ -6,15 +6,27 @@
</div> </div>
<Form ref="formInline" :model="param.seachOption" inline :label-width="80"> <Form ref="formInline" :model="param.seachOption" inline :label-width="80">
<FormItem label="用户ID"> <FormItem label="筛选">
<Input v-model="param.seachOption.value" style="width: 140px" class="ml10" /> <Select v-model="param.seachOption.key" style="width: 120px" @on-change="onSearchKeyChange">
</FormItem> <Option value="user_id">用户</Option>
<FormItem>
<Select v-model="param.seachOption.key" style="width: 120px">
<Option value="user_id">用户ID</Option>
<Option value="status">状态</Option> <Option value="status">状态</Option>
</Select> </Select>
</FormItem> </FormItem>
<FormItem v-if="param.seachOption.key === 'user_id'">
<Select
v-model="param.seachOption.value"
filterable
clearable
placeholder="选择用户"
style="width: 260px"
class="ml10"
>
<Option v-for="u in bizUserOptions" :key="u.id" :value="String(u.id)">{{ bizUserLabel(u) }}</Option>
</Select>
</FormItem>
<FormItem v-else>
<Input v-model="param.seachOption.value" style="width: 160px" placeholder="状态" class="ml10" />
</FormItem>
<FormItem> <FormItem>
<Button type="primary" @click="load(1)">查询</Button> <Button type="primary" @click="load(1)">查询</Button>
<Button type="default" @click="resetQuery" class="ml10">重置</Button> <Button type="default" @click="resetQuery" class="ml10">重置</Button>
@@ -39,7 +51,11 @@
<Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate"> <Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate">
<Form :label-width="100"> <Form :label-width="100">
<FormItem label="用户ID"><Input v-model="createForm.user_id" type="number" /></FormItem> <FormItem label="用户">
<Select v-model="createForm.user_id" filterable clearable placeholder="请选择" style="width: 100%">
<Option v-for="u in bizUserOptions" :key="u.id" :value="u.id">{{ bizUserLabel(u) }}</Option>
</Select>
</FormItem>
<FormItem label="名称"><Input v-model="createForm.token_name" placeholder="default" /></FormItem> <FormItem label="名称"><Input v-model="createForm.token_name" placeholder="default" /></FormItem>
<FormItem label="过期时间"><Input v-model="createForm.expire_at" placeholder="2026-12-31 23:59:59" /></FormItem> <FormItem label="过期时间"><Input v-model="createForm.expire_at" placeholder="2026-12-31 23:59:59" /></FormItem>
</Form> </Form>
@@ -58,9 +74,11 @@
<script> <script>
import tokenServer from '@/api/subscription/token_server.js' import tokenServer from '@/api/subscription/token_server.js'
import { downloadCsvFromRows } from '@/utils/csvExport.js' import { downloadCsvFromRows } from '@/utils/csvExport.js'
import subscriptionRelations from '@/mixins/subscriptionRelations.js'
export default { export default {
name: 'SubscriptionTokens', name: 'SubscriptionTokens',
mixins: [subscriptionRelations],
data() { data() {
return { return {
rows: [], rows: [],
@@ -105,10 +123,14 @@ export default {
] ]
}, },
}, },
mounted() { async mounted() {
await this.loadBizRelationOptions()
this.load(1) this.load(1)
}, },
methods: { methods: {
onSearchKeyChange() {
this.param.seachOption.value = ''
},
async load(page) { async load(page) {
if (page) this.param.pageOption.page = page if (page) this.param.pageOption.page = page
const res = await tokenServer.page({ param: this.param }) const res = await tokenServer.page({ param: this.param })
@@ -134,14 +156,24 @@ export default {
2, 2,
'0' '0'
)} 23:59:59` )} 23:59:59`
this.createForm = { user_id: '', token_name: 'default', expire_at: fmt } this.createForm = { user_id: undefined, token_name: 'default', expire_at: fmt }
this.createModal = true this.createModal = true
}, },
async submitCreate() { submitCreate() {
const uid = this.createForm.user_id
if (uid === undefined || uid === null || uid === '') {
this.$Message.warning('请选择用户')
return false
}
this.saving = true this.saving = true
this._submitCreate()
return false
},
async _submitCreate() {
const uid = this.createForm.user_id
try { try {
const res = await tokenServer.create({ const res = await tokenServer.create({
user_id: Number(this.createForm.user_id), user_id: Number(uid),
token_name: this.createForm.token_name || 'default', token_name: this.createForm.token_name || 'default',
expire_at: this.createForm.expire_at, expire_at: this.createForm.expire_at,
}) })

View File

@@ -7,12 +7,37 @@
<Form ref="formInline" :model="param.seachOption" inline :label-width="80" class="usage-query-form"> <Form ref="formInline" :model="param.seachOption" inline :label-width="80" class="usage-query-form">
<FormItem label="条件"> <FormItem label="条件">
<Select v-model="param.seachOption.key" class="usage-select"> <Select v-model="param.seachOption.key" class="usage-select" @on-change="onSearchKeyChange">
<Option value="user_id">用户ID</Option> <Option value="user_id">用户</Option>
<Option value="stat_month">月份</Option> <Option value="stat_month">月份</Option>
<Option value="plan_id">套餐ID</Option> <Option value="plan_id">套餐</Option>
</Select> </Select>
<Input v-model="param.seachOption.value" class="ml10 usage-search-input" placeholder="关键字" /> <Select
v-if="param.seachOption.key === 'user_id'"
v-model="param.seachOption.value"
filterable
clearable
placeholder="选择用户"
class="ml10 usage-search-input"
>
<Option v-for="u in bizUserOptions" :key="u.id" :value="String(u.id)">{{ bizUserLabel(u) }}</Option>
</Select>
<Select
v-else-if="param.seachOption.key === 'plan_id'"
v-model="param.seachOption.value"
filterable
clearable
placeholder="选择套餐"
class="ml10 usage-search-input"
>
<Option v-for="p in bizPlanOptions" :key="p.id" :value="String(p.id)">{{ bizPlanLabel(p) }}</Option>
</Select>
<Input
v-else
v-model="param.seachOption.value"
class="ml10 usage-search-input"
placeholder="YYYY-MM"
/>
</FormItem> </FormItem>
<FormItem> <FormItem>
<Button type="primary" @click="load(1)">查询</Button> <Button type="primary" @click="load(1)">查询</Button>
@@ -38,8 +63,16 @@
<Modal v-model="modal" :title="form.id ? '编辑用量' : '新增用量'" width="640" :loading="saving" @on-ok="save"> <Modal v-model="modal" :title="form.id ? '编辑用量' : '新增用量'" width="640" :loading="saving" @on-ok="save">
<Form ref="formRef" :model="form" :rules="rules" :label-width="120"> <Form ref="formRef" :model="form" :rules="rules" :label-width="120">
<FormItem label="用户ID" prop="user_id"><Input v-model="form.user_id" type="number" /></FormItem> <FormItem label="用户" prop="user_id">
<FormItem label="套餐ID" prop="plan_id"><Input v-model="form.plan_id" type="number" /></FormItem> <Select v-model="form.user_id" filterable clearable placeholder="请选择" style="width: 100%">
<Option v-for="u in bizUserOptions" :key="u.id" :value="u.id">{{ bizUserLabel(u) }}</Option>
</Select>
</FormItem>
<FormItem label="套餐" prop="plan_id">
<Select v-model="form.plan_id" filterable clearable placeholder="请选择" style="width: 100%">
<Option v-for="p in bizPlanOptions" :key="p.id" :value="p.id">{{ bizPlanLabel(p) }}</Option>
</Select>
</FormItem>
<FormItem label="月份 YYYY-MM" prop="stat_month"><Input v-model="form.stat_month" placeholder="2025-03" /></FormItem> <FormItem label="月份 YYYY-MM" prop="stat_month"><Input v-model="form.stat_month" placeholder="2025-03" /></FormItem>
<FormItem label="msg_count"><Input v-model="form.msg_count" type="number" /></FormItem> <FormItem label="msg_count"><Input v-model="form.msg_count" type="number" /></FormItem>
<FormItem label="mass_count"><Input v-model="form.mass_count" type="number" /></FormItem> <FormItem label="mass_count"><Input v-model="form.mass_count" type="number" /></FormItem>
@@ -54,9 +87,11 @@
<script> <script>
import usageServer from '@/api/subscription/usage_server.js' import usageServer from '@/api/subscription/usage_server.js'
import { downloadCsvFromRows } from '@/utils/csvExport.js' import { downloadCsvFromRows } from '@/utils/csvExport.js'
import subscriptionRelations from '@/mixins/subscriptionRelations.js'
export default { export default {
name: 'SubscriptionUsage', name: 'SubscriptionUsage',
mixins: [subscriptionRelations],
data() { data() {
return { return {
rows: [], rows: [],
@@ -69,8 +104,8 @@ export default {
saving: false, saving: false,
form: {}, form: {},
rules: { rules: {
user_id: [{ required: true, message: '必填', trigger: 'blur' }], user_id: [{ required: true, type: 'number', message: '请选择用户', trigger: 'change' }],
plan_id: [{ required: true, message: '必填', trigger: 'blur' }], plan_id: [{ required: true, type: 'number', message: '请选择套餐', trigger: 'change' }],
stat_month: [{ required: true, message: '必填', trigger: 'blur' }], stat_month: [{ required: true, message: '必填', trigger: 'blur' }],
}, },
} }
@@ -100,10 +135,14 @@ export default {
] ]
}, },
}, },
mounted() { async mounted() {
await this.loadBizRelationOptions()
this.load(1) this.load(1)
}, },
methods: { methods: {
onSearchKeyChange() {
this.param.seachOption.value = ''
},
async load(page) { async load(page) {
if (page) this.param.pageOption.page = page if (page) this.param.pageOption.page = page
const res = await usageServer.page({ param: this.param }) const res = await usageServer.page({ param: this.param })
@@ -124,11 +163,15 @@ export default {
}, },
openEdit(row) { openEdit(row) {
if (row) { if (row) {
this.form = { ...row } this.form = {
...row,
user_id: row.user_id != null && row.user_id !== '' ? Number(row.user_id) : undefined,
plan_id: row.plan_id != null && row.plan_id !== '' ? Number(row.plan_id) : undefined,
}
} else { } else {
this.form = { this.form = {
user_id: '', user_id: undefined,
plan_id: '', plan_id: undefined,
stat_month: '', stat_month: '',
msg_count: 0, msg_count: 0,
mass_count: 0, mass_count: 0,

View File

@@ -125,15 +125,25 @@ export default {
}, },
'详情' '详情'
), ),
h( p.row.status === 'disabled'
'Button', ? h(
{ 'Button',
props: { type: 'warning', size: 'small' }, {
class: { ml8: true }, props: { type: 'success', size: 'small' },
on: { click: () => this.doDisable(p.row) }, class: { ml8: true },
}, on: { click: () => this.doEnable(p.row) },
'禁用' },
), '启用'
)
: h(
'Button',
{
props: { type: 'warning', size: 'small' },
class: { ml8: true },
on: { click: () => this.doDisable(p.row) },
},
'禁用'
),
h( h(
'Button', 'Button',
{ {
@@ -235,6 +245,21 @@ export default {
}, },
}) })
}, },
doEnable(row) {
this.$Modal.confirm({
title: '启用用户',
content: '确认重新启用该用户?',
onOk: async () => {
const res = await userServer.enable({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已启用')
this.load(1)
} else {
this.$Message.error((res && res.message) || '操作失败')
}
},
})
},
doDel(row) { doDel(row) {
this.$Modal.confirm({ this.$Modal.confirm({
title: '删除用户', title: '删除用户',

View File

@@ -86,6 +86,20 @@ module.exports = {
}); });
ctx.success({}); ctx.success({});
}, },
"POST /biz_user/enable": async (ctx) => {
const body = getRequestBody(ctx);
const id = body.id;
if (id == null) return ctx.fail("缺少 id");
await baseModel.biz_user.update({ status: "active" }, { where: { id } });
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: id,
action: "biz_user.enable",
resource_type: "biz_user",
resource_id: id,
});
ctx.success({});
},
"POST /biz_user/export": async (ctx) => { "POST /biz_user/export": async (ctx) => {
const body = getRequestBody(ctx); const body = getRequestBody(ctx);
const res = await crud.exportCsv("biz_user", body); const res = await crud.exportCsv("biz_user", body);