From 084c437096c2713c82857bc86d764ae3c2a37a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 1 Apr 2026 14:23:57 +0800 Subject: [PATCH] 1 --- _docs/merge_biz_into_swagger.js | 1 + _docs/sql/biz_api_call_log_response_body.sql | 3 + .../api/subscription/api_call_log_server.js | 11 + admin/src/router/component-map.js | 2 + admin/src/views/subscription/api_call_log.vue | 202 ++++++++++++++++++ api/controller_admin/biz_api_stats.js | 13 ++ api/model/biz_api_call_log.js | 6 +- api/service/biz_proxy_service.js | 32 ++- 8 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 _docs/sql/biz_api_call_log_response_body.sql create mode 100644 admin/src/api/subscription/api_call_log_server.js create mode 100644 admin/src/views/subscription/api_call_log.vue diff --git a/_docs/merge_biz_into_swagger.js b/_docs/merge_biz_into_swagger.js index b7afd18..e9642c3 100644 --- a/_docs/merge_biz_into_swagger.js +++ b/_docs/merge_biz_into_swagger.js @@ -274,6 +274,7 @@ const definitions = { const paths = {}; paths["/admin_api/biz_api_call_log/page"] = post("API 调用明细分页", "BizAdminPageRequest"); +paths["/admin_api/biz_api_call_log/export"] = post("API 调用明细导出", "BizAdminPageRequest"); paths["/admin_api/biz_api_stats/by_user"] = post("按用户统计接口调用", "BizApiStatsByUserRequest"); paths["/admin_api/biz_api_stats/by_api"] = post("按接口路径统计调用", "BizApiStatsByApiRequest"); paths["/admin_api/biz_api_stats/summary"] = post("调用量汇总与趋势", "BizApiStatsSummaryRequest"); diff --git a/_docs/sql/biz_api_call_log_response_body.sql b/_docs/sql/biz_api_call_log_response_body.sql new file mode 100644 index 0000000..a520517 --- /dev/null +++ b/_docs/sql/biz_api_call_log_response_body.sql @@ -0,0 +1,3 @@ +-- 调用日志:存储上游响应摘要(与 api/model/biz_api_call_log.js 中 response_body 一致) +ALTER TABLE `biz_api_call_log` + ADD COLUMN `response_body` TEXT NULL COMMENT '上游响应体 JSON 文本(截断存储)' AFTER `response_time`; diff --git a/admin/src/api/subscription/api_call_log_server.js b/admin/src/api/subscription/api_call_log_server.js new file mode 100644 index 0000000..95859b1 --- /dev/null +++ b/admin/src/api/subscription/api_call_log_server.js @@ -0,0 +1,11 @@ +class ApiCallLogServer { + async page(row) { + return window.framework.http.post("/biz_api_call_log/page", row); + } + + async exportRows(row) { + return window.framework.http.post("/biz_api_call_log/export", row); + } +} + +export default new ApiCallLogServer(); diff --git a/admin/src/router/component-map.js b/admin/src/router/component-map.js index 768e6c3..3ad98d4 100644 --- a/admin/src/router/component-map.js +++ b/admin/src/router/component-map.js @@ -8,6 +8,7 @@ import SubscriptionTokens from '../views/subscription/tokens.vue' import SubscriptionPayment from '../views/subscription/payment.vue' import SubscriptionUsage from '../views/subscription/usage.vue' import SubscriptionAuditLog from '../views/subscription/audit_log.vue' +import SubscriptionApiCallLog from '../views/subscription/api_call_log.vue' const componentMap = { // 与 sys_menu.component 一致:库中常见为 home/index 或 home/index.vue @@ -22,6 +23,7 @@ const componentMap = { 'subscription/payment': SubscriptionPayment, 'subscription/usage': SubscriptionUsage, 'subscription/audit': SubscriptionAuditLog, + 'subscription/api_call_log': SubscriptionApiCallLog, } export default componentMap; diff --git a/admin/src/views/subscription/api_call_log.vue b/admin/src/views/subscription/api_call_log.vue new file mode 100644 index 0000000..2912384 --- /dev/null +++ b/admin/src/views/subscription/api_call_log.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/api/controller_admin/biz_api_stats.js b/api/controller_admin/biz_api_stats.js index 80514b0..8639f1a 100644 --- a/api/controller_admin/biz_api_stats.js +++ b/api/controller_admin/biz_api_stats.js @@ -123,4 +123,17 @@ module.exports = { }); ctx.success({ rows, count }); }, + + "POST /biz_api_call_log/export": async (ctx) => { + const body = ctx.getBody(); + const param = body.param || body; + const biz_api_call_log = baseModel.biz_api_call_log; + const where = build_search_where(biz_api_call_log, param.seachOption || {}); + const rows = await biz_api_call_log.findAll({ + where, + limit: 10000, + order: [["id", "DESC"]], + }); + ctx.success({ rows }); + }, }; diff --git a/api/model/biz_api_call_log.js b/api/model/biz_api_call_log.js index d116991..1e54111 100644 --- a/api/model/biz_api_call_log.js +++ b/api/model/biz_api_call_log.js @@ -32,6 +32,10 @@ module.exports = (db) => { allowNull: false, defaultValue: 0, }, + response_body: { + type: Sequelize.TEXT, + allowNull: true, + }, call_date: { type: Sequelize.DATEONLY, allowNull: false, @@ -50,6 +54,6 @@ module.exports = (db) => { } ); - //biz_api_call_log.sync({ force: true }); + //biz_api_call_log.sync({ force: true }); return biz_api_call_log; }; diff --git a/api/service/biz_proxy_service.js b/api/service/biz_proxy_service.js index eec5bdf..9d28add 100644 --- a/api/service/biz_proxy_service.js +++ b/api/service/biz_proxy_service.js @@ -5,6 +5,26 @@ const logs = require("../../tool/logs_proxy"); const upstreamBaseUrl = config.upstream_api_url || "http://127.0.0.1:8888"; +/** 写入日志用:序列化响应并截断,避免 TEXT 过大 */ +const RESPONSE_BODY_MAX_LEN = 16000; + +function serialize_response_for_log(data) { + if (data === undefined || data === null) return ""; + let s; + if (typeof data === "string") s = data; + else { + try { + s = JSON.stringify(data); + } catch (e) { + s = String(data); + } + } + if (s.length > RESPONSE_BODY_MAX_LEN) { + return `${s.slice(0, RESPONSE_BODY_MAX_LEN)}…[truncated]`; + } + return s; +} + /** * 转发请求到上游并记录调用日志 * @param {object} params @@ -57,6 +77,7 @@ async function forwardRequest({ api_path, method, query, body, headers, auth_ctx http_method: method.toUpperCase(), status_code, response_time, + response_body: serialize_response_for_log(resp_data), }).catch((e) => logs.error("[proxy] 写调用日志失败", e.message)); return { status: status_code, data: resp_data, headers: resp_headers }; @@ -65,7 +86,15 @@ async function forwardRequest({ api_path, method, query, body, headers, auth_ctx /** * 写入 API 调用日志 */ -async function writeCallLog({ user_id, token_id, api_path, http_method, status_code, response_time }) { +async function writeCallLog({ + user_id, + token_id, + api_path, + http_method, + status_code, + response_time, + response_body, +}) { try { const now = new Date(); const call_date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; @@ -76,6 +105,7 @@ async function writeCallLog({ user_id, token_id, api_path, http_method, status_c http_method, status_code, response_time, + response_body: response_body || null, call_date, created_at: now, });