1
This commit is contained in:
104
.cursor/rules/node-core-framework.mdc
Normal file
104
.cursor/rules/node-core-framework.mdc
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
description: Node Core(Koa2+Sequelize)框架集成、启动与控制器约定;若项目含 framework/node-core-framework.js 则遵循本规范
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Node Core 规范
|
||||||
|
|
||||||
|
若项目根目录(或约定路径)存在 **`framework/node-core-framework.js`**(或等价 Node Core 打包产物),后端启动与路由须按本节执行。
|
||||||
|
|
||||||
|
## 本仓库路径对照
|
||||||
|
|
||||||
|
| 规范表述 | 本仓库 |
|
||||||
|
|---------|--------|
|
||||||
|
| 框架入口 | `./framework/node-core-framework.js` |
|
||||||
|
| 框架配置 | `./config/framework.config.js` |
|
||||||
|
| 业务后台控制器目录 | `api/controller_admin`(前缀 `/admin_api`,勿在键名里重复写前缀) |
|
||||||
|
| 小程序/开放端控制器 | `api/controller_front`(前缀 `/api`) |
|
||||||
|
| 手动注册/代理等自定义 | `api/controller_custom`(仅 `app.js` 的 `beforeInitApi` 内 `addRoutes`) |
|
||||||
|
| 模型关联 | `./config/model.associations.js` |
|
||||||
|
| 白名单 | `./config/config.js` → `allowUrls` |
|
||||||
|
| `beforeInitApi` 实现体 | `./config/before_init_api.js`(由 `framework.config.js` 挂到 `init` 参数) |
|
||||||
|
| 管理端通用工具(非业务域) | `api/utils/query_helpers.js`(分页/筛选/导出)、`api/utils/biz_audit.js`(审计) |
|
||||||
|
| 跨端/复杂业务 | `api/service/*`(如 `biz_auth_verify`、`biz_proxy_service`、`biz_subscription_logic`、`biz_token_logic`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Framework 启动(`app.js`)
|
||||||
|
|
||||||
|
## 入口流程
|
||||||
|
|
||||||
|
1. `require('./framework/node-core-framework.js')` → `Framework.init({ ... })`
|
||||||
|
2. **配置**:展开 `require('./config/framework.config.js')`,勿在 `app.js` 内重复写数据库、端口等业务配置。
|
||||||
|
3. **模型关联**:传入 `businessAssociations: require('./config/model.associations.js')`。
|
||||||
|
4. **`beforeInitApi`**:在 `Framework.init` **resolve 之前**执行;本仓库逻辑在 **`config/before_init_api.js`**,由 **`config/framework.config.js`** 导出并随 `...config` 传入 `init`,**`app.js` 只负责 `Framework.init({ ...config, businessAssociations })`**。
|
||||||
|
- 动态转发路由:在 `beforeInitApi` 内 `require('./api/controller_custom/proxy_api')` 的 `buildProxyRoutes()`,再注册两套前缀:
|
||||||
|
- `framework.addRoutes('', proxyRoutes)` — 与 OpenAPI/Swagger 文档路径一致(如 `/admin/...`)
|
||||||
|
- `framework.addRoutes('/api', proxyRoutes)` — 兼容 `/api/admin/...`
|
||||||
|
- **不要**把代理路由写在 `api/controller_front`;自定义注册放在 `api/controller_custom`。
|
||||||
|
5. **`await framework.start(config.port.node)`**:真正监听 HTTP。
|
||||||
|
6. **定时任务**等依赖框架就绪的逻辑放在 `start()` 里、且 **`framework.start` 完成之后**再 `require`/`init`,例如 `middleware/schedule.js`。
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- `start()` 使用 `try/catch`;失败时打印错误并 `process.exit(1)`,避免进程无监听挂起。
|
||||||
|
|
||||||
|
## 修改启动行为时注意
|
||||||
|
|
||||||
|
- 新增 `addRoutes`:键格式与框架约定一致,如 `'POST /path': async (ctx) => { ... }`。
|
||||||
|
- 白名单路径在 `config/config.js` 的 `allowUrls`(框架层免 token)与业务层(如转发 Token 鉴权)是两段逻辑,改路由前缀时需同步 `allowUrls`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 路由体系
|
||||||
|
|
||||||
|
- **约定式控制器**:`framework.config.js` 的 `apiPaths` 指向目录,扫描导出;单条路径 = `prefix` + 控制器内声明的路径。
|
||||||
|
- **内置系统管理**:后台接口固定前缀 **`/admin_api`**(由框架注册系统控制器;本仓库业务管理接口在 `api/controller_admin`,同前缀)。
|
||||||
|
- **手动路由**:仅在 **`beforeInitApi`** 内调用 `framework.addRoutes(prefix, routes)`;`routes` 键名格式与控制器相同。不要在 `Framework.init()` 已结束后再注册同类路由(会晚于静态路由流水线,易 404)。
|
||||||
|
|
||||||
|
## 鉴权与白名单
|
||||||
|
|
||||||
|
- `allowUrls`:路径**子串**匹配则免框架层 admin/applet token;另有默认放行段(以框架 `middleware` 为准)。
|
||||||
|
- `apiPaths` 项可设 `authType`:`admin` → 校验 **`admin-token`**(`ctx.getAdminUserId()`);`applet` → **`applet-token`**(`ctx.getPappletUserId()`)。
|
||||||
|
|
||||||
|
## 中间件时机
|
||||||
|
|
||||||
|
- `beforeInitApi(framework)`:在框架完成路由装配流水线中的**早阶段**执行,适合 `framework.addRoutes` 或与路由顺序相关的 `use`。
|
||||||
|
- `init` **之后**再往 `app` 追加的中间件通常位于路由之后,多数请求已被消费,一般仅兜底。
|
||||||
|
|
||||||
|
## 接口与命名(业务侧)
|
||||||
|
|
||||||
|
- 语义化 URL,**优先 POST** 做变更类接口;路径片段可与项目统一为 **snake_case**(如 `/order/create`)。
|
||||||
|
- JSON 响应优先用 Context 扩展:`ctx.success` / `ctx.fail` / `ctx.tokenFail`;参数可用 `ctx.get`(query+body 合并,body 覆盖 query,以框架实现为准)。
|
||||||
|
|
||||||
|
## 修改核心 / 打包注意
|
||||||
|
|
||||||
|
- 路由注册顺序以框架 `init` 内部为准;新增动态加载路径时若存在 webpack 打包,需与框架内 `__non_webpack_require__` 等行为一致。
|
||||||
|
|
||||||
|
## 控制器约定
|
||||||
|
|
||||||
|
### 导出形状
|
||||||
|
|
||||||
|
- 每个文件 `module.exports = { 'METHOD /path': handler, ... }`。
|
||||||
|
- **METHOD** 仅为 **`GET` 或 `POST`**,后接**一个空格**再写路径,例如:`POST /goods/create`、`GET /goods/detail/:id`。
|
||||||
|
- `handler` 建议为 **async** `(ctx, next) => { ... }`;需继续管道时 `await next()`。
|
||||||
|
|
||||||
|
### 管理端(本仓库)
|
||||||
|
|
||||||
|
- 文件在 **`api/controller_admin`**;对外完整路径 = **`/admin_api` + 键名中的路径**,键名里**不要**再写 `/admin_api`。
|
||||||
|
- 需登录:请求头 `admin-token: <jwt>`。
|
||||||
|
|
||||||
|
### 业务/开放端(本仓库)
|
||||||
|
|
||||||
|
- `api/controller_front`,`apiPaths` 中 `prefix: '/api'`,`authType: 'applet'`。
|
||||||
|
|
||||||
|
### 常见 Context API(以框架为准)
|
||||||
|
|
||||||
|
- 参数:`ctx.get('id')`、`ctx.getBody()`、`ctx.getQuery()`、`ctx.getPageSize()` 等。
|
||||||
|
- 响应:`ctx.success(data, msg)`、`ctx.fail(msg)`、`ctx.tokenFail()`、`ctx.json(code, message, data)`。
|
||||||
|
- 鉴权:`ctx.getAdminUserId()`、`ctx.getPappletUserId()`。
|
||||||
|
|
||||||
|
### 避免
|
||||||
|
|
||||||
|
- 不要用 **`PUT` / `DELETE`** 等作为导出键(框架只按 **GET/POST** 注册)。
|
||||||
|
- 不要把 **`addRoutes`** 散落到 `init` 完成后的随意位置;统一放 **`beforeInitApi`**。
|
||||||
9028
_docs/swagger.json
Normal file
9028
_docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,21 @@
|
|||||||
const stats = require("../service/biz_api_stats_service");
|
const Sequelize = require("sequelize");
|
||||||
|
const { Op } = Sequelize;
|
||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const { find_page } = require("../service/biz_query_helpers");
|
const { find_page } = require("../utils/query_helpers");
|
||||||
|
|
||||||
|
function build_date_where(start_date, end_date) {
|
||||||
|
const where = {};
|
||||||
|
if (start_date && end_date) {
|
||||||
|
where.call_date = { [Op.between]: [start_date, end_date] };
|
||||||
|
} else if (start_date) {
|
||||||
|
where.call_date = { [Op.gte]: start_date };
|
||||||
|
} else if (end_date) {
|
||||||
|
where.call_date = { [Op.lte]: end_date };
|
||||||
|
}
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/** 按用户查询调用统计 */
|
|
||||||
"POST /biz_api_stats/by_user": async (ctx) => {
|
"POST /biz_api_stats/by_user": async (ctx) => {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
const { user_id, start_date, end_date } = body;
|
const { user_id, start_date, end_date } = body;
|
||||||
@@ -11,11 +23,22 @@ module.exports = {
|
|||||||
ctx.fail("缺少 user_id");
|
ctx.fail("缺少 user_id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await stats.getStatsByUser(user_id, start_date, end_date);
|
const where = { user_id, ...build_date_where(start_date, end_date) };
|
||||||
ctx.success(data);
|
const rows = await baseModel.biz_api_call_log.findAll({
|
||||||
|
attributes: [
|
||||||
|
"api_path",
|
||||||
|
[baseModel.Sequelize.fn("COUNT", baseModel.Sequelize.col("id")), "call_count"],
|
||||||
|
[baseModel.Sequelize.fn("AVG", baseModel.Sequelize.col("response_time")), "avg_response_time"],
|
||||||
|
],
|
||||||
|
where,
|
||||||
|
group: ["api_path"],
|
||||||
|
order: [[baseModel.Sequelize.literal("call_count"), "DESC"]],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
const total = rows.reduce((s, r) => s + Number(r.call_count), 0);
|
||||||
|
ctx.success({ total, rows });
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 按接口路径查询调用统计 */
|
|
||||||
"POST /biz_api_stats/by_api": async (ctx) => {
|
"POST /biz_api_stats/by_api": async (ctx) => {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
const { api_path, start_date, end_date } = body;
|
const { api_path, start_date, end_date } = body;
|
||||||
@@ -23,19 +46,65 @@ module.exports = {
|
|||||||
ctx.fail("缺少 api_path");
|
ctx.fail("缺少 api_path");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await stats.getStatsByApi(api_path, start_date, end_date);
|
const where = { api_path, ...build_date_where(start_date, end_date) };
|
||||||
ctx.success(data);
|
const rows = await baseModel.biz_api_call_log.findAll({
|
||||||
|
attributes: [
|
||||||
|
"user_id",
|
||||||
|
[baseModel.Sequelize.fn("COUNT", baseModel.Sequelize.col("id")), "call_count"],
|
||||||
|
[baseModel.Sequelize.fn("AVG", baseModel.Sequelize.col("response_time")), "avg_response_time"],
|
||||||
|
],
|
||||||
|
where,
|
||||||
|
group: ["user_id"],
|
||||||
|
order: [[baseModel.Sequelize.literal("call_count"), "DESC"]],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
const total = rows.reduce((s, r) => s + Number(r.call_count), 0);
|
||||||
|
ctx.success({ total, rows });
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 综合统计面板 */
|
|
||||||
"POST /biz_api_stats/summary": async (ctx) => {
|
"POST /biz_api_stats/summary": async (ctx) => {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
const { start_date, end_date, top_limit } = body;
|
const { start_date, end_date, top_limit } = body;
|
||||||
const data = await stats.getSummary(start_date, end_date, top_limit || 10);
|
const dateWhere = build_date_where(start_date, end_date);
|
||||||
ctx.success(data);
|
const Seq = baseModel.Sequelize;
|
||||||
|
const lim = top_limit || 10;
|
||||||
|
|
||||||
|
const totalResult = await baseModel.biz_api_call_log.count({ where: dateWhere });
|
||||||
|
|
||||||
|
const daily_trend = await baseModel.biz_api_call_log.findAll({
|
||||||
|
attributes: ["call_date", [Seq.fn("COUNT", Seq.col("id")), "call_count"]],
|
||||||
|
where: dateWhere,
|
||||||
|
group: ["call_date"],
|
||||||
|
order: [["call_date", "ASC"]],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const top_apis = await baseModel.biz_api_call_log.findAll({
|
||||||
|
attributes: ["api_path", [Seq.fn("COUNT", Seq.col("id")), "call_count"]],
|
||||||
|
where: dateWhere,
|
||||||
|
group: ["api_path"],
|
||||||
|
order: [[Seq.literal("call_count"), "DESC"]],
|
||||||
|
limit: lim,
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const top_users = await baseModel.biz_api_call_log.findAll({
|
||||||
|
attributes: ["user_id", [Seq.fn("COUNT", Seq.col("id")), "call_count"]],
|
||||||
|
where: dateWhere,
|
||||||
|
group: ["user_id"],
|
||||||
|
order: [[Seq.literal("call_count"), "DESC"]],
|
||||||
|
limit: lim,
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.success({
|
||||||
|
total_calls: totalResult,
|
||||||
|
daily_trend,
|
||||||
|
top_apis,
|
||||||
|
top_users,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 调用日志分页列表 */
|
|
||||||
"POST /biz_api_call_log/page": async (ctx) => {
|
"POST /biz_api_call_log/page": async (ctx) => {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
const { count, rows } = await find_page(baseModel.biz_api_call_log, "biz_api_call_log", body);
|
const { count, rows } = await find_page(baseModel.biz_api_call_log, "biz_api_call_log", body);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const { find_page, find_for_export } = require("../service/biz_query_helpers");
|
const { find_page, find_for_export } = require("../utils/query_helpers");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"POST /biz_audit_log/page": async (ctx) => {
|
"POST /biz_audit_log/page": async (ctx) => {
|
||||||
|
|||||||
@@ -1,8 +1,48 @@
|
|||||||
const dashboard = require("../service/biz_dashboard_service");
|
const Sequelize = require("sequelize");
|
||||||
|
const { Op } = Sequelize;
|
||||||
|
const baseModel = require("../../middleware/baseModel");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"GET /biz_dashboard/summary": async (ctx) => {
|
"GET /biz_dashboard/summary": async (ctx) => {
|
||||||
const data = await dashboard.summary();
|
const now = new Date();
|
||||||
ctx.success(data);
|
const in7 = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const [
|
||||||
|
userTotal,
|
||||||
|
userActive,
|
||||||
|
planActive,
|
||||||
|
subPending,
|
||||||
|
subActive,
|
||||||
|
subExpired,
|
||||||
|
tokenActive,
|
||||||
|
renewSoon,
|
||||||
|
] = await Promise.all([
|
||||||
|
baseModel.biz_user.count(),
|
||||||
|
baseModel.biz_user.count({ where: { status: "active" } }),
|
||||||
|
baseModel.biz_plan.count({ where: { status: "active" } }),
|
||||||
|
baseModel.biz_subscription.count({ where: { status: "pending" } }),
|
||||||
|
baseModel.biz_subscription.count({ where: { status: "active" } }),
|
||||||
|
baseModel.biz_subscription.count({ where: { status: "expired" } }),
|
||||||
|
baseModel.biz_api_token.count({ where: { status: "active" } }),
|
||||||
|
baseModel.biz_subscription.count({
|
||||||
|
where: {
|
||||||
|
status: "active",
|
||||||
|
end_time: { [Op.between]: [now, in7] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ctx.success({
|
||||||
|
users: { total: userTotal, active: userActive },
|
||||||
|
plans: { active: planActive },
|
||||||
|
subscriptions: {
|
||||||
|
pending: subPending,
|
||||||
|
active: subActive,
|
||||||
|
expired: subExpired,
|
||||||
|
renew_within_7d: renewSoon,
|
||||||
|
},
|
||||||
|
tokens: { active: tokenActive },
|
||||||
|
server_time: now.toISOString(),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const logic = require("../service/biz_subscription_logic");
|
const logic = require("../service/biz_subscription_logic");
|
||||||
const audit = require("../service/biz_audit_service");
|
const audit = require("../utils/biz_audit");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const { find_page, find_for_export, normalize_for_write } = require("../service/biz_query_helpers");
|
const { find_page, find_for_export, normalize_for_write } = require("../utils/query_helpers");
|
||||||
const audit = require("../service/biz_audit_service");
|
const audit = require("../utils/biz_audit");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"POST /biz_plan/page": async (ctx) => {
|
"POST /biz_plan/page": async (ctx) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const { find_page, find_for_export } = require("../service/biz_query_helpers");
|
const { find_page, find_for_export } = require("../utils/query_helpers");
|
||||||
const logic = require("../service/biz_subscription_logic");
|
const logic = require("../service/biz_subscription_logic");
|
||||||
const audit = require("../service/biz_audit_service");
|
const audit = require("../utils/biz_audit");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"POST /biz_subscription/page": async (ctx) => {
|
"POST /biz_subscription/page": async (ctx) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const { find_page, find_for_export } = require("../service/biz_query_helpers");
|
const { find_page, find_for_export } = require("../utils/query_helpers");
|
||||||
const tokenLogic = require("../service/biz_token_logic");
|
const tokenLogic = require("../service/biz_token_logic");
|
||||||
const audit = require("../service/biz_audit_service");
|
const audit = require("../utils/biz_audit");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"POST /biz_token/page": async (ctx) => {
|
"POST /biz_token/page": async (ctx) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const { find_page, find_for_export, normalize_for_write } = require("../service/biz_query_helpers");
|
const { find_page, find_for_export, normalize_for_write } = require("../utils/query_helpers");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"POST /biz_usage/page": async (ctx) => {
|
"POST /biz_usage/page": async (ctx) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const Sequelize = require("sequelize");
|
const Sequelize = require("sequelize");
|
||||||
const { find_for_export, normalize_for_write, build_search_where } = require("../service/biz_query_helpers");
|
const { find_for_export, normalize_for_write, build_search_where } = require("../utils/query_helpers");
|
||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const tokenLogic = require("../service/biz_token_logic");
|
const tokenLogic = require("../service/biz_token_logic");
|
||||||
const audit = require("../service/biz_audit_service");
|
const audit = require("../utils/biz_audit");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"POST /biz_user/page": async (ctx) => {
|
"POST /biz_user/page": async (ctx) => {
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
|
||||||
const { op } = baseModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建日期范围 where 条件
|
|
||||||
*/
|
|
||||||
function buildDateWhere(start_date, end_date) {
|
|
||||||
const where = {};
|
|
||||||
if (start_date && end_date) {
|
|
||||||
where.call_date = { [op.between]: [start_date, end_date] };
|
|
||||||
} else if (start_date) {
|
|
||||||
where.call_date = { [op.gte]: start_date };
|
|
||||||
} else if (end_date) {
|
|
||||||
where.call_date = { [op.lte]: end_date };
|
|
||||||
}
|
|
||||||
return where;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按用户统计调用量(按接口路径分组)
|
|
||||||
* @param {number} user_id
|
|
||||||
* @param {string} [start_date] - YYYY-MM-DD
|
|
||||||
* @param {string} [end_date] - YYYY-MM-DD
|
|
||||||
*/
|
|
||||||
async function getStatsByUser(user_id, start_date, end_date) {
|
|
||||||
const where = { user_id, ...buildDateWhere(start_date, end_date) };
|
|
||||||
const rows = await baseModel.biz_api_call_log.findAll({
|
|
||||||
attributes: [
|
|
||||||
"api_path",
|
|
||||||
[baseModel.Sequelize.fn("COUNT", baseModel.Sequelize.col("id")), "call_count"],
|
|
||||||
[baseModel.Sequelize.fn("AVG", baseModel.Sequelize.col("response_time")), "avg_response_time"],
|
|
||||||
],
|
|
||||||
where,
|
|
||||||
group: ["api_path"],
|
|
||||||
order: [[baseModel.Sequelize.literal("call_count"), "DESC"]],
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = rows.reduce((s, r) => s + Number(r.call_count), 0);
|
|
||||||
return { total, rows };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按接口路径统计调用量(按用户分组)
|
|
||||||
* @param {string} api_path
|
|
||||||
* @param {string} [start_date]
|
|
||||||
* @param {string} [end_date]
|
|
||||||
*/
|
|
||||||
async function getStatsByApi(api_path, start_date, end_date) {
|
|
||||||
const where = { api_path, ...buildDateWhere(start_date, end_date) };
|
|
||||||
const rows = await baseModel.biz_api_call_log.findAll({
|
|
||||||
attributes: [
|
|
||||||
"user_id",
|
|
||||||
[baseModel.Sequelize.fn("COUNT", baseModel.Sequelize.col("id")), "call_count"],
|
|
||||||
[baseModel.Sequelize.fn("AVG", baseModel.Sequelize.col("response_time")), "avg_response_time"],
|
|
||||||
],
|
|
||||||
where,
|
|
||||||
group: ["user_id"],
|
|
||||||
order: [[baseModel.Sequelize.literal("call_count"), "DESC"]],
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = rows.reduce((s, r) => s + Number(r.call_count), 0);
|
|
||||||
return { total, rows };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 综合统计面板:总调用量、按天趋势、Top 接口、Top 用户
|
|
||||||
* @param {string} [start_date]
|
|
||||||
* @param {string} [end_date]
|
|
||||||
* @param {number} [top_limit=10]
|
|
||||||
*/
|
|
||||||
async function getSummary(start_date, end_date, top_limit = 10) {
|
|
||||||
const dateWhere = buildDateWhere(start_date, end_date);
|
|
||||||
const Seq = baseModel.Sequelize;
|
|
||||||
|
|
||||||
// 总调用量
|
|
||||||
const totalResult = await baseModel.biz_api_call_log.count({ where: dateWhere });
|
|
||||||
|
|
||||||
// 按天趋势
|
|
||||||
const daily_trend = await baseModel.biz_api_call_log.findAll({
|
|
||||||
attributes: [
|
|
||||||
"call_date",
|
|
||||||
[Seq.fn("COUNT", Seq.col("id")), "call_count"],
|
|
||||||
],
|
|
||||||
where: dateWhere,
|
|
||||||
group: ["call_date"],
|
|
||||||
order: [["call_date", "ASC"]],
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Top 接口
|
|
||||||
const top_apis = await baseModel.biz_api_call_log.findAll({
|
|
||||||
attributes: [
|
|
||||||
"api_path",
|
|
||||||
[Seq.fn("COUNT", Seq.col("id")), "call_count"],
|
|
||||||
],
|
|
||||||
where: dateWhere,
|
|
||||||
group: ["api_path"],
|
|
||||||
order: [[Seq.literal("call_count"), "DESC"]],
|
|
||||||
limit: top_limit,
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Top 用户
|
|
||||||
const top_users = await baseModel.biz_api_call_log.findAll({
|
|
||||||
attributes: [
|
|
||||||
"user_id",
|
|
||||||
[Seq.fn("COUNT", Seq.col("id")), "call_count"],
|
|
||||||
],
|
|
||||||
where: dateWhere,
|
|
||||||
group: ["user_id"],
|
|
||||||
order: [[Seq.literal("call_count"), "DESC"]],
|
|
||||||
limit: top_limit,
|
|
||||||
raw: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
total_calls: totalResult,
|
|
||||||
daily_trend,
|
|
||||||
top_apis,
|
|
||||||
top_users,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { getStatsByUser, getStatsByApi, getSummary };
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
const baseModel = require("../../middleware/baseModel");
|
|
||||||
const { op } = baseModel;
|
|
||||||
|
|
||||||
async function summary() {
|
|
||||||
const now = new Date();
|
|
||||||
const in7 = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const [
|
|
||||||
userTotal,
|
|
||||||
userActive,
|
|
||||||
planActive,
|
|
||||||
subPending,
|
|
||||||
subActive,
|
|
||||||
subExpired,
|
|
||||||
tokenActive,
|
|
||||||
renewSoon,
|
|
||||||
] = await Promise.all([
|
|
||||||
baseModel.biz_user.count(),
|
|
||||||
baseModel.biz_user.count({ where: { status: "active" } }),
|
|
||||||
baseModel.biz_plan.count({ where: { status: "active" } }),
|
|
||||||
baseModel.biz_subscription.count({ where: { status: "pending" } }),
|
|
||||||
baseModel.biz_subscription.count({ where: { status: "active" } }),
|
|
||||||
baseModel.biz_subscription.count({ where: { status: "expired" } }),
|
|
||||||
baseModel.biz_api_token.count({ where: { status: "active" } }),
|
|
||||||
baseModel.biz_subscription.count({
|
|
||||||
where: {
|
|
||||||
status: "active",
|
|
||||||
end_time: { [op.between]: [now, in7] },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
users: { total: userTotal, active: userActive },
|
|
||||||
plans: { active: planActive },
|
|
||||||
subscriptions: {
|
|
||||||
pending: subPending,
|
|
||||||
active: subActive,
|
|
||||||
expired: subExpired,
|
|
||||||
renew_within_7d: renewSoon,
|
|
||||||
},
|
|
||||||
tokens: { active: tokenActive },
|
|
||||||
server_time: now.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { summary };
|
|
||||||
@@ -3,13 +3,6 @@ const logs = require("../../tool/logs_proxy");
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录审计(失败不影响主流程)
|
* 记录审计(失败不影响主流程)
|
||||||
* @param {object} p
|
|
||||||
* @param {number} [p.admin_user_id]
|
|
||||||
* @param {number} [p.biz_user_id]
|
|
||||||
* @param {string} p.action
|
|
||||||
* @param {string} [p.resource_type]
|
|
||||||
* @param {number} [p.resource_id]
|
|
||||||
* @param {object} [p.detail]
|
|
||||||
*/
|
*/
|
||||||
async function logAudit(p) {
|
async function logAudit(p) {
|
||||||
try {
|
try {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 列表筛选、写入字段裁剪、审计日志列表补列(供 admin 控制器直接配合 baseModel 使用)
|
* 管理端列表筛选、导出、写入字段裁剪(供 controller_admin 配合 baseModel)
|
||||||
*/
|
*/
|
||||||
const Sequelize = require("sequelize");
|
const Sequelize = require("sequelize");
|
||||||
const { Op } = Sequelize;
|
const { Op } = Sequelize;
|
||||||
54
app.js
54
app.js
@@ -1,60 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* 后端应用入口(模板项目)
|
* 后端应用入口 — node-core-framework(约定见 .cursor/rules/node-core-framework.mdc)
|
||||||
* 使用 node-core-framework 框架
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Framework = require('./framework/node-core-framework.js');
|
const Framework = require("./framework/node-core-framework.js");
|
||||||
const config = require('./config/framework.config.js');
|
const config = require("./config/framework.config.js");
|
||||||
const businessAssociations = require('./config/model.associations.js');
|
const businessAssociations = require("./config/model.associations.js");
|
||||||
|
|
||||||
|
|
||||||
// 定时任务(在 Framework 初始化后再加载)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
try {
|
try {
|
||||||
console.log('🚀 正在启动应用...\n');
|
console.log("🚀 正在启动应用...\n");
|
||||||
console.log('⚙️ 正在初始化框架...');
|
console.log("⚙️ 正在初始化框架...");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const framework = await Framework.init({
|
const framework = await Framework.init({
|
||||||
...config,
|
...config,
|
||||||
businessAssociations,
|
businessAssociations,
|
||||||
beforeInitApi: async (framework) => {
|
|
||||||
const { buildProxyRoutes } = require('./api/controller_custom/proxy_api');
|
|
||||||
// 从 swagger.json 动态注册 193 个转发路由到 /api 前缀
|
|
||||||
const proxyRoutes = buildProxyRoutes();
|
|
||||||
const n = Object.keys(proxyRoutes).length;
|
|
||||||
// 与 swagger 文档一致:/admin/...、/login/...
|
|
||||||
framework.addRoutes("", proxyRoutes);
|
|
||||||
// 兼容历史调用:/api/admin/...
|
|
||||||
framework.addRoutes("/api", proxyRoutes);
|
|
||||||
console.log(`📡 已注册 ${n} 个转发接口(文档路径 + /api 前缀各一套)`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await framework.start(config.port.node);
|
await framework.start(config.port.node);
|
||||||
const schedule = require('./middleware/schedule.js');
|
|
||||||
// if (config.env !== 'development') {
|
|
||||||
await schedule.init();
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const schedule = require("./middleware/schedule.js");
|
||||||
|
await schedule.init();
|
||||||
|
|
||||||
console.log(`\n📚 API 文档: http://localhost:${config.port.node}/api/docs`);
|
console.log(`\n📚 API 文档: http://localhost:${config.port.node}/api/docs`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('\n' + '='.repeat(60));
|
console.error("\n" + "=".repeat(60));
|
||||||
console.error('❌ 应用启动失败!');
|
console.error("❌ 应用启动失败!");
|
||||||
console.error('='.repeat(60));
|
console.error("=".repeat(60));
|
||||||
console.error('错误信息:', error.message);
|
console.error("错误信息:", error.message);
|
||||||
console.error('错误堆栈:', error.stack);
|
console.error("错误堆栈:", error.stack);
|
||||||
console.error('='.repeat(60) + '\n');
|
console.error("=".repeat(60) + "\n");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
config/before_init_api.js
Normal file
11
config/before_init_api.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Framework.init 的 beforeInitApi:在 _setupRoutes 之前注册手动路由(规范见 .cursor/rules/node-core-framework.mdc)
|
||||||
|
*/
|
||||||
|
module.exports = async function beforeInitApi(framework) {
|
||||||
|
const { buildProxyRoutes } = require("../api/controller_custom/proxy_api");
|
||||||
|
const proxy_routes = buildProxyRoutes();
|
||||||
|
const n = Object.keys(proxy_routes).length;
|
||||||
|
framework.addRoutes("", proxy_routes);
|
||||||
|
framework.addRoutes("/api", proxy_routes);
|
||||||
|
console.log(`📡 已注册 ${n} 个转发接口(文档路径 + /api 前缀各一套)`);
|
||||||
|
};
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
* 基于 node-core-framework 的配置
|
* 基于 node-core-framework 的配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const baseConfig = require('./config.js');
|
const baseConfig = require("./config.js");
|
||||||
const customSchemas = require('./custom.schemas.js');
|
const customSchemas = require("./custom.schemas.js");
|
||||||
|
const beforeInitApi = require("./before_init_api");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// ===== 必需配置 =====
|
// ===== 必需配置 =====
|
||||||
@@ -119,6 +120,9 @@ module.exports = {
|
|||||||
|
|
||||||
|
|
||||||
// 自定义 Swagger Schemas
|
// 自定义 Swagger Schemas
|
||||||
customSchemas: customSchemas
|
customSchemas: customSchemas,
|
||||||
|
|
||||||
|
// 路由流水线早期回调:注册转发等 addRoutes(勿在 init 之后散落注册)
|
||||||
|
beforeInitApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user