This commit is contained in:
张成
2026-04-01 11:03:40 +08:00
parent 30a909762e
commit 6f61287c70
17 changed files with 100 additions and 9683 deletions

View File

@@ -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`);

View File

@@ -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_log003_biz_audit.sql、biz_api_call_logbiz_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库内仅存 hashplan_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='月用量';

View File

@@ -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);
*/

View File

@@ -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='订阅模块审计';

View File

@@ -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
-- iconMaterial 图标名如 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 均已注册)。

View File

@@ -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 全量重建。

View File

@@ -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;

View File

@@ -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 结构增删列后再插入。

View File

@@ -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转发总调用次数';

View File

@@ -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
View 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'
);

View File

@@ -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');

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -51,7 +51,6 @@ module.exports = (db) => {
},
},
{
tableName: "biz_plans",
timestamps: false,
underscored: true,
}

View File

@@ -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);
});