diff --git a/_docs/sql/002_subscription_menu_seed.sql b/_docs/sql/002_subscription_menu_seed.sql new file mode 100644 index 0000000..0fe341e --- /dev/null +++ b/_docs/sql/002_subscription_menu_seed.sql @@ -0,0 +1,59 @@ +-- ============================================================================= +-- sys_menu 订阅模块菜单插入脚本(字段与 api/model/sys_menu.js 一致) +-- 执行前请备份。若已存在同名「订阅管理」父菜单,请先删除子菜单再删父级,或改下面名称。 +-- 若数据库表另有 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); +*/ diff --git a/_docs/sql/README_migrate_order.txt b/_docs/sql/README_migrate_order.txt new file mode 100644 index 0000000..2c51043 --- /dev/null +++ b/_docs/sql/README_migrate_order.txt @@ -0,0 +1,6 @@ +订阅模块数据库脚本建议执行顺序: +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 结构增删列后再插入。 diff --git a/_docs/订阅模块实施计划.md b/_docs/订阅模块实施计划.md index 1164860..1445006 100644 --- a/_docs/订阅模块实施计划.md +++ b/_docs/订阅模块实施计划.md @@ -119,11 +119,11 @@ | 页面 | 路由 component key(示例) | 功能 | |------|------------------------------|------| -| 用户列表/编辑 | `biz/user` | 表格 + 搜索 + 抽屉表单 | -| 套餐列表/编辑 | `biz/plan` | JSON 功能点编辑器(简易 textarea 或 key-value) | -| 订阅操作 | `biz/subscription` | 开通/升级/续费/取消向导或表单 | -| Token | `biz/token` | 列表、创建(展示一次明文)、吊销 | -| 支付确认 | `biz/payment` | 线下确认、链接确认表单 | +| 用户列表/编辑 | `subscription/user` | 表格 + 搜索 + 抽屉表单 | +| 套餐列表/编辑 | `subscription/plan` | JSON 功能点编辑器(简易 textarea 或 key-value) | +| 订阅操作 | `subscription/subscription` | 开通/升级/续费/取消向导或表单 | +| Token | `subscription/token` | 列表、创建(展示一次明文)、吊销 | +| 支付确认 | `subscription/payment` | 线下确认、链接确认表单 | 菜单数据若来自后端 `sys_menu`,需在库中插入对应菜单项,`component` 与 `component-map.js` **key 一致**。 diff --git a/admin/src/api/subscription/audit_server.js b/admin/src/api/subscription/audit_server.js new file mode 100644 index 0000000..3150bcf --- /dev/null +++ b/admin/src/api/subscription/audit_server.js @@ -0,0 +1,11 @@ +class AuditServer { + async page(row) { + return window.framework.http.post("/biz_audit_log/page", row); + } + + async exportRows(row) { + return window.framework.http.post("/biz_audit_log/export", row); + } +} + +export default new AuditServer(); diff --git a/admin/src/api/subscription/dashboard_server.js b/admin/src/api/subscription/dashboard_server.js new file mode 100644 index 0000000..d868cb8 --- /dev/null +++ b/admin/src/api/subscription/dashboard_server.js @@ -0,0 +1,7 @@ +class DashboardServer { + async summary() { + return window.framework.http.get("/biz_dashboard/summary", {}); + } +} + +export default new DashboardServer(); diff --git a/admin/src/api/biz/biz_payment_server.js b/admin/src/api/subscription/payment_server.js similarity index 77% rename from admin/src/api/biz/biz_payment_server.js rename to admin/src/api/subscription/payment_server.js index 4116879..cce4259 100644 --- a/admin/src/api/biz/biz_payment_server.js +++ b/admin/src/api/subscription/payment_server.js @@ -1,4 +1,4 @@ -class BizPaymentServer { +class PaymentServer { async confirmOffline(row) { return window.framework.http.post("/biz_payment/confirm-offline", row); } @@ -8,4 +8,4 @@ class BizPaymentServer { } } -export default new BizPaymentServer(); +export default new PaymentServer(); diff --git a/admin/src/api/biz/biz_plan_server.js b/admin/src/api/subscription/plan_server.js similarity index 77% rename from admin/src/api/biz/biz_plan_server.js rename to admin/src/api/subscription/plan_server.js index ecf4b78..f8bc915 100644 --- a/admin/src/api/biz/biz_plan_server.js +++ b/admin/src/api/subscription/plan_server.js @@ -1,4 +1,4 @@ -class BizPlanServer { +class PlanServer { async page(row) { return window.framework.http.post("/biz_plan/page", row); } @@ -22,6 +22,10 @@ class BizPlanServer { async all() { return window.framework.http.get("/biz_plan/all", {}); } + + async exportRows(row) { + return window.framework.http.post("/biz_plan/export", row); + } } -export default new BizPlanServer(); +export default new PlanServer(); diff --git a/admin/src/api/biz/biz_subscription_server.js b/admin/src/api/subscription/subscriptions_server.js similarity index 77% rename from admin/src/api/biz/biz_subscription_server.js rename to admin/src/api/subscription/subscriptions_server.js index 20a114c..0817f5b 100644 --- a/admin/src/api/biz/biz_subscription_server.js +++ b/admin/src/api/subscription/subscriptions_server.js @@ -1,4 +1,4 @@ -class BizSubscriptionServer { +class SubscriptionsServer { async page(row) { return window.framework.http.post("/biz_subscription/page", row); } @@ -22,6 +22,10 @@ class BizSubscriptionServer { async cancel(row) { return window.framework.http.post("/biz_subscription/cancel", row); } + + async exportRows(row) { + return window.framework.http.post("/biz_subscription/export", row); + } } -export default new BizSubscriptionServer(); +export default new SubscriptionsServer(); diff --git a/admin/src/api/biz/biz_token_server.js b/admin/src/api/subscription/token_server.js similarity index 64% rename from admin/src/api/biz/biz_token_server.js rename to admin/src/api/subscription/token_server.js index b6dce12..3990e52 100644 --- a/admin/src/api/biz/biz_token_server.js +++ b/admin/src/api/subscription/token_server.js @@ -1,4 +1,4 @@ -class BizTokenServer { +class TokenServer { async page(row) { return window.framework.http.post("/biz_token/page", row); } @@ -10,6 +10,10 @@ class BizTokenServer { async revoke(row) { return window.framework.http.post("/biz_token/revoke", row); } + + async exportRows(row) { + return window.framework.http.post("/biz_token/export", row); + } } -export default new BizTokenServer(); +export default new TokenServer(); diff --git a/admin/src/api/subscription/usage_server.js b/admin/src/api/subscription/usage_server.js new file mode 100644 index 0000000..c38b0c1 --- /dev/null +++ b/admin/src/api/subscription/usage_server.js @@ -0,0 +1,23 @@ +class UsageServer { + async page(row) { + return window.framework.http.post("/biz_usage/page", row); + } + + async add(row) { + return window.framework.http.post("/biz_usage/add", row); + } + + async edit(row) { + return window.framework.http.post("/biz_usage/edit", row); + } + + async del(row) { + return window.framework.http.post("/biz_usage/del", row); + } + + async exportRows(row) { + return window.framework.http.post("/biz_usage/export", row); + } +} + +export default new UsageServer(); diff --git a/admin/src/api/biz/biz_user_server.js b/admin/src/api/subscription/user_server.js similarity index 68% rename from admin/src/api/biz/biz_user_server.js rename to admin/src/api/subscription/user_server.js index d28a777..b00411b 100644 --- a/admin/src/api/biz/biz_user_server.js +++ b/admin/src/api/subscription/user_server.js @@ -1,4 +1,4 @@ -class BizUserServer { +class UserServer { async page(row) { return window.framework.http.post("/biz_user/page", row); } @@ -22,6 +22,14 @@ class BizUserServer { async disable(row) { return window.framework.http.post("/biz_user/disable", row); } + + async exportRows(row) { + return window.framework.http.post("/biz_user/export", row); + } + + async revokeAllTokens(row) { + return window.framework.http.post("/biz_user/revoke_all_tokens", row); + } } -export default new BizUserServer(); +export default new UserServer(); diff --git a/admin/src/router/component-map.js b/admin/src/router/component-map.js index a7814c8..aa9701b 100644 --- a/admin/src/router/component-map.js +++ b/admin/src/router/component-map.js @@ -1,18 +1,24 @@ // 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue) import TestPage from '../views/test/test.vue' -import BizUsers from '../views/biz/biz_users.vue' -import BizPlans from '../views/biz/biz_plans.vue' -import BizSubscriptions from '../views/biz/biz_subscriptions.vue' -import BizTokens from '../views/biz/biz_tokens.vue' -import BizPayment from '../views/biz/biz_payment.vue' +import SubscriptionDashboard from '../views/subscription/dashboard.vue' +import SubscriptionUsers from '../views/subscription/users.vue' +import SubscriptionPlans from '../views/subscription/plans.vue' +import SubscriptionRecords from '../views/subscription/subscriptions.vue' +import SubscriptionTokens from '../views/subscription/tokens.vue' +import SubscriptionPayment from '../views/subscription/payment.vue' +import SubscriptionUsage from '../views/subscription/usage.vue' +import SubscriptionAuditLog from '../views/subscription/audit_log.vue' const componentMap = { 'test/test': TestPage, - 'biz/user': BizUsers, - 'biz/plan': BizPlans, - 'biz/subscription': BizSubscriptions, - 'biz/token': BizTokens, - 'biz/payment': BizPayment, + 'subscription/dashboard': SubscriptionDashboard, + 'subscription/user': SubscriptionUsers, + 'subscription/plan': SubscriptionPlans, + 'subscription/subscription': SubscriptionRecords, + 'subscription/token': SubscriptionTokens, + 'subscription/payment': SubscriptionPayment, + 'subscription/usage': SubscriptionUsage, + 'subscription/audit': SubscriptionAuditLog, } export default componentMap; diff --git a/admin/src/utils/csvExport.js b/admin/src/utils/csvExport.js new file mode 100644 index 0000000..39a3578 --- /dev/null +++ b/admin/src/utils/csvExport.js @@ -0,0 +1,24 @@ +/** 将对象数组导出为 CSV 并触发浏览器下载(兼容 POST /export 返回的 rows) */ +export function downloadCsvFromRows(rows, filename = "export.csv") { + if (!rows || !rows.length) { + return false; + } + const keys = Object.keys(rows[0]); + const esc = (v) => { + const s = v === null || v === undefined ? "" : String(v); + if (/[",\n\r]/.test(s)) return `"${s.replace(/"/g, '""')}"`; + return s; + }; + const lines = [keys.join(",")]; + for (const r of rows) { + lines.push(keys.map((k) => esc(r[k])).join(",")); + } + const blob = new Blob(["\ufeff" + lines.join("\n")], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + return true; +} diff --git a/admin/src/views/subscription/audit_log.vue b/admin/src/views/subscription/audit_log.vue new file mode 100644 index 0000000..07f565c --- /dev/null +++ b/admin/src/views/subscription/audit_log.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/admin/src/views/subscription/dashboard.vue b/admin/src/views/subscription/dashboard.vue new file mode 100644 index 0000000..ebea99e --- /dev/null +++ b/admin/src/views/subscription/dashboard.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/admin/src/views/biz/biz_payment.vue b/admin/src/views/subscription/payment.vue similarity index 77% rename from admin/src/views/biz/biz_payment.vue rename to admin/src/views/subscription/payment.vue index 1e76557..8789599 100644 --- a/admin/src/views/biz/biz_payment.vue +++ b/admin/src/views/subscription/payment.vue @@ -1,11 +1,11 @@ diff --git a/admin/src/views/biz/biz_users.vue b/admin/src/views/subscription/users.vue similarity index 77% rename from admin/src/views/biz/biz_users.vue rename to admin/src/views/subscription/users.vue index 07eefbe..15f38eb 100644 --- a/admin/src/views/biz/biz_users.vue +++ b/admin/src/views/subscription/users.vue @@ -1,11 +1,12 @@