1
This commit is contained in:
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* 从 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`);
|
||||
@@ -1,111 +0,0 @@
|
||||
-- WechatAdminWeb 订阅模块业务表(MySQL 8+)
|
||||
-- 执行前请确认库名;字段与 api/model/biz_*.js 中 tableName 完全一致:
|
||||
--
|
||||
-- Sequelize 模型名 物理表名(本文件与其它 _docs/sql)
|
||||
-- ---------------- ----------------
|
||||
-- biz_user biz_user
|
||||
-- biz_plan biz_plans
|
||||
-- biz_subscription biz_subscriptions
|
||||
-- biz_api_token biz_api_token
|
||||
-- biz_usage_monthly biz_usage_monthly
|
||||
--
|
||||
-- 另见:biz_audit_log(003_biz_audit.sql)、biz_api_call_log(biz_api_call_log.sql)
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 业务用户(与 sys_user 后台账号区分)
|
||||
CREATE TABLE IF NOT EXISTS `biz_user` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`mobile` VARCHAR(20) NULL DEFAULT NULL,
|
||||
`email` VARCHAR(120) NULL DEFAULT NULL,
|
||||
`company_name` VARCHAR(200) NULL DEFAULT NULL,
|
||||
`status` ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_biz_user_mobile` (`mobile`),
|
||||
KEY `idx_biz_user_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='业务用户';
|
||||
|
||||
-- 套餐
|
||||
CREATE TABLE IF NOT EXISTS `biz_plans` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`plan_code` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`plan_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`monthly_price` DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
`auth_fee` DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
`account_limit` INT NOT NULL DEFAULT 0,
|
||||
`active_user_limit` INT NOT NULL DEFAULT 0,
|
||||
`msg_quota` INT NOT NULL DEFAULT 0,
|
||||
`mass_quota` INT NOT NULL DEFAULT 0,
|
||||
`friend_quota` INT NOT NULL DEFAULT 0,
|
||||
`sns_quota` INT NOT NULL DEFAULT 0,
|
||||
`enabled_features` JSON NULL,
|
||||
`status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_biz_plans_code` (`plan_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='套餐';
|
||||
|
||||
-- 订阅实例
|
||||
CREATE TABLE IF NOT EXISTS `biz_subscriptions` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`plan_id` BIGINT UNSIGNED NOT NULL,
|
||||
`status` ENUM('pending', 'active', 'expired', 'cancelled') NOT NULL DEFAULT 'pending',
|
||||
`start_time` DATETIME NOT NULL,
|
||||
`end_time` DATETIME NOT NULL,
|
||||
`renew_mode` ENUM('manual', 'auto') NOT NULL DEFAULT 'manual',
|
||||
`payment_channel` ENUM('offline', 'pay_link') NULL DEFAULT NULL,
|
||||
`payment_ref` VARCHAR(200) NULL DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_biz_sub_user` (`user_id`),
|
||||
KEY `idx_biz_sub_plan` (`plan_id`),
|
||||
KEY `idx_biz_sub_status_end` (`status`, `end_time`),
|
||||
CONSTRAINT `fk_biz_sub_user` FOREIGN KEY (`user_id`) REFERENCES `biz_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_biz_sub_plan` FOREIGN KEY (`plan_id`) REFERENCES `biz_plans` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订阅';
|
||||
|
||||
-- API Token(库内仅存 hash;plan_id 冗余便于鉴权少联表)
|
||||
CREATE TABLE IF NOT EXISTS `biz_api_token` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`plan_id` BIGINT UNSIGNED NULL DEFAULT NULL,
|
||||
`token_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`token_hash` VARCHAR(64) NOT NULL,
|
||||
`status` ENUM('active', 'revoked', 'expired') NOT NULL DEFAULT 'active',
|
||||
`expire_at` DATETIME NOT NULL,
|
||||
`last_used_at` DATETIME NULL DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_biz_token_hash` (`token_hash`),
|
||||
KEY `idx_biz_token_user` (`user_id`),
|
||||
KEY `idx_biz_token_plan` (`plan_id`),
|
||||
CONSTRAINT `fk_biz_token_user` FOREIGN KEY (`user_id`) REFERENCES `biz_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_biz_token_plan` FOREIGN KEY (`plan_id`) REFERENCES `biz_plans` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API Token';
|
||||
|
||||
-- 月用量
|
||||
CREATE TABLE IF NOT EXISTS `biz_usage_monthly` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`plan_id` BIGINT UNSIGNED NOT NULL,
|
||||
`stat_month` CHAR(7) NOT NULL COMMENT 'YYYY-MM',
|
||||
`msg_count` INT NOT NULL DEFAULT 0,
|
||||
`mass_count` INT NOT NULL DEFAULT 0,
|
||||
`friend_count` INT NOT NULL DEFAULT 0,
|
||||
`sns_count` INT NOT NULL DEFAULT 0,
|
||||
`active_user_count` INT NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_biz_usage_user_month` (`user_id`, `stat_month`),
|
||||
KEY `idx_biz_usage_plan` (`plan_id`),
|
||||
CONSTRAINT `fk_biz_usage_user` FOREIGN KEY (`user_id`) REFERENCES `biz_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_biz_usage_plan` FOREIGN KEY (`plan_id`) REFERENCES `biz_plans` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='月用量';
|
||||
@@ -1,61 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 表 sys_menu(与 api/model/sys_menu.js 模型同名;执行前以 information_schema 确认实际表名)
|
||||
-- 订阅模块菜单插入脚本(字段与模型一致)
|
||||
-- path:不含斜杠(与首页 path=home 等约定一致);component 仍为 subscription/xxx 供前端映射
|
||||
-- 执行前请备份。若已存在同名「订阅管理」父菜单,请先删除子菜单再删父级,或改下面名称。
|
||||
-- 若数据库表另有 created_at / updated_at 等列,请在 INSERT 中补全或给默认值。
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 方案一(推荐):一级目录「订阅管理」+ 子菜单,parent_id 指向父记录 id
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(name, parent_id, icon, path, type, model_id, form_id, component, api_path, is_show_menu, is_show, sort)
|
||||
VALUES
|
||||
(
|
||||
'订阅管理',
|
||||
0,
|
||||
'ios-apps',
|
||||
'subscription',
|
||||
'菜单',
|
||||
0,
|
||||
0,
|
||||
'',
|
||||
'',
|
||||
1,
|
||||
1,
|
||||
900
|
||||
);
|
||||
|
||||
SET @sub_parent_id = LAST_INSERT_ID();
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(name, parent_id, icon, path, type, model_id, form_id, component, api_path, is_show_menu, is_show, sort)
|
||||
VALUES
|
||||
('运营看板', @sub_parent_id, 'ios-speedometer', 'subscription_dashboard', '页面', 0, 0, 'subscription/dashboard', '', 1, 1, 10),
|
||||
('业务用户', @sub_parent_id, 'ios-people', 'subscription_user', '页面', 0, 0, 'subscription/user', '', 1, 1, 20),
|
||||
('套餐管理', @sub_parent_id, 'ios-pricetags', 'subscription_plan', '页面', 0, 0, 'subscription/plan', '', 1, 1, 30),
|
||||
('订阅列表', @sub_parent_id, 'ios-list', 'subscription_subscription', '页面', 0, 0, 'subscription/subscription', '', 1, 1, 40),
|
||||
('API Token', @sub_parent_id, 'ios-key', 'subscription_token', '页面', 0, 0, 'subscription/token', '', 1, 1, 50),
|
||||
('支付确认', @sub_parent_id, 'ios-cash', 'subscription_payment', '页面', 0, 0, 'subscription/payment', '', 1, 1, 60),
|
||||
('月用量', @sub_parent_id, 'ios-analytics', 'subscription_usage', '页面', 0, 0, 'subscription/usage', '', 1, 1, 70),
|
||||
('审计日志', @sub_parent_id, 'ios-paper', 'subscription_audit', '页面', 0, 0, 'subscription/audit', '', 1, 1, 80);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 方案二(可选):全部挂在根节点 parent_id=0,无父级目录(与 component-map 仍一致)
|
||||
-- 若已执行方案一,请勿再执行下面语句,避免重复菜单。
|
||||
-- -----------------------------------------------------------------------------
|
||||
/*
|
||||
INSERT INTO sys_menu
|
||||
(name, parent_id, icon, path, type, model_id, form_id, component, api_path, is_show_menu, is_show, sort)
|
||||
VALUES
|
||||
('运营看板', 0, 'ios-speedometer', 'subscription_dashboard', '页面', 0, 0, 'subscription/dashboard', '', 1, 1, 910),
|
||||
('业务用户', 0, 'ios-people', 'subscription_user', '页面', 0, 0, 'subscription/user', '', 1, 1, 920),
|
||||
('套餐管理', 0, 'ios-pricetags', 'subscription_plan', '页面', 0, 0, 'subscription/plan', '', 1, 1, 930),
|
||||
('订阅列表', 0, 'ios-list', 'subscription_subscription', '页面', 0, 0, 'subscription/subscription', '', 1, 1, 940),
|
||||
('API Token', 0, 'ios-key', 'subscription_token', '页面', 0, 0, 'subscription/token', '', 1, 1, 950),
|
||||
('支付确认', 0, 'ios-cash', 'subscription_payment', '页面', 0, 0, 'subscription/payment', '', 1, 1, 960),
|
||||
('月用量', 0, 'ios-analytics', 'subscription_usage', '页面', 0, 0, 'subscription/usage', '', 1, 1, 970),
|
||||
('审计日志', 0, 'ios-paper', 'subscription_audit', '页面', 0, 0, 'subscription/audit', '', 1, 1, 980);
|
||||
*/
|
||||
@@ -1,17 +0,0 @@
|
||||
-- 审计日志(关键操作留痕)
|
||||
-- 模型:api/model/biz_audit_log.js → tableName: biz_audit_log
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `biz_audit_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`admin_user_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '后台操作者 sys_user.id,可空',
|
||||
`biz_user_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '相关业务用户',
|
||||
`action` VARCHAR(64) NOT NULL COMMENT '动作标识',
|
||||
`resource_type` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`resource_id` BIGINT UNSIGNED NULL DEFAULT NULL,
|
||||
`detail` JSON NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_biz_audit_action` (`action`),
|
||||
KEY `idx_biz_audit_created` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订阅模块审计';
|
||||
@@ -1,30 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 表 sys_menu(与 api/model/sys_menu.js 模型同名;执行前以 information_schema 确认实际表名)
|
||||
-- 首页菜单(字段与模型、现有库中菜单数据格式一致)
|
||||
--
|
||||
-- 参考库中同类数据示例:
|
||||
-- path:首页为 home(无前导 /);订阅子页为 /subscription/xxx
|
||||
-- component:与 admin/src/router/component-map.js 的 key 一致,可为 home/index 或 home/index.vue
|
||||
-- icon:Material 图标名如 md-home(与系统页 system/sys_user.vue 等同风格)
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(name, parent_id, icon, path, type, model_id, form_id, component, api_path, is_show_menu, is_show, sort)
|
||||
VALUES
|
||||
(
|
||||
'首页',
|
||||
0,
|
||||
'md-home',
|
||||
'home',
|
||||
'页面',
|
||||
0,
|
||||
0,
|
||||
'home/index.vue',
|
||||
'',
|
||||
1,
|
||||
1,
|
||||
0
|
||||
);
|
||||
|
||||
-- 若已存在 name=首页 或 path=home 的记录,请先删除或改值后再执行,避免重复。
|
||||
-- 若你希望 component 不带后缀,可将上面 'home/index.vue' 改为 'home/index'(component-map 两种 key 均已注册)。
|
||||
@@ -1,37 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 目标物理表名:biz_user(与模型 api/model/biz_user.js 的 tableName 一致;旧名 biz_users 仅为历史迁移说明)。
|
||||
-- 若早期已按旧版 001 建表 biz_users,而运行时报错查 biz_user,可执行本脚本迁移表名。
|
||||
-- 执行前备份数据库。若子表尚未创建,可跳过本脚本,直接 DROP biz_users 后重跑新版 001_biz_schema.sql。
|
||||
-- =============================================================================
|
||||
|
||||
-- 若不存在 biz_users 则无需执行
|
||||
-- 步骤:去外键引用 -> 重命名父表 ->(子表 FK 仍指向旧名时需重建,MySQL 8 重命名父表后约束名可能需检查)
|
||||
|
||||
-- 1) 删除引用 biz_users 的外键(子表若已存在)
|
||||
SET @db = DATABASE();
|
||||
|
||||
SET @sql = (
|
||||
SELECT GROUP_CONCAT(CONCAT('ALTER TABLE `', TABLE_NAME, '` DROP FOREIGN KEY `', CONSTRAINT_NAME, '`') SEPARATOR '; ')
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND REFERENCED_TABLE_NAME = 'biz_users'
|
||||
);
|
||||
-- 若上面为空则无子表 FK,可手动执行下面 RENAME
|
||||
|
||||
-- 手工示例(按实际约束名调整):
|
||||
-- ALTER TABLE `biz_subscriptions` DROP FOREIGN KEY `fk_biz_sub_user`;
|
||||
-- ALTER TABLE `biz_api_token` DROP FOREIGN KEY `fk_biz_token_user`;
|
||||
-- ALTER TABLE `biz_usage_monthly` DROP FOREIGN KEY `fk_biz_usage_user`;
|
||||
|
||||
-- 2) 重命名业务用户表
|
||||
-- RENAME TABLE `biz_users` TO `biz_user`;
|
||||
|
||||
-- 3) 重新添加外键(与 001_biz_schema.sql 一致)
|
||||
-- ALTER TABLE `biz_subscriptions`
|
||||
-- ADD CONSTRAINT `fk_biz_sub_user` FOREIGN KEY (`user_id`) REFERENCES `biz_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-- ALTER TABLE `biz_api_token`
|
||||
-- ADD CONSTRAINT `fk_biz_token_user` FOREIGN KEY (`user_id`) REFERENCES `biz_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-- ALTER TABLE `biz_usage_monthly`
|
||||
-- ADD CONSTRAINT `fk_biz_usage_user` FOREIGN KEY (`user_id`) REFERENCES `biz_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- 更简单做法(无重要数据):DROP 子表与 biz_users,再执行新版 001_biz_schema.sql 全量重建。
|
||||
@@ -1,28 +0,0 @@
|
||||
-- 表名与 api/model 中 tableName 一致:biz_user、biz_plans、biz_subscriptions、biz_api_token、biz_usage_monthly、biz_audit_log
|
||||
-- 已有库若 `created_at` / `updated_at` 无默认值,插入会失败(模型已关闭 Sequelize timestamps 且未声明时间字段时依赖库默认值)。
|
||||
-- 按需对已有表执行(新库直接执行 001/003 即可,无需本文件)。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `biz_user`
|
||||
MODIFY `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE `biz_plans`
|
||||
MODIFY `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE `biz_subscriptions`
|
||||
MODIFY `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE `biz_api_token`
|
||||
MODIFY `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE `biz_usage_monthly`
|
||||
MODIFY `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
|
||||
|
||||
ALTER TABLE `biz_audit_log`
|
||||
MODIFY `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -1,6 +0,0 @@
|
||||
订阅模块数据库脚本建议执行顺序:
|
||||
1. 001_biz_schema.sql — 业务表(用户/套餐/订阅/Token/月用量)
|
||||
2. 003_biz_audit.sql — 审计表 biz_audit_log
|
||||
3. 002_biz_menu_seed.sql — 管理端菜单(按实际 sys_menu 表结构调整列后执行)
|
||||
|
||||
说明:若 002 与现有 sys_menu 字段不一致,请在库中对照 sys_menu 结构增删列后再插入。
|
||||
@@ -1,15 +0,0 @@
|
||||
-- 模型 biz_plan → 表 biz_plans;模型 biz_usage_monthly → 表 biz_usage_monthly(与 api/model 中 tableName 一致)
|
||||
-- 在「与后端 app 相同」的 MySQL 库中执行(否则会一直报 Unknown column)。
|
||||
-- 可重复执行:若列已存在会报错,可忽略对应语句或改用 scripts/migrate_biz_plan_api_columns.js
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- biz_plans(分两条,方便只缺其中一列时单独执行)
|
||||
ALTER TABLE `biz_plans`
|
||||
ADD COLUMN `allowed_apis` JSON DEFAULT NULL COMMENT '可访问的接口路径列表,null=不限制';
|
||||
ALTER TABLE `biz_plans`
|
||||
ADD COLUMN `api_call_quota` INT NOT NULL DEFAULT 0 COMMENT '每月API总调用次数上限,0=不限制';
|
||||
|
||||
-- biz_usage_monthly
|
||||
ALTER TABLE `biz_usage_monthly`
|
||||
ADD COLUMN `api_call_count` INT NOT NULL DEFAULT 0 COMMENT '当月API转发总调用次数';
|
||||
@@ -1,17 +0,0 @@
|
||||
-- 模型:api/model/biz_api_call_log.js → tableName: biz_api_call_log
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `biz_api_call_log` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '业务用户ID',
|
||||
`token_id` BIGINT UNSIGNED NOT NULL COMMENT '使用的Token ID',
|
||||
`api_path` VARCHAR(200) NOT NULL COMMENT '接口路径',
|
||||
`http_method` VARCHAR(10) NOT NULL DEFAULT 'POST',
|
||||
`status_code` INT NOT NULL DEFAULT 0 COMMENT '上游返回的HTTP状态码',
|
||||
`response_time` INT NOT NULL DEFAULT 0 COMMENT '上游响应耗时ms',
|
||||
`call_date` DATE NOT NULL COMMENT '调用日期',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_user_date` (`user_id`, `call_date`),
|
||||
INDEX `idx_api_date` (`api_path`, `call_date`),
|
||||
INDEX `idx_user_api` (`user_id`, `api_path`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API调用日志';
|
||||
99
_docs/sql/biz_plans.sql
Normal file
99
_docs/sql/biz_plans.sql
Normal file
@@ -0,0 +1,99 @@
|
||||
-- 套餐表 biz_plan(与 api/model/biz_plan.js 一致,tableName: biz_plan)
|
||||
-- MySQL 8+,字符集 utf8mb4
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `biz_plan` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`plan_code` VARCHAR(64) NOT NULL COMMENT '唯一编码',
|
||||
`plan_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '展示名称',
|
||||
`monthly_price` DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '月价',
|
||||
`auth_fee` DECIMAL(12, 2) NOT NULL DEFAULT 0.00 COMMENT '授权费',
|
||||
`account_limit` INT NOT NULL DEFAULT 0 COMMENT '账号上限,0 表示由业务解释',
|
||||
`active_user_limit` INT NOT NULL DEFAULT 0 COMMENT '活跃用户数上限',
|
||||
`msg_quota` INT NOT NULL DEFAULT 0 COMMENT '消息额度',
|
||||
`mass_quota` INT NOT NULL DEFAULT 0 COMMENT '群发额度',
|
||||
`friend_quota` INT NOT NULL DEFAULT 0 COMMENT '加好友额度',
|
||||
`sns_quota` INT NOT NULL DEFAULT 0 COMMENT '朋友圈额度',
|
||||
`enabled_features` JSON NULL COMMENT '功能点(JSON),null 表示不限制',
|
||||
`allowed_apis` JSON NULL COMMENT '可访问接口路径 JSON 数组,null 表示不限制',
|
||||
`api_call_quota` INT NOT NULL DEFAULT 0 COMMENT '每月 API 转发总次数上限,0 表示不限制',
|
||||
`status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_biz_plan_plan_code` (`plan_code`),
|
||||
KEY `idx_biz_plan_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ---------- 若已有旧表缺列,可单独执行(列已存在会报错,可忽略)----------
|
||||
-- ALTER TABLE `biz_plan` ADD COLUMN `enabled_features` JSON NULL COMMENT '功能点(JSON),null 表示不限制';
|
||||
-- ALTER TABLE `biz_plan` ADD COLUMN `allowed_apis` JSON NULL COMMENT '可访问接口路径 JSON 数组,null 表示不限制';
|
||||
-- ALTER TABLE `biz_plan` ADD COLUMN `api_call_quota` INT NOT NULL DEFAULT 0 COMMENT '每月 API 转发总次数上限,0 表示不限制';
|
||||
|
||||
-- ---------- 示例数据(可选;plan_code 冲突则先删或改编码)----------
|
||||
INSERT INTO `biz_plan` (
|
||||
`plan_code`,
|
||||
`plan_name`,
|
||||
`monthly_price`,
|
||||
`auth_fee`,
|
||||
`account_limit`,
|
||||
`active_user_limit`,
|
||||
`msg_quota`,
|
||||
`mass_quota`,
|
||||
`friend_quota`,
|
||||
`sns_quota`,
|
||||
`enabled_features`,
|
||||
`allowed_apis`,
|
||||
`api_call_quota`,
|
||||
`status`
|
||||
) VALUES
|
||||
(
|
||||
'plan_junior',
|
||||
'初级版',
|
||||
299.00,
|
||||
0.00,
|
||||
3,
|
||||
50,
|
||||
3000,
|
||||
100,
|
||||
200,
|
||||
100,
|
||||
JSON_ARRAY('登录', '好友', '消息', '管理'),
|
||||
JSON_ARRAY('/login/GetLoginStatus', '/login/DeviceLogin', '/message/SendText', '/friend/GetContactList'),
|
||||
50000,
|
||||
'active'
|
||||
),
|
||||
(
|
||||
'plan_senior',
|
||||
'高级版',
|
||||
899.00,
|
||||
0.00,
|
||||
20,
|
||||
500,
|
||||
50000,
|
||||
2000,
|
||||
5000,
|
||||
2000,
|
||||
JSON_ARRAY(
|
||||
'登录', '好友', '消息', '群聊', '朋友圈', '小程序', '管理',
|
||||
'设备', '收藏', '视频号', '标签', '支付', '企业微信', '商店', '其他', 'Ws'
|
||||
),
|
||||
NULL,
|
||||
500000,
|
||||
'active'
|
||||
),
|
||||
(
|
||||
'plan_custom',
|
||||
'定制版',
|
||||
0.00,
|
||||
0.00,
|
||||
9999,
|
||||
9999,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
0,
|
||||
'active'
|
||||
);
|
||||
@@ -1,96 +0,0 @@
|
||||
-- 套餐示例数据(表 biz_plans,模型 biz_plan / api/model/biz_plan.js)
|
||||
-- 执行库须与项目 NODE_ENV 对应库一致;若表无 allowed_apis / api_call_quota 请先执行 alter_plan_api_permission.sql
|
||||
-- plan_code 唯一:重复执行会主键/唯一冲突,可先 DELETE WHERE plan_code IN (...) 或改用下面「按编码更新」段落
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========== 1)插入三条示例套餐 ==========
|
||||
INSERT INTO `biz_plans` (
|
||||
`plan_code`,
|
||||
`plan_name`,
|
||||
`monthly_price`,
|
||||
`auth_fee`,
|
||||
`account_limit`,
|
||||
`active_user_limit`,
|
||||
`msg_quota`,
|
||||
`mass_quota`,
|
||||
`friend_quota`,
|
||||
`sns_quota`,
|
||||
`enabled_features`,
|
||||
`allowed_apis`,
|
||||
`api_call_quota`,
|
||||
`status`
|
||||
) VALUES
|
||||
(
|
||||
'plan_junior',
|
||||
'初级版',
|
||||
299.00,
|
||||
0.00,
|
||||
3,
|
||||
50,
|
||||
3000,
|
||||
100,
|
||||
200,
|
||||
100,
|
||||
JSON_ARRAY('登录', '好友', '消息', '管理'),
|
||||
JSON_ARRAY('/login/GetLoginStatus', '/login/DeviceLogin', '/message/SendText', '/friend/GetContactList'),
|
||||
50000,
|
||||
'active'
|
||||
),
|
||||
(
|
||||
'plan_senior',
|
||||
'高级版',
|
||||
899.00,
|
||||
0.00,
|
||||
20,
|
||||
500,
|
||||
50000,
|
||||
2000,
|
||||
5000,
|
||||
2000,
|
||||
JSON_ARRAY(
|
||||
'登录', '好友', '消息', '群聊', '朋友圈', '小程序', '管理',
|
||||
'设备', '收藏', '视频号', '标签', '支付', '企业微信', '商店', '其他', 'Ws'
|
||||
),
|
||||
NULL,
|
||||
500000,
|
||||
'active'
|
||||
),
|
||||
(
|
||||
'plan_custom',
|
||||
'定制版',
|
||||
0.00,
|
||||
0.00,
|
||||
9999,
|
||||
9999,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
0,
|
||||
'active'
|
||||
);
|
||||
|
||||
-- ========== 2)仅当列已存在时:按 plan_code 幂等覆盖(无则 INSERT 需自行补全列)==========
|
||||
-- 若你已手工删过上面 INSERT,可用下面语句改已有编码的数据:
|
||||
|
||||
-- UPDATE `biz_plans` SET
|
||||
-- `plan_name` = '初级版',
|
||||
-- `monthly_price` = 299.00,
|
||||
-- `msg_quota` = 3000,
|
||||
-- `api_call_quota` = 50000,
|
||||
-- `allowed_apis` = JSON_ARRAY('/login/GetLoginStatus', '/message/SendText'),
|
||||
-- `status` = 'active'
|
||||
-- WHERE `plan_code` = 'plan_junior';
|
||||
|
||||
-- ========== 3)常用维护语句 ==========
|
||||
-- 下架套餐
|
||||
-- UPDATE `biz_plans` SET `status` = 'inactive' WHERE `plan_code` = 'plan_junior';
|
||||
|
||||
-- 某套餐改为「全接口 + 不限次数」(接口层 allowed_apis=null、api_call_quota=0 表示不限制)
|
||||
-- UPDATE `biz_plans` SET `allowed_apis` = NULL, `api_call_quota` = 0 WHERE `plan_code` = 'plan_senior';
|
||||
|
||||
-- 清空测试套餐(谨慎:有外键订阅时可能拦截)
|
||||
-- DELETE FROM `biz_plans` WHERE `plan_code` IN ('plan_junior', 'plan_senior', 'plan_custom');
|
||||
9028
_docs/swagger.json
9028
_docs/swagger.json
File diff suppressed because it is too large
Load Diff
@@ -78,7 +78,7 @@
|
||||
新建迁移或 SQL 脚本(建议 `_docs/sql/` 或 `migrations/`,与团队习惯一致)包含:
|
||||
|
||||
- `biz_user`(与模型 `api/model/biz_user.js` 的 `tableName` 一致;与 `sys_user` 后台账号区分)
|
||||
- `biz_plans`
|
||||
- `biz_plan`
|
||||
- `biz_subscriptions`(字段含 `renew_mode`、`payment_channel`、`payment_ref` 等)
|
||||
- `biz_api_token`(`token_hash`、`plan_id` 可选冗余)
|
||||
- `biz_usage_monthly`(`stat_month` YYYY-MM)
|
||||
|
||||
@@ -51,7 +51,6 @@ module.exports = (db) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "biz_plans",
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* 为 biz_plans / biz_usage_monthly 补齐套餐 API 权限相关字段(可重复执行)
|
||||
*/
|
||||
const mysql = require("mysql2/promise");
|
||||
const config = require("../config/config");
|
||||
|
||||
async function column_exists(conn, schema, table, column) {
|
||||
const [rows] = await conn.query(
|
||||
`SELECT COUNT(*) AS c FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
||||
[schema, table, column]
|
||||
);
|
||||
return Number(rows[0].c) > 0;
|
||||
}
|
||||
|
||||
async function table_exists(conn, schema, table) {
|
||||
const [rows] = await conn.query(
|
||||
`SELECT COUNT(*) AS c FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
|
||||
[schema, table]
|
||||
);
|
||||
return Number(rows[0].c) > 0;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const db = config.db;
|
||||
if (!db || !db.database) {
|
||||
throw new Error("config.db.database 未配置");
|
||||
}
|
||||
const conn = await mysql.createConnection({
|
||||
host: db.host,
|
||||
port: db.port || 3306,
|
||||
user: db.username,
|
||||
password: db.password,
|
||||
database: db.database,
|
||||
});
|
||||
const schema = db.database;
|
||||
try {
|
||||
if (!(await table_exists(conn, schema, "biz_plans"))) {
|
||||
console.warn("跳过 biz_plans:当前库中不存在该表(请确认连接的是已部署订阅模块的库)");
|
||||
} else if (!(await column_exists(conn, schema, "biz_plans", "allowed_apis"))) {
|
||||
await conn.query(
|
||||
"ALTER TABLE `biz_plans` ADD COLUMN `allowed_apis` JSON DEFAULT NULL COMMENT '可访问的接口路径列表,null=不限制'"
|
||||
);
|
||||
console.log("已添加 biz_plans.allowed_apis");
|
||||
}
|
||||
if (
|
||||
(await table_exists(conn, schema, "biz_plans")) &&
|
||||
!(await column_exists(conn, schema, "biz_plans", "api_call_quota"))
|
||||
) {
|
||||
await conn.query(
|
||||
"ALTER TABLE `biz_plans` ADD COLUMN `api_call_quota` INT NOT NULL DEFAULT 0 COMMENT '每月API总调用次数上限,0=不限制'"
|
||||
);
|
||||
console.log("已添加 biz_plans.api_call_quota");
|
||||
}
|
||||
if (!(await table_exists(conn, schema, "biz_usage_monthly"))) {
|
||||
console.warn("跳过 biz_usage_monthly:当前库中不存在该表");
|
||||
} else if (!(await column_exists(conn, schema, "biz_usage_monthly", "api_call_count"))) {
|
||||
await conn.query(
|
||||
"ALTER TABLE `biz_usage_monthly` ADD COLUMN `api_call_count` INT NOT NULL DEFAULT 0 COMMENT '当月API转发总调用次数'"
|
||||
);
|
||||
console.log("已添加 biz_usage_monthly.api_call_count");
|
||||
}
|
||||
console.log("字段检查完成");
|
||||
} finally {
|
||||
await conn.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user