1
This commit is contained in:
59
_docs/sql/002_subscription_menu_seed.sql
Normal file
59
_docs/sql/002_subscription_menu_seed.sql
Normal file
@@ -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);
|
||||||
|
*/
|
||||||
6
_docs/sql/README_migrate_order.txt
Normal file
6
_docs/sql/README_migrate_order.txt
Normal file
@@ -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 结构增删列后再插入。
|
||||||
@@ -119,11 +119,11 @@
|
|||||||
|
|
||||||
| 页面 | 路由 component key(示例) | 功能 |
|
| 页面 | 路由 component key(示例) | 功能 |
|
||||||
|------|------------------------------|------|
|
|------|------------------------------|------|
|
||||||
| 用户列表/编辑 | `biz/user` | 表格 + 搜索 + 抽屉表单 |
|
| 用户列表/编辑 | `subscription/user` | 表格 + 搜索 + 抽屉表单 |
|
||||||
| 套餐列表/编辑 | `biz/plan` | JSON 功能点编辑器(简易 textarea 或 key-value) |
|
| 套餐列表/编辑 | `subscription/plan` | JSON 功能点编辑器(简易 textarea 或 key-value) |
|
||||||
| 订阅操作 | `biz/subscription` | 开通/升级/续费/取消向导或表单 |
|
| 订阅操作 | `subscription/subscription` | 开通/升级/续费/取消向导或表单 |
|
||||||
| Token | `biz/token` | 列表、创建(展示一次明文)、吊销 |
|
| Token | `subscription/token` | 列表、创建(展示一次明文)、吊销 |
|
||||||
| 支付确认 | `biz/payment` | 线下确认、链接确认表单 |
|
| 支付确认 | `subscription/payment` | 线下确认、链接确认表单 |
|
||||||
|
|
||||||
菜单数据若来自后端 `sys_menu`,需在库中插入对应菜单项,`component` 与 `component-map.js` **key 一致**。
|
菜单数据若来自后端 `sys_menu`,需在库中插入对应菜单项,`component` 与 `component-map.js` **key 一致**。
|
||||||
|
|
||||||
|
|||||||
11
admin/src/api/subscription/audit_server.js
Normal file
11
admin/src/api/subscription/audit_server.js
Normal file
@@ -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();
|
||||||
7
admin/src/api/subscription/dashboard_server.js
Normal file
7
admin/src/api/subscription/dashboard_server.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class DashboardServer {
|
||||||
|
async summary() {
|
||||||
|
return window.framework.http.get("/biz_dashboard/summary", {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DashboardServer();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class BizPaymentServer {
|
class PaymentServer {
|
||||||
async confirmOffline(row) {
|
async confirmOffline(row) {
|
||||||
return window.framework.http.post("/biz_payment/confirm-offline", 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();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class BizPlanServer {
|
class PlanServer {
|
||||||
async page(row) {
|
async page(row) {
|
||||||
return window.framework.http.post("/biz_plan/page", row);
|
return window.framework.http.post("/biz_plan/page", row);
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,10 @@ class BizPlanServer {
|
|||||||
async all() {
|
async all() {
|
||||||
return window.framework.http.get("/biz_plan/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();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class BizSubscriptionServer {
|
class SubscriptionsServer {
|
||||||
async page(row) {
|
async page(row) {
|
||||||
return window.framework.http.post("/biz_subscription/page", row);
|
return window.framework.http.post("/biz_subscription/page", row);
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,10 @@ class BizSubscriptionServer {
|
|||||||
async cancel(row) {
|
async cancel(row) {
|
||||||
return window.framework.http.post("/biz_subscription/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();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class BizTokenServer {
|
class TokenServer {
|
||||||
async page(row) {
|
async page(row) {
|
||||||
return window.framework.http.post("/biz_token/page", row);
|
return window.framework.http.post("/biz_token/page", row);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,10 @@ class BizTokenServer {
|
|||||||
async revoke(row) {
|
async revoke(row) {
|
||||||
return window.framework.http.post("/biz_token/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();
|
||||||
23
admin/src/api/subscription/usage_server.js
Normal file
23
admin/src/api/subscription/usage_server.js
Normal file
@@ -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();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class BizUserServer {
|
class UserServer {
|
||||||
async page(row) {
|
async page(row) {
|
||||||
return window.framework.http.post("/biz_user/page", row);
|
return window.framework.http.post("/biz_user/page", row);
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,14 @@ class BizUserServer {
|
|||||||
async disable(row) {
|
async disable(row) {
|
||||||
return window.framework.http.post("/biz_user/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();
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
// 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue)
|
// 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue)
|
||||||
import TestPage from '../views/test/test.vue'
|
import TestPage from '../views/test/test.vue'
|
||||||
import BizUsers from '../views/biz/biz_users.vue'
|
import SubscriptionDashboard from '../views/subscription/dashboard.vue'
|
||||||
import BizPlans from '../views/biz/biz_plans.vue'
|
import SubscriptionUsers from '../views/subscription/users.vue'
|
||||||
import BizSubscriptions from '../views/biz/biz_subscriptions.vue'
|
import SubscriptionPlans from '../views/subscription/plans.vue'
|
||||||
import BizTokens from '../views/biz/biz_tokens.vue'
|
import SubscriptionRecords from '../views/subscription/subscriptions.vue'
|
||||||
import BizPayment from '../views/biz/biz_payment.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 = {
|
const componentMap = {
|
||||||
'test/test': TestPage,
|
'test/test': TestPage,
|
||||||
'biz/user': BizUsers,
|
'subscription/dashboard': SubscriptionDashboard,
|
||||||
'biz/plan': BizPlans,
|
'subscription/user': SubscriptionUsers,
|
||||||
'biz/subscription': BizSubscriptions,
|
'subscription/plan': SubscriptionPlans,
|
||||||
'biz/token': BizTokens,
|
'subscription/subscription': SubscriptionRecords,
|
||||||
'biz/payment': BizPayment,
|
'subscription/token': SubscriptionTokens,
|
||||||
|
'subscription/payment': SubscriptionPayment,
|
||||||
|
'subscription/usage': SubscriptionUsage,
|
||||||
|
'subscription/audit': SubscriptionAuditLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default componentMap;
|
export default componentMap;
|
||||||
|
|||||||
24
admin/src/utils/csvExport.js
Normal file
24
admin/src/utils/csvExport.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
131
admin/src/views/subscription/audit_log.vue
Normal file
131
admin/src/views/subscription/audit_log.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sub-page">
|
||||||
|
<div class="sub-toolbar">
|
||||||
|
<h2 class="sub-title">审计日志</h2>
|
||||||
|
<Button type="primary" @click="load(1)">刷新</Button>
|
||||||
|
<Button class="ml8" @click="doExport">导出 CSV</Button>
|
||||||
|
</div>
|
||||||
|
<div class="sub-search">
|
||||||
|
<Form inline>
|
||||||
|
<FormItem label="动作">
|
||||||
|
<Select v-model="param.seachOption.key" style="width: 140px">
|
||||||
|
<Option value="action">action</Option>
|
||||||
|
<Option value="resource_type">resource_type</Option>
|
||||||
|
</Select>
|
||||||
|
<Input v-model="param.seachOption.value" class="ml8" style="width: 220px" placeholder="模糊/精确" />
|
||||||
|
</FormItem>
|
||||||
|
<Button type="primary" @click="load(1)">查询</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<Table :columns="columns" :data="rows" border stripe />
|
||||||
|
<div class="sub-page-bar">
|
||||||
|
<Page
|
||||||
|
:total="total"
|
||||||
|
:current="param.pageOption.page"
|
||||||
|
:page-size="param.pageOption.pageSize"
|
||||||
|
show-total
|
||||||
|
@on-change="onPage"
|
||||||
|
@on-page-size-change="onSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import auditServer from '@/api/subscription/audit_server.js'
|
||||||
|
import { downloadCsvFromRows } from '@/utils/csvExport.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubscriptionAuditLog',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
total: 0,
|
||||||
|
param: {
|
||||||
|
seachOption: { key: 'action', value: '' },
|
||||||
|
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
columns() {
|
||||||
|
return [
|
||||||
|
{ title: 'ID', key: 'id', width: 80 },
|
||||||
|
{ title: '后台用户', key: 'admin_user_id', width: 100 },
|
||||||
|
{ title: '业务用户', key: 'biz_user_id', width: 100 },
|
||||||
|
{ title: '动作', key: 'action', minWidth: 160 },
|
||||||
|
{ title: '资源类型', key: 'resource_type', width: 120 },
|
||||||
|
{ title: '资源ID', key: 'resource_id', width: 90 },
|
||||||
|
{
|
||||||
|
title: '详情',
|
||||||
|
key: 'detail',
|
||||||
|
minWidth: 200,
|
||||||
|
render: (h, p) => {
|
||||||
|
const d = p.row.detail
|
||||||
|
const s = d == null ? '' : typeof d === 'string' ? d : JSON.stringify(d)
|
||||||
|
return h('span', { attrs: { title: s } }, s.length > 80 ? s.slice(0, 80) + '…' : s)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '时间', key: 'created_at', minWidth: 160 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load(1)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async load(page) {
|
||||||
|
if (page) this.param.pageOption.page = page
|
||||||
|
const res = await auditServer.page({ param: this.param })
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.rows = res.data.rows || []
|
||||||
|
this.total = res.data.count || 0
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '加载失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPage(p) {
|
||||||
|
this.param.pageOption.page = p
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
onSize(s) {
|
||||||
|
this.param.pageOption.pageSize = s
|
||||||
|
this.load(1)
|
||||||
|
},
|
||||||
|
async doExport() {
|
||||||
|
const res = await auditServer.exportRows({ param: this.param })
|
||||||
|
if (res && res.code === 0 && res.data && res.data.rows) {
|
||||||
|
const rows = res.data.rows.map((r) => ({
|
||||||
|
...r,
|
||||||
|
detail: r.detail == null ? '' : typeof r.detail === 'object' ? JSON.stringify(r.detail) : r.detail,
|
||||||
|
}))
|
||||||
|
downloadCsvFromRows(rows, 'audit_log.csv')
|
||||||
|
this.$Message.success('已导出')
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sub-page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.sub-title {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.ml8 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.sub-search {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.sub-page-bar {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
admin/src/views/subscription/dashboard.vue
Normal file
95
admin/src/views/subscription/dashboard.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sub-page">
|
||||||
|
<div class="sub-toolbar">
|
||||||
|
<h2 class="sub-title">订阅运营看板</h2>
|
||||||
|
<Button type="primary" @click="load">刷新</Button>
|
||||||
|
</div>
|
||||||
|
<Row :gutter="16" v-if="stats">
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">业务用户总数</p><p class="num">{{ stats.users.total }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">正常用户</p><p class="num">{{ stats.users.active }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">上线套餐数</p><p class="num">{{ stats.plans.active }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">有效 Token</p><p class="num">{{ stats.tokens.active }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">待支付订阅</p><p class="num">{{ stats.subscriptions.pending }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">生效中订阅</p><p class="num">{{ stats.subscriptions.active }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover><p class="lbl">已过期订阅</p><p class="num">{{ stats.subscriptions.expired }}</p></Card>
|
||||||
|
</Col>
|
||||||
|
<Col span="6">
|
||||||
|
<Card dis-hover
|
||||||
|
><p class="lbl">7 天内到期(活跃)</p><p class="num warn">{{ stats.subscriptions.renew_within_7d }}</p></Card
|
||||||
|
>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<p v-else class="muted">加载中…</p>
|
||||||
|
<p class="muted small">数据时间:{{ stats && stats.server_time }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import dashboardServer from '@/api/subscription/dashboard_server.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubscriptionDashboard',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
stats: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async load() {
|
||||||
|
const res = await dashboardServer.summary()
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.stats = res.data
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '加载失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sub-page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.sub-title {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.lbl {
|
||||||
|
margin: 0;
|
||||||
|
color: #888;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.num.warn {
|
||||||
|
color: #ed4014;
|
||||||
|
}
|
||||||
|
.muted {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.small {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="biz-page">
|
<div class="sub-page">
|
||||||
<h2 class="biz-title">支付确认(轻量)</h2>
|
<h2 class="sub-title">支付确认(轻量)</h2>
|
||||||
<p class="biz-desc">将 <code>pending</code> 订阅置为 <code>active</code>,并写入支付单号。</p>
|
<p class="sub-desc">将 <code>pending</code> 订阅置为 <code>active</code>,并写入支付单号。</p>
|
||||||
<Card dis-hover title="线下确认" style="max-width: 520px; margin-bottom: 16px">
|
<Card dis-hover title="线下确认" style="max-width: 520px; margin-bottom: 16px">
|
||||||
<Form :label-width="110">
|
<Form :label-width="110">
|
||||||
<FormItem label="订阅ID">
|
<FormItem label="订阅ID">
|
||||||
<Input v-model="offline.subscription_id" type="number" placeholder="biz_subscriptions.id" />
|
<Input v-model="offline.subscription_id" type="number" placeholder="订阅记录 id" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="支付单号">
|
<FormItem label="支付单号">
|
||||||
<Input v-model="offline.payment_ref" placeholder="流水号/凭证号" />
|
<Input v-model="offline.payment_ref" placeholder="流水号/凭证号" />
|
||||||
@@ -15,6 +15,10 @@
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Alert type="info" show-icon style="max-width: 720px; margin-bottom: 16px">
|
||||||
|
对外鉴权接口(免登录,见白名单):<code>POST /api/auth/verify</code>,body 示例:
|
||||||
|
<code>{"token":"明文","feature":"msg","usage_delta":{"msg":1}}</code>
|
||||||
|
</Alert>
|
||||||
<Card dis-hover title="链接支付确认" style="max-width: 520px">
|
<Card dis-hover title="链接支付确认" style="max-width: 520px">
|
||||||
<Form :label-width="110">
|
<Form :label-width="110">
|
||||||
<FormItem label="订阅ID">
|
<FormItem label="订阅ID">
|
||||||
@@ -32,10 +36,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bizPaymentServer from '@/api/biz/biz_payment_server.js'
|
import paymentServer from '@/api/subscription/payment_server.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BizPayment',
|
name: 'SubscriptionPayment',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
offline: { subscription_id: '', payment_ref: '' },
|
offline: { subscription_id: '', payment_ref: '' },
|
||||||
@@ -48,7 +52,7 @@ export default {
|
|||||||
async doOffline() {
|
async doOffline() {
|
||||||
this.loading1 = true
|
this.loading1 = true
|
||||||
try {
|
try {
|
||||||
const res = await bizPaymentServer.confirmOffline({
|
const res = await paymentServer.confirmOffline({
|
||||||
subscription_id: Number(this.offline.subscription_id),
|
subscription_id: Number(this.offline.subscription_id),
|
||||||
payment_ref: this.offline.payment_ref,
|
payment_ref: this.offline.payment_ref,
|
||||||
})
|
})
|
||||||
@@ -64,7 +68,7 @@ export default {
|
|||||||
async doLink() {
|
async doLink() {
|
||||||
this.loading2 = true
|
this.loading2 = true
|
||||||
try {
|
try {
|
||||||
const res = await bizPaymentServer.confirmLink({
|
const res = await paymentServer.confirmLink({
|
||||||
subscription_id: Number(this.link.subscription_id),
|
subscription_id: Number(this.link.subscription_id),
|
||||||
payment_ref: this.link.payment_ref,
|
payment_ref: this.link.payment_ref,
|
||||||
})
|
})
|
||||||
@@ -82,14 +86,14 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.biz-page {
|
.sub-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.biz-title {
|
.sub-title {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.biz-desc {
|
.sub-desc {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="biz-page">
|
<div class="sub-page">
|
||||||
<div class="biz-toolbar">
|
<div class="sub-toolbar">
|
||||||
<h2 class="biz-title">套餐</h2>
|
<h2 class="sub-title">套餐</h2>
|
||||||
<Button type="primary" @click="openEdit(null)">新增套餐</Button>
|
<Button type="primary" @click="openEdit(null)">新增套餐</Button>
|
||||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||||
|
<Button class="ml8" @click="doExport">导出 CSV</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="biz-search">
|
<div class="sub-search">
|
||||||
<Form inline :label-width="70">
|
<Form inline :label-width="70">
|
||||||
<FormItem label="条件">
|
<FormItem label="条件">
|
||||||
<Select v-model="param.seachOption.key" style="width: 140px">
|
<Select v-model="param.seachOption.key" style="width: 140px">
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<Table :columns="columns" :data="rows" border stripe />
|
<Table :columns="columns" :data="rows" border stripe />
|
||||||
<div class="biz-page-bar">
|
<div class="sub-page-bar">
|
||||||
<Page
|
<Page
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="param.pageOption.page"
|
:current="param.pageOption.page"
|
||||||
@@ -77,10 +78,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bizPlanServer from '@/api/biz/biz_plan_server.js'
|
import planServer from '@/api/subscription/plan_server.js'
|
||||||
|
import { downloadCsvFromRows } from '@/utils/csvExport.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BizPlans',
|
name: 'SubscriptionPlans',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
@@ -128,7 +130,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async load(page) {
|
async load(page) {
|
||||||
if (page) this.param.pageOption.page = page
|
if (page) this.param.pageOption.page = page
|
||||||
const res = await bizPlanServer.page({ param: this.param })
|
const res = await planServer.page({ param: this.param })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.rows = res.data.rows || []
|
this.rows = res.data.rows || []
|
||||||
this.total = res.data.count || 0
|
this.total = res.data.count || 0
|
||||||
@@ -191,7 +193,7 @@ export default {
|
|||||||
}
|
}
|
||||||
const payload = { ...this.form, enabled_features }
|
const payload = { ...this.form, enabled_features }
|
||||||
try {
|
try {
|
||||||
const res = this.form.id ? await bizPlanServer.edit(payload) : await bizPlanServer.add(payload)
|
const res = this.form.id ? await planServer.edit(payload) : await planServer.add(payload)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('保存成功')
|
this.$Message.success('保存成功')
|
||||||
this.modal = false
|
this.modal = false
|
||||||
@@ -205,7 +207,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async toggle(row) {
|
async toggle(row) {
|
||||||
const res = await bizPlanServer.toggle({ id: row.id })
|
const res = await planServer.toggle({ id: row.id })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('状态已更新为 ' + (res.data && res.data.status))
|
this.$Message.success('状态已更新为 ' + (res.data && res.data.status))
|
||||||
this.load()
|
this.load()
|
||||||
@@ -218,7 +220,7 @@ export default {
|
|||||||
title: '删除套餐',
|
title: '删除套餐',
|
||||||
content: '确认删除?若已被订阅引用可能失败。',
|
content: '确认删除?若已被订阅引用可能失败。',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
const res = await bizPlanServer.del({ id: row.id })
|
const res = await planServer.del({ id: row.id })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('已删除')
|
this.$Message.success('已删除')
|
||||||
this.load(1)
|
this.load(1)
|
||||||
@@ -228,18 +230,27 @@ export default {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async doExport() {
|
||||||
|
const res = await planServer.exportRows({ param: this.param })
|
||||||
|
if (res && res.code === 0 && res.data && res.data.rows) {
|
||||||
|
downloadCsvFromRows(res.data.rows, 'plans.csv')
|
||||||
|
this.$Message.success('已导出')
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.biz-page {
|
.sub-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.biz-toolbar {
|
.sub-toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.biz-title {
|
.sub-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 16px 0 0;
|
margin: 0 16px 0 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -248,10 +259,10 @@ export default {
|
|||||||
.ml8 {
|
.ml8 {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.biz-search {
|
.sub-search {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.biz-page-bar {
|
.sub-page-bar {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="biz-page">
|
<div class="sub-page">
|
||||||
<div class="biz-toolbar">
|
<div class="sub-toolbar">
|
||||||
<h2 class="biz-title">订阅</h2>
|
<h2 class="sub-title">订阅</h2>
|
||||||
<Button type="primary" @click="openOpen">开通订阅</Button>
|
<Button type="primary" @click="openOpen">开通订阅</Button>
|
||||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||||
|
<Button class="ml8" @click="doExport">导出 CSV</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="biz-search">
|
<div class="sub-search">
|
||||||
<Form inline>
|
<Form inline>
|
||||||
<FormItem label="用户ID">
|
<FormItem label="用户ID">
|
||||||
<Input v-model="param.seachOption.value" style="width: 140px" placeholder="筛选 user_id" />
|
<Input v-model="param.seachOption.value" style="width: 140px" placeholder="筛选 user_id" />
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<Table :columns="columns" :data="rows" border stripe />
|
<Table :columns="columns" :data="rows" border stripe />
|
||||||
<div class="biz-page-bar">
|
<div class="sub-page-bar">
|
||||||
<Page
|
<Page
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="param.pageOption.page"
|
:current="param.pageOption.page"
|
||||||
@@ -76,10 +77,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bizSubscriptionServer from '@/api/biz/biz_subscription_server.js'
|
import subscriptionsServer from '@/api/subscription/subscriptions_server.js'
|
||||||
|
import { downloadCsvFromRows } from '@/utils/csvExport.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BizSubscriptions',
|
name: 'SubscriptionRecords',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
@@ -127,7 +129,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async load(page) {
|
async load(page) {
|
||||||
if (page) this.param.pageOption.page = page
|
if (page) this.param.pageOption.page = page
|
||||||
const res = await bizSubscriptionServer.page({ param: this.param })
|
const res = await subscriptionsServer.page({ param: this.param })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.rows = res.data.rows || []
|
this.rows = res.data.rows || []
|
||||||
this.total = res.data.count || 0
|
this.total = res.data.count || 0
|
||||||
@@ -176,7 +178,7 @@ export default {
|
|||||||
payment_channel: this.openForm.payment_channel || null,
|
payment_channel: this.openForm.payment_channel || null,
|
||||||
payment_ref: this.openForm.payment_ref || null,
|
payment_ref: this.openForm.payment_ref || null,
|
||||||
}
|
}
|
||||||
const res = await bizSubscriptionServer.open(body)
|
const res = await subscriptionsServer.open(body)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('已创建订阅')
|
this.$Message.success('已创建订阅')
|
||||||
this.openModal = false
|
this.openModal = false
|
||||||
@@ -197,7 +199,7 @@ export default {
|
|||||||
if (!this.currentRow) return
|
if (!this.currentRow) return
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
const res = await bizSubscriptionServer.renew({
|
const res = await subscriptionsServer.renew({
|
||||||
subscription_id: this.currentRow.id,
|
subscription_id: this.currentRow.id,
|
||||||
end_time: this.renewForm.end_time,
|
end_time: this.renewForm.end_time,
|
||||||
})
|
})
|
||||||
@@ -221,7 +223,7 @@ export default {
|
|||||||
if (!this.currentRow) return
|
if (!this.currentRow) return
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
const res = await bizSubscriptionServer.upgrade({
|
const res = await subscriptionsServer.upgrade({
|
||||||
subscription_id: this.currentRow.id,
|
subscription_id: this.currentRow.id,
|
||||||
new_plan_id: Number(this.upgradeForm.new_plan_id),
|
new_plan_id: Number(this.upgradeForm.new_plan_id),
|
||||||
start_time: this.upgradeForm.start_time || undefined,
|
start_time: this.upgradeForm.start_time || undefined,
|
||||||
@@ -243,7 +245,7 @@ export default {
|
|||||||
title: '取消订阅',
|
title: '取消订阅',
|
||||||
content: '确认取消?',
|
content: '确认取消?',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
const res = await bizSubscriptionServer.cancel({ subscription_id: row.id })
|
const res = await subscriptionsServer.cancel({ subscription_id: row.id })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('已取消')
|
this.$Message.success('已取消')
|
||||||
this.load(1)
|
this.load(1)
|
||||||
@@ -253,18 +255,27 @@ export default {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async doExport() {
|
||||||
|
const res = await subscriptionsServer.exportRows({ param: this.param })
|
||||||
|
if (res && res.code === 0 && res.data && res.data.rows) {
|
||||||
|
downloadCsvFromRows(res.data.rows, 'subscriptions.csv')
|
||||||
|
this.$Message.success('已导出')
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.biz-page {
|
.sub-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.biz-toolbar {
|
.sub-toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.biz-title {
|
.sub-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 16px 0 0;
|
margin: 0 16px 0 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -273,7 +284,7 @@ export default {
|
|||||||
.ml8 {
|
.ml8 {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.biz-page-bar {
|
.sub-page-bar {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="biz-page">
|
<div class="sub-page">
|
||||||
<div class="biz-toolbar">
|
<div class="sub-toolbar">
|
||||||
<h2 class="biz-title">API Token</h2>
|
<h2 class="sub-title">API Token</h2>
|
||||||
<Button type="primary" @click="openCreate">创建 Token</Button>
|
<Button type="primary" @click="openCreate">创建 Token</Button>
|
||||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||||
|
<Button class="ml8" @click="doExport">导出 CSV</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="biz-search">
|
<div class="sub-search">
|
||||||
<Form inline>
|
<Form inline>
|
||||||
<FormItem label="用户ID">
|
<FormItem label="用户ID">
|
||||||
<Input v-model="param.seachOption.value" style="width: 140px" />
|
<Input v-model="param.seachOption.value" style="width: 140px" />
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<Table :columns="columns" :data="rows" border stripe />
|
<Table :columns="columns" :data="rows" border stripe />
|
||||||
<div class="biz-page-bar">
|
<div class="sub-page-bar">
|
||||||
<Page
|
<Page
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="param.pageOption.page"
|
:current="param.pageOption.page"
|
||||||
@@ -50,10 +51,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bizTokenServer from '@/api/biz/biz_token_server.js'
|
import tokenServer from '@/api/subscription/token_server.js'
|
||||||
|
import { downloadCsvFromRows } from '@/utils/csvExport.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BizTokens',
|
name: 'SubscriptionTokens',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
@@ -104,7 +106,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async load(page) {
|
async load(page) {
|
||||||
if (page) this.param.pageOption.page = page
|
if (page) this.param.pageOption.page = page
|
||||||
const res = await bizTokenServer.page({ param: this.param })
|
const res = await tokenServer.page({ param: this.param })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.rows = res.data.rows || []
|
this.rows = res.data.rows || []
|
||||||
this.total = res.data.count || 0
|
this.total = res.data.count || 0
|
||||||
@@ -133,7 +135,7 @@ export default {
|
|||||||
async submitCreate() {
|
async submitCreate() {
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
const res = await bizTokenServer.create({
|
const res = await tokenServer.create({
|
||||||
user_id: Number(this.createForm.user_id),
|
user_id: Number(this.createForm.user_id),
|
||||||
token_name: this.createForm.token_name || 'default',
|
token_name: this.createForm.token_name || 'default',
|
||||||
expire_at: this.createForm.expire_at,
|
expire_at: this.createForm.expire_at,
|
||||||
@@ -156,7 +158,7 @@ export default {
|
|||||||
title: '吊销 Token',
|
title: '吊销 Token',
|
||||||
content: '确认吊销?',
|
content: '确认吊销?',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
const res = await bizTokenServer.revoke({ id: row.id })
|
const res = await tokenServer.revoke({ id: row.id })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('已吊销')
|
this.$Message.success('已吊销')
|
||||||
this.load(1)
|
this.load(1)
|
||||||
@@ -166,18 +168,27 @@ export default {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async doExport() {
|
||||||
|
const res = await tokenServer.exportRows({ param: this.param })
|
||||||
|
if (res && res.code === 0 && res.data && res.data.rows) {
|
||||||
|
downloadCsvFromRows(res.data.rows, 'api_tokens.csv')
|
||||||
|
this.$Message.success('已导出')
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.biz-page {
|
.sub-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.biz-toolbar {
|
.sub-toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.biz-title {
|
.sub-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 16px 0 0;
|
margin: 0 16px 0 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -186,7 +197,7 @@ export default {
|
|||||||
.ml8 {
|
.ml8 {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.biz-page-bar {
|
.sub-page-bar {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
208
admin/src/views/subscription/usage.vue
Normal file
208
admin/src/views/subscription/usage.vue
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sub-page">
|
||||||
|
<div class="sub-toolbar">
|
||||||
|
<h2 class="sub-title">月用量</h2>
|
||||||
|
<Button type="primary" @click="openEdit(null)">新增</Button>
|
||||||
|
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||||
|
<Button class="ml8" @click="doExport">导出 CSV</Button>
|
||||||
|
</div>
|
||||||
|
<div class="sub-search">
|
||||||
|
<Form inline>
|
||||||
|
<FormItem label="条件">
|
||||||
|
<Select v-model="param.seachOption.key" style="width: 140px">
|
||||||
|
<Option value="user_id">用户ID</Option>
|
||||||
|
<Option value="stat_month">月份</Option>
|
||||||
|
<Option value="plan_id">套餐ID</Option>
|
||||||
|
</Select>
|
||||||
|
<Input v-model="param.seachOption.value" class="ml8" style="width: 200px" />
|
||||||
|
</FormItem>
|
||||||
|
<Button type="primary" @click="load(1)">查询</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<Table :columns="columns" :data="rows" border stripe />
|
||||||
|
<div class="sub-page-bar">
|
||||||
|
<Page
|
||||||
|
:total="total"
|
||||||
|
:current="param.pageOption.page"
|
||||||
|
:page-size="param.pageOption.pageSize"
|
||||||
|
show-total
|
||||||
|
@on-change="onPage"
|
||||||
|
@on-page-size-change="onSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal v-model="modal" :title="form.id ? '编辑用量' : '新增用量'" width="640" :loading="saving" @on-ok="save">
|
||||||
|
<Form ref="formRef" :model="form" :rules="rules" :label-width="120">
|
||||||
|
<FormItem label="用户ID" prop="user_id"><Input v-model="form.user_id" type="number" /></FormItem>
|
||||||
|
<FormItem label="套餐ID" prop="plan_id"><Input v-model="form.plan_id" type="number" /></FormItem>
|
||||||
|
<FormItem label="月份 YYYY-MM" prop="stat_month"><Input v-model="form.stat_month" placeholder="2025-03" /></FormItem>
|
||||||
|
<FormItem label="msg_count"><Input v-model="form.msg_count" type="number" /></FormItem>
|
||||||
|
<FormItem label="mass_count"><Input v-model="form.mass_count" type="number" /></FormItem>
|
||||||
|
<FormItem label="friend_count"><Input v-model="form.friend_count" type="number" /></FormItem>
|
||||||
|
<FormItem label="sns_count"><Input v-model="form.sns_count" type="number" /></FormItem>
|
||||||
|
<FormItem label="active_user_count"><Input v-model="form.active_user_count" type="number" /></FormItem>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import usageServer from '@/api/subscription/usage_server.js'
|
||||||
|
import { downloadCsvFromRows } from '@/utils/csvExport.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SubscriptionUsage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rows: [],
|
||||||
|
total: 0,
|
||||||
|
param: {
|
||||||
|
seachOption: { key: 'stat_month', value: '' },
|
||||||
|
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
},
|
||||||
|
modal: false,
|
||||||
|
saving: false,
|
||||||
|
form: {},
|
||||||
|
rules: {
|
||||||
|
user_id: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||||
|
plan_id: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||||
|
stat_month: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
columns() {
|
||||||
|
return [
|
||||||
|
{ title: 'ID', key: 'id', width: 70 },
|
||||||
|
{ title: '用户', key: 'user_id', width: 90 },
|
||||||
|
{ title: '套餐', key: 'plan_id', width: 90 },
|
||||||
|
{ title: '月份', key: 'stat_month', width: 100 },
|
||||||
|
{ title: 'msg', key: 'msg_count', width: 80 },
|
||||||
|
{ title: 'mass', key: 'mass_count', width: 80 },
|
||||||
|
{ title: 'friend', key: 'friend_count', width: 80 },
|
||||||
|
{ title: 'sns', key: 'sns_count', width: 80 },
|
||||||
|
{ title: 'active_user', key: 'active_user_count', width: 110 },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'a',
|
||||||
|
width: 140,
|
||||||
|
render: (h, p) =>
|
||||||
|
h('div', [
|
||||||
|
h('Button', { props: { type: 'info', size: 'small' }, on: { click: () => this.openEdit(p.row) } }, '编辑'),
|
||||||
|
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doDel(p.row) } }, '删除'),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load(1)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async load(page) {
|
||||||
|
if (page) this.param.pageOption.page = page
|
||||||
|
const res = await usageServer.page({ param: this.param })
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.rows = res.data.rows || []
|
||||||
|
this.total = res.data.count || 0
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '加载失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPage(p) {
|
||||||
|
this.param.pageOption.page = p
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
onSize(s) {
|
||||||
|
this.param.pageOption.pageSize = s
|
||||||
|
this.load(1)
|
||||||
|
},
|
||||||
|
openEdit(row) {
|
||||||
|
if (row) {
|
||||||
|
this.form = { ...row }
|
||||||
|
} else {
|
||||||
|
this.form = {
|
||||||
|
user_id: '',
|
||||||
|
plan_id: '',
|
||||||
|
stat_month: '',
|
||||||
|
msg_count: 0,
|
||||||
|
mass_count: 0,
|
||||||
|
friend_count: 0,
|
||||||
|
sns_count: 0,
|
||||||
|
active_user_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.modal = true
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.saving = true
|
||||||
|
this.$refs.formRef.validate(async (ok) => {
|
||||||
|
if (!ok) {
|
||||||
|
this.saving = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = { ...this.form }
|
||||||
|
payload.user_id = Number(payload.user_id)
|
||||||
|
payload.plan_id = Number(payload.plan_id)
|
||||||
|
try {
|
||||||
|
const res = this.form.id ? await usageServer.edit(payload) : await usageServer.add(payload)
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.$Message.success('保存成功')
|
||||||
|
this.modal = false
|
||||||
|
this.load(1)
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
doDel(row) {
|
||||||
|
this.$Modal.confirm({
|
||||||
|
title: '删除',
|
||||||
|
content: '确认删除该用量记录?',
|
||||||
|
onOk: async () => {
|
||||||
|
const res = await usageServer.del({ id: row.id })
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.$Message.success('已删除')
|
||||||
|
this.load(1)
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async doExport() {
|
||||||
|
const res = await usageServer.exportRows({ param: this.param })
|
||||||
|
if (res && res.code === 0 && res.data && res.data.rows) {
|
||||||
|
downloadCsvFromRows(res.data.rows, 'usage_monthly.csv')
|
||||||
|
this.$Message.success('已导出')
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sub-page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.sub-title {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.ml8 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.sub-search {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.sub-page-bar {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="biz-page">
|
<div class="sub-page">
|
||||||
<div class="biz-toolbar">
|
<div class="sub-toolbar">
|
||||||
<h2 class="biz-title">业务用户</h2>
|
<h2 class="sub-title">业务用户</h2>
|
||||||
<Button type="primary" @click="openEdit(null)">新增</Button>
|
<Button type="primary" @click="openEdit(null)">新增</Button>
|
||||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||||
|
<Button class="ml8" @click="doExport">导出 CSV</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="biz-search">
|
<div class="sub-search">
|
||||||
<Form inline :label-width="70">
|
<Form inline :label-width="70">
|
||||||
<FormItem label="条件">
|
<FormItem label="条件">
|
||||||
<Select v-model="param.seachOption.key" style="width: 140px">
|
<Select v-model="param.seachOption.key" style="width: 140px">
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<Table :columns="columns" :data="rows" border stripe />
|
<Table :columns="columns" :data="rows" border stripe />
|
||||||
<div class="biz-page-bar">
|
<div class="sub-page-bar">
|
||||||
<Page
|
<Page
|
||||||
:total="total"
|
:total="total"
|
||||||
:current="param.pageOption.page"
|
:current="param.pageOption.page"
|
||||||
@@ -63,10 +64,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bizUserServer from '@/api/biz/biz_user_server.js'
|
import userServer from '@/api/subscription/user_server.js'
|
||||||
|
import { downloadCsvFromRows } from '@/utils/csvExport.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BizUsers',
|
name: 'SubscriptionUsers',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows: [],
|
||||||
@@ -104,7 +106,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'a',
|
key: 'a',
|
||||||
width: 220,
|
width: 340,
|
||||||
render: (h, p) => {
|
render: (h, p) => {
|
||||||
return h('div', [
|
return h('div', [
|
||||||
h(
|
h(
|
||||||
@@ -133,6 +135,15 @@ export default {
|
|||||||
},
|
},
|
||||||
'禁用'
|
'禁用'
|
||||||
),
|
),
|
||||||
|
h(
|
||||||
|
'Button',
|
||||||
|
{
|
||||||
|
props: { type: 'default', size: 'small' },
|
||||||
|
class: { ml8: true },
|
||||||
|
on: { click: () => this.revokeAllTokens(p.row) },
|
||||||
|
},
|
||||||
|
'吊销全部Token'
|
||||||
|
),
|
||||||
h(
|
h(
|
||||||
'Button',
|
'Button',
|
||||||
{
|
{
|
||||||
@@ -154,7 +165,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async load(page) {
|
async load(page) {
|
||||||
if (page) this.param.pageOption.page = page
|
if (page) this.param.pageOption.page = page
|
||||||
const res = await bizUserServer.page({ param: this.param })
|
const res = await userServer.page({ param: this.param })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.rows = res.data.rows || []
|
this.rows = res.data.rows || []
|
||||||
this.total = res.data.count || 0
|
this.total = res.data.count || 0
|
||||||
@@ -187,8 +198,8 @@ export default {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = this.form.id
|
const res = this.form.id
|
||||||
? await bizUserServer.edit(this.form)
|
? await userServer.edit(this.form)
|
||||||
: await bizUserServer.add(this.form)
|
: await userServer.add(this.form)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('保存成功')
|
this.$Message.success('保存成功')
|
||||||
this.modal = false
|
this.modal = false
|
||||||
@@ -202,7 +213,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async showDetail(row) {
|
async showDetail(row) {
|
||||||
const res = await bizUserServer.detail(row.id)
|
const res = await userServer.detail(row.id)
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.detail = res.data
|
this.detail = res.data
|
||||||
this.detailVisible = true
|
this.detailVisible = true
|
||||||
@@ -215,7 +226,7 @@ export default {
|
|||||||
title: '禁用用户',
|
title: '禁用用户',
|
||||||
content: '确认禁用该用户?',
|
content: '确认禁用该用户?',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
const res = await bizUserServer.disable({ id: row.id })
|
const res = await userServer.disable({ id: row.id })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('已禁用')
|
this.$Message.success('已禁用')
|
||||||
this.load(1)
|
this.load(1)
|
||||||
@@ -230,7 +241,7 @@ export default {
|
|||||||
title: '删除用户',
|
title: '删除用户',
|
||||||
content: '确认删除?若存在订阅/Token 可能受外键限制。',
|
content: '确认删除?若存在订阅/Token 可能受外键限制。',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
const res = await bizUserServer.del({ id: row.id })
|
const res = await userServer.del({ id: row.id })
|
||||||
if (res && res.code === 0) {
|
if (res && res.code === 0) {
|
||||||
this.$Message.success('已删除')
|
this.$Message.success('已删除')
|
||||||
this.load(1)
|
this.load(1)
|
||||||
@@ -240,18 +251,43 @@ export default {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async doExport() {
|
||||||
|
const res = await userServer.exportRows({ param: this.param })
|
||||||
|
if (res && res.code === 0 && res.data && res.data.rows) {
|
||||||
|
downloadCsvFromRows(res.data.rows, 'users.csv')
|
||||||
|
this.$Message.success('已导出')
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
revokeAllTokens(row) {
|
||||||
|
this.$Modal.confirm({
|
||||||
|
title: '吊销全部 Token',
|
||||||
|
content: '将吊销该用户下所有有效 Token,是否继续?',
|
||||||
|
onOk: async () => {
|
||||||
|
const res = await userServer.revokeAllTokens({ user_id: row.id })
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
const n = (res.data && res.data.revoked) != null ? res.data.revoked : 0
|
||||||
|
this.$Message.success('已吊销 ' + n + ' 条')
|
||||||
|
this.load(1)
|
||||||
|
} else {
|
||||||
|
this.$Message.error((res && res.message) || '操作失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.biz-page {
|
.sub-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.biz-toolbar {
|
.sub-toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.biz-title {
|
.sub-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 16px 0 0;
|
margin: 0 16px 0 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -260,10 +296,10 @@ export default {
|
|||||||
.ml8 {
|
.ml8 {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.biz-search {
|
.sub-search {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.biz-page-bar {
|
.sub-page-bar {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -13,11 +13,11 @@ function num(v) {
|
|||||||
return Number.isNaN(n) ? 0 : n;
|
return Number.isNaN(n) ? 0 : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 0 表示无限制(不校验) */
|
/** 0 表示无限制;已用量超过 limit 则超限(允许用到等于 limit) */
|
||||||
function quotaExceeded(used, limit) {
|
function quotaExceeded(used, limit) {
|
||||||
const li = num(limit);
|
const li = num(limit);
|
||||||
if (li <= 0) return false;
|
if (li <= 0) return false;
|
||||||
return num(used) >= li;
|
return num(used) > li;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ module.exports = {
|
|||||||
// 数据库配置(测试环境)
|
// 数据库配置(测试环境)
|
||||||
// ============================================
|
// ============================================
|
||||||
"db": {
|
"db": {
|
||||||
"username": "wms_node",
|
"username": "wechat_admin",
|
||||||
"password": "SLrhewk5eePr5Z7Y",
|
"password": "YJEjLXnxJ3WraSiy",
|
||||||
"database": "wms_node",
|
"database": "wechat_admin",
|
||||||
"host": "192.144.167.231",
|
"host": "101.132.75.138",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ module.exports = {
|
|||||||
// 数据库配置(测试环境)
|
// 数据库配置(测试环境)
|
||||||
// ============================================
|
// ============================================
|
||||||
"db": {
|
"db": {
|
||||||
"username": "wms_node",
|
"username": "wechat_admin",
|
||||||
"password": "SLrhewk5eePr5Z7Y",
|
"password": "YJEjLXnxJ3WraSiy",
|
||||||
"database": "wms_node",
|
"database": "wechat_admin",
|
||||||
"host": "192.144.167.231",
|
"host": "101.132.75.138",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class Schedule {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const bizSubscriptionLogic = require("../api/service/biz_subscription_logic");
|
const bizSubscriptionLogic = require("../api/service/biz_subscription_logic");
|
||||||
|
const usageSvc = require("../api/service/biz_usage_service");
|
||||||
|
const baseModel = require("../middleware/baseModel");
|
||||||
|
|
||||||
node_schedule.scheduleJob("10 0 * * *", async () => {
|
node_schedule.scheduleJob("10 0 * * *", async () => {
|
||||||
await this.execute_with_lock("biz_subscription_expire", async () => {
|
await this.execute_with_lock("biz_subscription_expire", async () => {
|
||||||
@@ -43,6 +45,29 @@ class Schedule {
|
|||||||
logs.log(`[定时任务] 订阅到期扫描完成,更新行数: ${n}`);
|
logs.log(`[定时任务] 订阅到期扫描完成,更新行数: ${n}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
node_schedule.scheduleJob("0 9 * * *", async () => {
|
||||||
|
await this.execute_with_lock("biz_subscription_renew_remind", async () => {
|
||||||
|
const subs = await baseModel.biz_subscription.findAll({ where: { status: "active" } });
|
||||||
|
const now = Date.now();
|
||||||
|
for (const s of subs) {
|
||||||
|
const end = new Date(s.end_time).getTime();
|
||||||
|
const days = Math.ceil((end - now) / 86400000);
|
||||||
|
if (days >= 0 && [7, 3, 1].includes(days)) {
|
||||||
|
logs.log(
|
||||||
|
`[续费提醒] subscription_id=${s.id} user_id=${s.user_id} 约 ${days} 天后到期`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
node_schedule.scheduleJob("30 0 1 * *", async () => {
|
||||||
|
await this.execute_with_lock("biz_usage_monthly_init", async () => {
|
||||||
|
const n = await usageSvc.ensureUsageRowsForCurrentMonth();
|
||||||
|
logs.log(`[定时任务] 月用量行初始化/补齐,处理订阅数: ${n}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user