1
This commit is contained in:
3760
_docs/API接口清单-按模块.md
3760
_docs/API接口清单-按模块.md
File diff suppressed because it is too large
Load Diff
162
_docs/_gen_api_doc.js
Normal file
162
_docs/_gen_api_doc.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 从 swagger.json 生成带参数的 API 接口清单 MD 文档
|
||||
* 用法: node _docs/_gen_api_doc.js
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const swagger = require("./swagger.json");
|
||||
const mdRaw = fs.readFileSync(path.join(__dirname, "接口说明文档-完整版-含营销等级.md"), "utf8");
|
||||
|
||||
// ========== 1. 解析套餐映射 ==========
|
||||
const planMap = {};
|
||||
let curPath = "";
|
||||
for (const line of mdRaw.split("\n")) {
|
||||
const m = line.match(/####.*`(POST|GET)\s+(.+?)`/);
|
||||
if (m) { curPath = m[2].trim(); continue; }
|
||||
const p = line.match(/对应套餐:\*\*(.+?)\*\*/);
|
||||
if (p && curPath) { planMap[curPath] = p[1]; curPath = ""; }
|
||||
}
|
||||
|
||||
// ========== 2. 解析 definitions ==========
|
||||
function resolveRef(ref) {
|
||||
if (!ref) return null;
|
||||
const name = ref.replace("#/definitions/", "");
|
||||
return swagger.definitions[name] || null;
|
||||
}
|
||||
|
||||
function resolveType(prop) {
|
||||
if (!prop) return "any";
|
||||
if (prop.$ref) {
|
||||
const name = prop.$ref.replace("#/definitions/", "");
|
||||
return name;
|
||||
}
|
||||
if (prop.type === "array") {
|
||||
if (prop.items) {
|
||||
if (prop.items.$ref) return resolveType(prop.items) + "[]";
|
||||
return (prop.items.type || "any") + "[]";
|
||||
}
|
||||
return "array";
|
||||
}
|
||||
let t = prop.type || "any";
|
||||
if (prop.format) t += `(${prop.format})`;
|
||||
return t;
|
||||
}
|
||||
|
||||
function getModelFields(def) {
|
||||
if (!def || !def.properties) return [];
|
||||
const fields = [];
|
||||
for (const [name, prop] of Object.entries(def.properties)) {
|
||||
fields.push({
|
||||
name,
|
||||
type: resolveType(prop),
|
||||
desc: (prop.description || "").trim() || "-",
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
// ========== 3. 按 tag 分组 ==========
|
||||
const tagNameMap = { "朋友": "好友", "/shop": "微信小店", "管理": "管理/授权" };
|
||||
function normTag(t) { return tagNameMap[t] || t; }
|
||||
|
||||
const tagOrder = [
|
||||
"登录", "用户", "好友", "标签", "消息", "消息回调",
|
||||
"群管理", "朋友圈", "收藏", "支付", "公众号/小程序",
|
||||
"企业微信", "视频号", "设备", "微信小店", "其他", "同步消息", "管理/授权",
|
||||
];
|
||||
|
||||
const groups = {};
|
||||
for (const [apiPath, methods] of Object.entries(swagger.paths)) {
|
||||
for (const [method, spec] of Object.entries(methods)) {
|
||||
const tag = (spec.tags && spec.tags[0]) || "未分类";
|
||||
if (!groups[tag]) groups[tag] = [];
|
||||
|
||||
const params = spec.parameters || [];
|
||||
const queryParams = params.filter((p) => p.in === "query");
|
||||
const bodyParam = params.find((p) => p.in === "body");
|
||||
|
||||
let bodyModelName = "";
|
||||
let bodyFields = [];
|
||||
if (bodyParam && bodyParam.schema && bodyParam.schema.$ref) {
|
||||
bodyModelName = bodyParam.schema.$ref.replace("#/definitions/", "");
|
||||
const def = resolveRef(bodyParam.schema.$ref);
|
||||
bodyFields = getModelFields(def);
|
||||
}
|
||||
|
||||
groups[tag].push({
|
||||
method: method.toUpperCase(),
|
||||
path: apiPath,
|
||||
summary: spec.summary || "",
|
||||
plan: planMap[apiPath] || "-",
|
||||
queryParams,
|
||||
bodyModelName,
|
||||
bodyFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sortedTags = Object.keys(groups).sort((a, b) => {
|
||||
const ia = tagOrder.indexOf(normTag(a));
|
||||
const ib = tagOrder.indexOf(normTag(b));
|
||||
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
|
||||
});
|
||||
|
||||
// ========== 4. 生成 MD ==========
|
||||
const totalApis = Object.values(groups).reduce((s, a) => s + a.length, 0);
|
||||
const planCount = {};
|
||||
for (const apis of Object.values(groups)) {
|
||||
for (const a of apis) {
|
||||
planCount[a.plan] = (planCount[a.plan] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let out = "# API 接口清单(按模块)\n\n";
|
||||
out += `> 接口总数:**${totalApis}**\n\n`;
|
||||
|
||||
out += "## 套餐统计\n\n";
|
||||
out += "| 套餐 | 接口数 |\n|---|---:|\n";
|
||||
for (const p of ["初级版", "高级版", "定制版", "白标/OEM"]) {
|
||||
if (planCount[p]) out += `| ${p} | ${planCount[p]} |\n`;
|
||||
}
|
||||
out += "\n---\n\n";
|
||||
|
||||
let secIdx = 0;
|
||||
for (const tag of sortedTags) {
|
||||
const apis = groups[tag];
|
||||
secIdx++;
|
||||
const displayTag = normTag(tag);
|
||||
out += `## ${secIdx}. ${displayTag}(${apis.length} 个接口)\n\n`;
|
||||
|
||||
apis.forEach((a, i) => {
|
||||
const num = `${secIdx}.${i + 1}`;
|
||||
out += `### ${num} \`${a.method} ${a.path}\` - ${a.summary}\n\n`;
|
||||
out += `- 套餐:**${a.plan}**\n\n`;
|
||||
|
||||
// Query 参数
|
||||
if (a.queryParams.length > 0) {
|
||||
out += "**Query 参数**\n\n";
|
||||
out += "| 参数名 | 类型 | 说明 |\n|---|---|---|\n";
|
||||
for (const q of a.queryParams) {
|
||||
out += `| \`${q.name}\` | ${q.type || "string"} | ${q.description || "-"} |\n`;
|
||||
}
|
||||
out += "\n";
|
||||
}
|
||||
|
||||
// Body 请求体
|
||||
if (a.bodyFields.length > 0) {
|
||||
out += `**请求体 (${a.bodyModelName})**\n\n`;
|
||||
out += "| 字段名 | 类型 | 说明 |\n|---|---|---|\n";
|
||||
for (const f of a.bodyFields) {
|
||||
out += `| \`${f.name}\` | \`${f.type}\` | ${f.desc} |\n`;
|
||||
}
|
||||
out += "\n";
|
||||
}
|
||||
|
||||
out += "---\n\n";
|
||||
});
|
||||
}
|
||||
|
||||
const outPath = path.join(__dirname, "API接口清单-按模块.md");
|
||||
fs.writeFileSync(outPath, out, "utf8");
|
||||
console.log(`done: ${outPath}, ${out.split("\n").length} lines`);
|
||||
@@ -74,9 +74,15 @@
|
||||
<FormItem label="朋友圈额度">
|
||||
<Input v-model="form.sns_quota" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="API调用上限">
|
||||
<Input v-model="form.api_call_quota" type="number" placeholder="0=不限制" />
|
||||
</FormItem>
|
||||
<FormItem label="功能点 JSON">
|
||||
<Input v-model="featuresText" type="textarea" :rows="4" placeholder='如 {"msg":true} 或 ["msg","mass"]' />
|
||||
</FormItem>
|
||||
<FormItem label="可用接口 JSON">
|
||||
<Input v-model="allowedApisText" type="textarea" :rows="4" placeholder='接口路径数组,如 ["/user/GetProfile","/message/SendText"],留空=不限制' />
|
||||
</FormItem>
|
||||
<FormItem label="状态" prop="status">
|
||||
<Select v-model="form.status" style="width: 100%">
|
||||
<Option value="active">上线</Option>
|
||||
@@ -106,6 +112,7 @@ export default {
|
||||
saving: false,
|
||||
form: {},
|
||||
featuresText: '{}',
|
||||
allowedApisText: '',
|
||||
rules: {
|
||||
plan_code: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
plan_name: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
@@ -120,6 +127,7 @@ export default {
|
||||
{ title: '编码', key: 'plan_code', width: 120 },
|
||||
{ title: '名称', key: 'plan_name', minWidth: 140 },
|
||||
{ title: '月费', key: 'monthly_price', width: 90 },
|
||||
{ title: 'API调用上限', key: 'api_call_quota', width: 120, render: (h, p) => h('span', p.row.api_call_quota > 0 ? p.row.api_call_quota : '不限') },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{
|
||||
title: '操作',
|
||||
@@ -166,6 +174,12 @@ export default {
|
||||
: typeof row.enabled_features === 'string'
|
||||
? row.enabled_features
|
||||
: JSON.stringify(row.enabled_features, null, 2)
|
||||
this.allowedApisText =
|
||||
row.allowed_apis == null
|
||||
? ''
|
||||
: typeof row.allowed_apis === 'string'
|
||||
? row.allowed_apis
|
||||
: JSON.stringify(row.allowed_apis, null, 2)
|
||||
} else {
|
||||
this.form = {
|
||||
plan_code: '',
|
||||
@@ -178,9 +192,11 @@ export default {
|
||||
mass_quota: 0,
|
||||
friend_quota: 0,
|
||||
sns_quota: 0,
|
||||
api_call_quota: 0,
|
||||
status: 'active',
|
||||
}
|
||||
this.featuresText = '{}'
|
||||
this.allowedApisText = ''
|
||||
}
|
||||
this.modal = true
|
||||
},
|
||||
@@ -202,7 +218,23 @@ export default {
|
||||
return
|
||||
}
|
||||
}
|
||||
const payload = { ...this.form, enabled_features }
|
||||
let allowed_apis = null
|
||||
const apisStr = (this.allowedApisText || '').trim()
|
||||
if (apisStr) {
|
||||
try {
|
||||
allowed_apis = JSON.parse(apisStr)
|
||||
if (!Array.isArray(allowed_apis)) {
|
||||
this.$Message.error('可用接口必须是 JSON 数组')
|
||||
this.saving = false
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
this.$Message.error('可用接口 JSON 格式错误')
|
||||
this.saving = false
|
||||
return
|
||||
}
|
||||
}
|
||||
const payload = { ...this.form, enabled_features, allowed_apis }
|
||||
try {
|
||||
const res = this.form.id ? await planServer.edit(payload) : await planServer.add(payload)
|
||||
if (res && res.code === 0) {
|
||||
|
||||
@@ -48,8 +48,7 @@ function buildProxyRoutes() {
|
||||
// 1. 提取 Token
|
||||
const token = extractToken(ctx);
|
||||
if (!token) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { ok: false, error_code: "TOKEN_MISSING", message: "缺少 Token" };
|
||||
ctx.fail("缺少 Token");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,8 +56,7 @@ function buildProxyRoutes() {
|
||||
const feature = pickFeature(spec);
|
||||
const authResult = await auth.verifyRequest({ token, feature, api_path: path });
|
||||
if (!authResult.ok) {
|
||||
ctx.status = 403;
|
||||
ctx.body = authResult;
|
||||
ctx.fail(authResult.message || "鉴权失败");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,9 +74,13 @@ function buildProxyRoutes() {
|
||||
auth_ctx: authResult.context,
|
||||
});
|
||||
|
||||
// 5. 原样返回上游响应
|
||||
ctx.status = result.status;
|
||||
ctx.body = result.data;
|
||||
// 5. 根据上游 Success 字段决定响应方式
|
||||
const upstream = result.data;
|
||||
if (upstream && upstream.Success === true) {
|
||||
ctx.success(upstream);
|
||||
} else {
|
||||
ctx.fail(upstream && upstream.Text ? upstream.Text : "上游请求失败", upstream);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const upstreamBaseUrl = config.upstream_api_url || "http://127.0.0.1:8888";
|
||||
* @param {object} params.auth_ctx - 鉴权上下文(verifyRequest 返回的 context)
|
||||
* @returns {object} { status, data, headers }
|
||||
*/
|
||||
async function forwardRequest({ api_path, method, query, body, headers, auth_ctx }) {
|
||||
async function forwardRequest({ api_path, method, query, body, headers, auth_ctx = {} }) {
|
||||
const url = `${upstreamBaseUrl}${api_path}`;
|
||||
const start = Date.now();
|
||||
let status_code = 0;
|
||||
@@ -66,6 +66,7 @@ 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 }) {
|
||||
try {
|
||||
const now = new Date();
|
||||
const call_date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
||||
await baseModel.biz_api_call_log.create({
|
||||
@@ -78,6 +79,10 @@ async function writeCallLog({ user_id, token_id, api_path, http_method, status_c
|
||||
call_date,
|
||||
created_at: now,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
logs.error("[proxy] 写调用日志失败", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { forwardRequest };
|
||||
|
||||
@@ -38,6 +38,6 @@ module.exports = {
|
||||
},
|
||||
|
||||
// 官方 API 上游地址(转发层目标)
|
||||
"upstream_api_url": "http://127.0.0.1:8888",
|
||||
"upstream_api_url": "http://113.44.162.180:7006",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user