From 7199c6b5cfa6a3eb18035e5c6cf41aeee3aeb5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 1 Apr 2026 10:37:51 +0800 Subject: [PATCH] 1 --- .../src/views/subscription/subscriptions.vue | 10 +- admin/src/views/subscription/users.vue | 121 +++++++++++++++++- api/controller_admin/biz_plan.js | 1 - api/controller_admin/biz_user.js | 35 ++++- api/model/biz_api_token.js | 4 +- api/service/biz_admin_crud.js | 1 - api/service/biz_token_logic.js | 3 +- api/service/biz_usage_service.js | 3 +- app.js | 8 +- config/config.js | 18 +++ 10 files changed, 187 insertions(+), 17 deletions(-) diff --git a/admin/src/views/subscription/subscriptions.vue b/admin/src/views/subscription/subscriptions.vue index a645ad0..f393c53 100644 --- a/admin/src/views/subscription/subscriptions.vue +++ b/admin/src/views/subscription/subscriptions.vue @@ -85,9 +85,9 @@ - +
- + @@ -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 { diff --git a/admin/src/views/subscription/users.vue b/admin/src/views/subscription/users.vue index fee135d..512fc45 100644 --- a/admin/src/views/subscription/users.vue +++ b/admin/src/views/subscription/users.vue @@ -54,11 +54,49 @@
- -

Token 数量:{{ detail.tokenCount }}

+ +

Token 数量:{{ detail.tokenCount }}

+

API Token

+ +

订阅记录

+ + +

点击表格某一行查看该 Token 详情(明文不可查,需重新创建)

+
+

暂无 Token

+ + + +
+

ID{{ selectedToken.id }}

+

名称{{ selectedToken.token_name }}

+

用户{{ selectedToken.user_id }}

+

套餐{{ selectedToken.plan_id != null ? selectedToken.plan_id : '—' }}

+

状态{{ selectedToken.status }}

+

过期时间{{ selectedToken.expire_at || '—' }}

+

最后使用{{ selectedToken.last_used_at || '—' }}

+
+
@@ -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; +} diff --git a/api/controller_admin/biz_plan.js b/api/controller_admin/biz_plan.js index c8c7e05..235d295 100644 --- a/api/controller_admin/biz_plan.js +++ b/api/controller_admin/biz_plan.js @@ -1,5 +1,4 @@ const crud = require("../service/biz_admin_crud"); - const baseModel = require("../../middleware/baseModel"); const audit = require("../service/biz_audit_service"); diff --git a/api/controller_admin/biz_user.js b/api/controller_admin/biz_user.js index 0a559f5..44bbce2 100644 --- a/api/controller_admin/biz_user.js +++ b/api/controller_admin/biz_user.js @@ -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) => { diff --git a/api/model/biz_api_token.js b/api/model/biz_api_token.js index a6df43d..d3aaa4b 100644 --- a/api/model/biz_api_token.js +++ b/api/model/biz_api_token.js @@ -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; }; diff --git a/api/service/biz_admin_crud.js b/api/service/biz_admin_crud.js index d2d679d..187f864 100644 --- a/api/service/biz_admin_crud.js +++ b/api/service/biz_admin_crud.js @@ -192,6 +192,5 @@ module.exports = { detail, all, exportCsv, - getRequestBody, buildSearchWhere, }; diff --git a/api/service/biz_token_logic.js b/api/service/biz_token_logic.js index b496217..c0e1380 100644 --- a/api/service/biz_token_logic.js +++ b/api/service/biz_token_logic.js @@ -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; diff --git a/api/service/biz_usage_service.js b/api/service/biz_usage_service.js index 2ff9ed5..b531ad3 100644 --- a/api/service/biz_usage_service.js +++ b/api/service/biz_usage_service.js @@ -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(); diff --git a/app.js b/app.js index bd67fa9..1c3c596 100644 --- a/app.js +++ b/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 前缀各一套)`); } }); diff --git a/config/config.js b/config/config.js index ff9ad6a..b89aed9 100644 --- a/config/config.js +++ b/config/config.js @@ -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/",