1
This commit is contained in:
@@ -85,9 +85,9 @@
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="upgradeModal" title="升级套餐" :loading="saving" @on-ok="submitUpgrade">
|
||||
<Modal v-model="upgradeModal" title="编辑订阅" :loading="saving" @on-ok="submitUpgrade">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="新套餐">
|
||||
<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>
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
render: (h, p) =>
|
||||
h('div', [
|
||||
h('Button', { props: { size: 'small' }, on: { click: () => this.openRenew(p.row) } }, '续费'),
|
||||
h('Button', { props: { size: 'small' }, class: { ml8: true }, on: { click: () => this.openUpgrade(p.row) } }, '升级'),
|
||||
h('Button', { props: { size: 'small' }, class: { ml8: true }, on: { click: () => this.openUpgrade(p.row) } }, '编辑'),
|
||||
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doCancel(p.row) } }, '取消'),
|
||||
]),
|
||||
},
|
||||
@@ -268,7 +268,7 @@ export default {
|
||||
if (!this.currentRow) return false
|
||||
const np = this.upgradeForm.new_plan_id
|
||||
if (np === undefined || np === null || np === '') {
|
||||
this.$Message.warning('请选择新套餐')
|
||||
this.$Message.warning('请选择套餐')
|
||||
return false
|
||||
}
|
||||
this.saving = true
|
||||
@@ -289,7 +289,7 @@ export default {
|
||||
end_time: this.upgradeForm.end_time || undefined,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已升级')
|
||||
this.$Message.success('已保存')
|
||||
this.upgradeModal = false
|
||||
this.load(1)
|
||||
} else {
|
||||
|
||||
@@ -54,11 +54,49 @@
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="detailVisible" title="用户详情" width="720" footer-hide>
|
||||
<p v-if="detail">Token 数量:{{ detail.tokenCount }}</p>
|
||||
<Modal v-model="detailVisible" title="用户详情" width="800" footer-hide>
|
||||
<p v-if="detail" class="mb8">Token 数量:{{ detail.tokenCount }}</p>
|
||||
<p v-if="detail && detail.tokens && detail.tokens.length" class="sub-title">API Token</p>
|
||||
<Table
|
||||
v-if="detail && detail.tokens && detail.tokens.length"
|
||||
:columns="tokenCols"
|
||||
:data="detail.tokens"
|
||||
size="small"
|
||||
border
|
||||
highlight-row
|
||||
class="mb16"
|
||||
@on-row-click="onTokenRowClick"
|
||||
/>
|
||||
<p v-if="detail && detail.subscriptions && detail.subscriptions.length" class="sub-title">订阅记录</p>
|
||||
<Table v-if="detail && detail.subscriptions" :columns="subCols" :data="detail.subscriptions" size="small"
|
||||
border />
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="tokenListVisible" :title="tokenListTitle" width="820" footer-hide>
|
||||
<p v-if="tokenListRows.length" class="text-muted mb8">点击表格某一行查看该 Token 详情(明文不可查,需重新创建)</p>
|
||||
<Table
|
||||
v-if="tokenListRows.length"
|
||||
:columns="tokenCols"
|
||||
:data="tokenListRows"
|
||||
size="small"
|
||||
border
|
||||
highlight-row
|
||||
@on-row-click="onTokenRowClick"
|
||||
/>
|
||||
<p v-else class="text-muted">暂无 Token</p>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="tokenDetailVisible" title="Token 详情" width="520" footer-hide>
|
||||
<div v-if="selectedToken" class="token-detail">
|
||||
<p><span class="label">ID</span>{{ selectedToken.id }}</p>
|
||||
<p><span class="label">名称</span>{{ selectedToken.token_name }}</p>
|
||||
<p><span class="label">用户</span>{{ selectedToken.user_id }}</p>
|
||||
<p><span class="label">套餐</span>{{ selectedToken.plan_id != null ? selectedToken.plan_id : '—' }}</p>
|
||||
<p><span class="label">状态</span>{{ selectedToken.status }}</p>
|
||||
<p><span class="label">过期时间</span>{{ selectedToken.expire_at || '—' }}</p>
|
||||
<p><span class="label">最后使用</span>{{ selectedToken.last_used_at || '—' }}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -85,6 +123,11 @@ export default {
|
||||
},
|
||||
detailVisible: false,
|
||||
detail: null,
|
||||
tokenListVisible: false,
|
||||
tokenListRows: [],
|
||||
tokenListUserName: '',
|
||||
tokenDetailVisible: false,
|
||||
selectedToken: null,
|
||||
subCols: [
|
||||
{ title: 'ID', key: 'id', width: 80 },
|
||||
{ title: '套餐ID', key: 'plan_id', width: 90 },
|
||||
@@ -102,6 +145,25 @@ export default {
|
||||
{ title: '手机', key: 'mobile', width: 130 },
|
||||
{ title: '公司', key: 'company_name', minWidth: 140 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{
|
||||
title: 'API Token',
|
||||
key: 'token_count',
|
||||
width: 130,
|
||||
render: (h, p) => {
|
||||
const n = p.row.token_count != null ? Number(p.row.token_count) : 0
|
||||
return h('div', [
|
||||
h('span', { class: { mr8: true } }, String(n)),
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'primary', size: 'small', ghost: true },
|
||||
on: { click: () => this.openTokenList(p.row) },
|
||||
},
|
||||
'查看'
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'a',
|
||||
@@ -167,6 +229,20 @@ export default {
|
||||
},
|
||||
]
|
||||
},
|
||||
tokenCols() {
|
||||
return [
|
||||
{ title: 'ID', key: 'id', width: 72 },
|
||||
{ title: '名称', key: 'token_name', minWidth: 100 },
|
||||
{ title: '套餐', key: 'plan_id', width: 80 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '过期', key: 'expire_at', minWidth: 150 },
|
||||
{ title: '最后使用', key: 'last_used_at', minWidth: 150 },
|
||||
]
|
||||
},
|
||||
tokenListTitle() {
|
||||
const name = this.tokenListUserName || ''
|
||||
return name ? `Token 列表 — ${name}` : 'Token 列表'
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load(1)
|
||||
@@ -230,6 +306,20 @@ export default {
|
||||
this.$Message.error((res && res.message) || '加载详情失败')
|
||||
}
|
||||
},
|
||||
async openTokenList(row) {
|
||||
const res = await userServer.detail(row.id)
|
||||
if (res && res.code === 0) {
|
||||
this.tokenListRows = res.data.tokens || []
|
||||
this.tokenListUserName = row.name || String(row.id)
|
||||
this.tokenListVisible = true
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '加载 Token 失败')
|
||||
}
|
||||
},
|
||||
onTokenRowClick(row) {
|
||||
this.selectedToken = row
|
||||
this.tokenDetailVisible = true
|
||||
},
|
||||
doDisable(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '禁用用户',
|
||||
@@ -340,4 +430,31 @@ export default {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mb16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-weight: 600;
|
||||
margin: 10px 0 8px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #808695;
|
||||
}
|
||||
|
||||
.mr8 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.token-detail .label {
|
||||
display: inline-block;
|
||||
width: 88px;
|
||||
color: #808695;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const crud = require("../service/biz_admin_crud");
|
||||
|
||||
const baseModel = require("../../middleware/baseModel");
|
||||
const audit = require("../service/biz_audit_service");
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const Sequelize = require("sequelize");
|
||||
const crud = require("../service/biz_admin_crud");
|
||||
|
||||
const baseModel = require("../../middleware/baseModel");
|
||||
@@ -7,8 +8,31 @@ const audit = require("../service/biz_audit_service");
|
||||
module.exports = {
|
||||
"POST /biz_user/page": async (ctx) => {
|
||||
const body = ctx.getBody();
|
||||
const data = await crud.page("biz_user", body);
|
||||
ctx.success({ rows: data.rows, count: data.count });
|
||||
const param = body.param || body;
|
||||
const pageOption = param.pageOption || {};
|
||||
const seachOption = param.seachOption || {};
|
||||
const pageNum = parseInt(pageOption.page, 10) || 1;
|
||||
const pageSize = parseInt(pageOption.pageSize, 10) || 20;
|
||||
const offset = (pageNum - 1) * pageSize;
|
||||
const model = baseModel.biz_user;
|
||||
const where = crud.buildSearchWhere(model, seachOption);
|
||||
const { count, rows } = await model.findAndCountAll({
|
||||
where,
|
||||
offset,
|
||||
limit: pageSize,
|
||||
order: [["id", "DESC"]],
|
||||
attributes: {
|
||||
include: [
|
||||
[
|
||||
Sequelize.literal(
|
||||
`(SELECT COUNT(*) FROM biz_api_tokens WHERE biz_api_tokens.user_id = biz_user.id)`
|
||||
),
|
||||
"token_count",
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
ctx.success({ rows, count });
|
||||
},
|
||||
"POST /biz_user/add": async (ctx) => {
|
||||
const body = ctx.getBody();
|
||||
@@ -62,10 +86,17 @@ module.exports = {
|
||||
const tokenCount = await baseModel.biz_api_token.count({
|
||||
where: { user_id: id },
|
||||
});
|
||||
const tokens = await baseModel.biz_api_token.findAll({
|
||||
where: { user_id: id },
|
||||
order: [["id", "DESC"]],
|
||||
limit: 200,
|
||||
attributes: ["id", "user_id", "plan_id", "token_name", "status", "expire_at", "last_used_at"],
|
||||
});
|
||||
ctx.success({
|
||||
user,
|
||||
subscriptions,
|
||||
tokenCount,
|
||||
tokens,
|
||||
});
|
||||
},
|
||||
"GET /biz_user/all": async (ctx) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = (db) => {
|
||||
const biz_api_token = db.define(
|
||||
"biz_api_token",
|
||||
{
|
||||
|
||||
|
||||
user_id: {
|
||||
type: Sequelize.BIGINT.UNSIGNED,
|
||||
allowNull: false,
|
||||
@@ -39,6 +39,6 @@ module.exports = (db) => {
|
||||
comment: "API Token",
|
||||
}
|
||||
);
|
||||
// biz_api_token.sync({ alter: true });
|
||||
// biz_api_token.sync({ force: true });
|
||||
return biz_api_token;
|
||||
};
|
||||
|
||||
@@ -192,6 +192,5 @@ module.exports = {
|
||||
detail,
|
||||
all,
|
||||
exportCsv,
|
||||
getRequestBody,
|
||||
buildSearchWhere,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const crypto = require("crypto");
|
||||
const Sequelize = require("sequelize");
|
||||
const op = Sequelize.Op;
|
||||
const baseModel = require("../../middleware/baseModel");
|
||||
const { op } = baseModel;
|
||||
|
||||
const MAX_TOKENS_PER_USER = 5;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const Sequelize = require("sequelize");
|
||||
const op = Sequelize.Op;
|
||||
const baseModel = require("../../middleware/baseModel");
|
||||
const { op } = baseModel;
|
||||
|
||||
function currentStatMonth(d = new Date()) {
|
||||
const y = d.getFullYear();
|
||||
|
||||
8
app.js
8
app.js
@@ -27,8 +27,12 @@ async function start() {
|
||||
const { buildProxyRoutes } = require('./api/controller_custom/proxy_api');
|
||||
// 从 swagger.json 动态注册 193 个转发路由到 /api 前缀
|
||||
const proxyRoutes = buildProxyRoutes();
|
||||
framework.addRoutes('/api', proxyRoutes);
|
||||
console.log(`📡 已注册 ${Object.keys(proxyRoutes).length} 个 API 转发路由`);
|
||||
const n = Object.keys(proxyRoutes).length;
|
||||
// 与 swagger 文档一致:/admin/...、/login/...
|
||||
framework.addRoutes("", proxyRoutes);
|
||||
// 兼容历史调用:/api/admin/...
|
||||
framework.addRoutes("/api", proxyRoutes);
|
||||
console.log(`📡 已注册 ${n} 个转发接口(文档路径 + /api 前缀各一套)`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,24 @@ const baseConfig = {
|
||||
"api/swagger.json",
|
||||
"/api/auth/verify",
|
||||
// 转发层路由白名单(框架不鉴权,由控制器内部做 Token 鉴权)
|
||||
// 与 swagger 一致的无 /api 前缀路径
|
||||
"/admin/",
|
||||
"/applet/",
|
||||
"/equipment/",
|
||||
"/favor/",
|
||||
"/finder/",
|
||||
"/friend/",
|
||||
"/group/",
|
||||
"/label/",
|
||||
"/login/",
|
||||
"/message/",
|
||||
"/other/",
|
||||
"/pay/",
|
||||
"/qy/",
|
||||
"/shop/",
|
||||
"/sns/",
|
||||
"/user/",
|
||||
"/ws/",
|
||||
"/api/admin/",
|
||||
"/api/applet/",
|
||||
"/api/equipment/",
|
||||
|
||||
Reference in New Issue
Block a user