Compare commits
2 Commits
b17d08ffa8
...
43382668a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43382668a3 | ||
|
|
6e38ba6b38 |
36
_sql/add_pricing_plans_menu.sql
Normal file
36
_sql/add_pricing_plans_menu.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- 在用户管理菜单下添加"价格套餐管理"菜单项
|
||||
-- 参考其他菜单项的配置格式
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
name,
|
||||
parent_id,
|
||||
model_id,
|
||||
form_id,
|
||||
icon,
|
||||
path,
|
||||
component,
|
||||
api_path,
|
||||
is_show_menu,
|
||||
is_show,
|
||||
type,
|
||||
sort,
|
||||
create_time,
|
||||
last_modify_time,
|
||||
is_delete
|
||||
) VALUES (
|
||||
'价格套餐管理', -- 菜单名称
|
||||
120, -- parent_id: 用户管理菜单的ID
|
||||
0, -- model_id
|
||||
0, -- form_id
|
||||
'md-pricetags', -- icon: 价格标签图标
|
||||
'pricing_plans', -- path: 路由路径
|
||||
'system/pricing_plans.vue', -- component: 组件路径(已在 component-map.js 中定义)
|
||||
'system/pricing_plans_server.js', -- api_path: API 服务文件路径
|
||||
1, -- is_show_menu: 1=显示在菜单栏
|
||||
1, -- is_show: 1=启用
|
||||
'页面', -- type: 页面类型
|
||||
3, -- sort: 排序(可根据实际情况调整)
|
||||
NOW(), -- create_time: 创建时间
|
||||
NOW(), -- last_modify_time: 最后修改时间
|
||||
0 -- is_delete: 0=未删除
|
||||
);
|
||||
24
_sql/create_pricing_plans_table.sql
Normal file
24
_sql/create_pricing_plans_table.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- 创建价格套餐表
|
||||
-- 用于存储各种价格套餐的配置信息
|
||||
|
||||
CREATE TABLE `pricing_plans` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '价格套餐ID',
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '套餐名称(如:体验套餐、月度套餐等)',
|
||||
`duration` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '时长描述(如:7天、30天、永久)',
|
||||
`days` INT(11) NOT NULL DEFAULT 0 COMMENT '天数(-1表示永久,0表示无限制)',
|
||||
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '售价(元)',
|
||||
`original_price` DECIMAL(10,2) NULL DEFAULT NULL COMMENT '原价(元),可为空表示无原价',
|
||||
`unit` VARCHAR(20) NOT NULL DEFAULT '元' COMMENT '价格单位',
|
||||
`discount` VARCHAR(50) NULL DEFAULT NULL COMMENT '折扣描述(如:8.3折、超值)',
|
||||
`features` TEXT NOT NULL COMMENT '功能特性列表(JSON字符串数组)',
|
||||
`featured` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为推荐套餐(1=推荐,0=普通)',
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(1=启用,0=禁用)',
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT '排序顺序(越小越靠前)',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
|
||||
`is_delete` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除(1=已删除,0=未删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_is_active` (`is_active`),
|
||||
INDEX `idx_is_delete` (`is_delete`),
|
||||
INDEX `idx_sort_order` (`sort_order`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='价格套餐表';
|
||||
89
_sql/insert_pricing_plans_data.sql
Normal file
89
_sql/insert_pricing_plans_data.sql
Normal file
@@ -0,0 +1,89 @@
|
||||
-- 插入初始价格套餐数据
|
||||
-- 基于原有前端接口 /config/pricing-plans 中的硬编码数据
|
||||
|
||||
INSERT INTO `pricing_plans` (
|
||||
`name`,
|
||||
`duration`,
|
||||
`days`,
|
||||
`price`,
|
||||
`original_price`,
|
||||
`unit`,
|
||||
`discount`,
|
||||
`features`,
|
||||
`featured`,
|
||||
`is_active`,
|
||||
`sort_order`,
|
||||
`create_time`,
|
||||
`last_modify_time`,
|
||||
`is_delete`
|
||||
) VALUES
|
||||
(
|
||||
'体验套餐',
|
||||
'7天',
|
||||
7,
|
||||
28.00,
|
||||
28.00,
|
||||
'元',
|
||||
NULL,
|
||||
'["7天使用权限", "全功能体验", "技术支持"]',
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
),
|
||||
(
|
||||
'月度套餐',
|
||||
'30天',
|
||||
30,
|
||||
99.00,
|
||||
120.00,
|
||||
'元',
|
||||
'约8.3折',
|
||||
'["30天使用权限", "全功能使用", "优先技术支持", "性价比最高"]',
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
),
|
||||
(
|
||||
'季度套餐',
|
||||
'90天',
|
||||
90,
|
||||
269.00,
|
||||
360.00,
|
||||
'元',
|
||||
'7.5折',
|
||||
'["90天使用权限", "全功能使用", "优先技术支持", "更优惠价格"]',
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
),
|
||||
(
|
||||
'终生套餐',
|
||||
'永久',
|
||||
-1,
|
||||
888.00,
|
||||
NULL,
|
||||
'元',
|
||||
'超值',
|
||||
'["永久使用权限", "全功能使用", "终身技术支持", "一次购买,终身使用", "最划算选择"]',
|
||||
0,
|
||||
1,
|
||||
4,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
);
|
||||
|
||||
-- 查询验证插入结果
|
||||
SELECT id, name, duration, price, original_price, featured, is_active, sort_order
|
||||
FROM pricing_plans
|
||||
WHERE is_delete = 0
|
||||
ORDER BY sort_order ASC;
|
||||
54
admin/src/api/system/pricing_plans_server.js
Normal file
54
admin/src/api/system/pricing_plans_server.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 价格套餐 API 服务
|
||||
*/
|
||||
|
||||
class PricingPlansServer {
|
||||
/**
|
||||
* 分页查询价格套餐
|
||||
* @param {Object} param - 查询参数
|
||||
* @param {Object} param.seachOption - 搜索条件
|
||||
* @param {Object} param.pageOption - 分页选项
|
||||
* @returns {Promise}
|
||||
*/
|
||||
page(param) {
|
||||
return window.framework.http.post('/pricing_plans/list', param)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取价格套餐详情
|
||||
* @param {Number|String} id - 价格套餐ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getById(id) {
|
||||
return window.framework.http.get('/pricing_plans/detail', { id })
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增价格套餐
|
||||
* @param {Object} row - 价格套餐数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
add(row) {
|
||||
return window.framework.http.post('/pricing_plans/create', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新价格套餐信息
|
||||
* @param {Object} row - 价格套餐数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
update(row) {
|
||||
return window.framework.http.post('/pricing_plans/update', row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除价格套餐
|
||||
* @param {Object} row - 价格套餐数据(包含id)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
del(row) {
|
||||
return window.framework.http.post('/pricing_plans/delete', { id: row.id })
|
||||
}
|
||||
}
|
||||
|
||||
export default new PricingPlansServer()
|
||||
@@ -21,6 +21,7 @@ import TaskStatus from '@/views/task/task_status.vue'
|
||||
import SystemConfig from '@/views/system/system_config.vue'
|
||||
import Version from '@/views/system/version.vue'
|
||||
import JobTypes from '@/views/work/job_types.vue'
|
||||
import PricingPlans from '@/views/system/pricing_plans.vue'
|
||||
|
||||
// 首页模块
|
||||
import HomeIndex from '@/views/home/index.vue'
|
||||
@@ -53,6 +54,8 @@ const componentMap = {
|
||||
'system/system_config': SystemConfig,
|
||||
'system/version': Version,
|
||||
'work/job_types': JobTypes,
|
||||
'system/pricing_plans': PricingPlans,
|
||||
'system/pricing_plans.vue': PricingPlans,
|
||||
'home/index': HomeIndex,
|
||||
|
||||
|
||||
|
||||
385
admin/src/views/system/pricing_plans.vue
Normal file
385
admin/src/views/system/pricing_plans.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div class="content-view">
|
||||
<div class="table-head-tool">
|
||||
<Button type="primary" @click="showAddWarp">新增价格套餐</Button>
|
||||
<Form ref="formInline" :model="gridOption.param.seachOption" inline :label-width="80">
|
||||
<FormItem :label-width="20" class="flex">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 120px"
|
||||
:placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 200px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select v-model="gridOption.param.seachOption.is_active" style="width: 120px" clearable
|
||||
@on-change="query(1)">
|
||||
<Option :value="1">启用</Option>
|
||||
<Option :value="0">禁用</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules" @on-save="handleSaveSuccess">
|
||||
</editModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pricingPlansServer from '@/api/system/pricing_plans_server.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
let rules = {}
|
||||
rules["name"] = [{ required: true, message: '请填写套餐名称', trigger: 'blur' }]
|
||||
rules["duration"] = [{ required: true, message: '请填写时长描述', trigger: 'blur' }]
|
||||
rules["days"] = [{ required: true, message: '请填写天数', trigger: 'blur' }]
|
||||
rules["price"] = [{ required: true, message: '请填写价格', trigger: 'blur' }]
|
||||
|
||||
return {
|
||||
seachTypes: [
|
||||
{ key: 'name', value: '套餐名称' },
|
||||
{ key: 'duration', value: '时长' }
|
||||
],
|
||||
gridOption: {
|
||||
param: {
|
||||
seachOption: {
|
||||
key: 'name',
|
||||
value: '',
|
||||
is_active: null
|
||||
},
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
rules: rules
|
||||
},
|
||||
listColumns: [
|
||||
{ title: 'ID', key: 'id', minWidth: 60 },
|
||||
{ title: '套餐名称', key: 'name', minWidth: 120 },
|
||||
{ title: '时长', key: 'duration', minWidth: 100 },
|
||||
{ title: '天数', key: 'days', minWidth: 80 },
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('span', `¥${params.row.price}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '原价',
|
||||
key: 'original_price',
|
||||
minWidth: 100,
|
||||
render: (h, params) => {
|
||||
return h('span', params.row.original_price ? `¥${params.row.original_price}` : '-')
|
||||
}
|
||||
},
|
||||
{ title: '折扣', key: 'discount', minWidth: 100 },
|
||||
{
|
||||
title: '推荐',
|
||||
key: 'featured',
|
||||
minWidth: 80,
|
||||
render: (h, params) => {
|
||||
return h('Tag', {
|
||||
props: { color: params.row.featured === 1 ? 'warning' : 'default' }
|
||||
}, params.row.featured === 1 ? '推荐' : '普通')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'is_active',
|
||||
minWidth: 80,
|
||||
render: (h, params) => {
|
||||
const status = params.row.is_active === 1
|
||||
return h('Tag', {
|
||||
props: { color: status ? 'success' : 'default' }
|
||||
}, status ? '启用' : '禁用')
|
||||
}
|
||||
},
|
||||
{ title: '排序', key: 'sort_order', minWidth: 80 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
render: (h, params) => {
|
||||
return h('div', [
|
||||
h('Button', {
|
||||
props: {
|
||||
type: 'primary',
|
||||
size: 'small'
|
||||
},
|
||||
style: {
|
||||
marginRight: '5px'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.edit(params.row)
|
||||
}
|
||||
}
|
||||
}, '编辑'),
|
||||
h('Button', {
|
||||
props: {
|
||||
type: 'error',
|
||||
size: 'small'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.del(params.row)
|
||||
}
|
||||
}
|
||||
}, '删除')
|
||||
])
|
||||
}
|
||||
}
|
||||
],
|
||||
editColumns: [
|
||||
{
|
||||
title: '套餐名称',
|
||||
key: 'name',
|
||||
type: 'input',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
title: '时长描述',
|
||||
key: 'duration',
|
||||
type: 'input',
|
||||
required: true,
|
||||
placeholder: '如:7天、30天、永久'
|
||||
},
|
||||
{
|
||||
title: '天数',
|
||||
key: 'days',
|
||||
type: 'number',
|
||||
required: true,
|
||||
tooltip: '-1表示永久,0表示无限制'
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
type: 'number',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
title: '原价',
|
||||
key: 'original_price',
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: '可留空,表示无原价'
|
||||
},
|
||||
{
|
||||
title: '单位',
|
||||
key: 'unit',
|
||||
type: 'input',
|
||||
required: false,
|
||||
defaultValue: '元'
|
||||
},
|
||||
{
|
||||
title: '折扣描述',
|
||||
key: 'discount',
|
||||
type: 'input',
|
||||
required: false,
|
||||
placeholder: '如:8.3折、超值'
|
||||
},
|
||||
{
|
||||
title: '功能特性',
|
||||
key: 'features',
|
||||
com: 'TextArea',
|
||||
required: false,
|
||||
placeholder: '请输入JSON数组格式,例如:["功能1", "功能2", "功能3"]',
|
||||
tooltip: '功能特性列表,JSON数组格式'
|
||||
},
|
||||
{
|
||||
title: '是否推荐',
|
||||
key: 'featured',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 1, label: '推荐' },
|
||||
{ value: 0, label: '普通' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '是否启用',
|
||||
key: 'is_active',
|
||||
type: 'radio',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 1, label: '启用' },
|
||||
{ value: 0, label: '禁用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '排序顺序',
|
||||
key: 'sort_order',
|
||||
type: 'number',
|
||||
required: false,
|
||||
defaultValue: 0,
|
||||
tooltip: '数字越小越靠前'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
seachTypePlaceholder() {
|
||||
const item = this.seachTypes.find(item => item.key === this.gridOption.param.seachOption.key)
|
||||
return item ? `请输入${item.value}` : '请选择'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.query(1)
|
||||
},
|
||||
methods: {
|
||||
query(page) {
|
||||
if (page) {
|
||||
this.gridOption.param.pageOption.page = page
|
||||
}
|
||||
const param = {
|
||||
pageOption: this.gridOption.param.pageOption,
|
||||
seachOption: {}
|
||||
}
|
||||
if (this.gridOption.param.seachOption.key && this.gridOption.param.seachOption.value) {
|
||||
param.seachOption[this.gridOption.param.seachOption.key] = this.gridOption.param.seachOption.value
|
||||
}
|
||||
if (this.gridOption.param.seachOption.is_active !== null) {
|
||||
param.seachOption.is_active = this.gridOption.param.seachOption.is_active
|
||||
}
|
||||
pricingPlansServer.page(param).then(res => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data
|
||||
this.gridOption.data = data.rows
|
||||
this.gridOption.param.pageOption.total = data.count || data.total || 0
|
||||
} else {
|
||||
this.$Message.error(res.message || '查询失败')
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error('查询失败:' + (err.message || '未知错误'))
|
||||
})
|
||||
},
|
||||
resetQuery() {
|
||||
this.gridOption.param.seachOption = {
|
||||
key: 'name',
|
||||
value: '',
|
||||
is_active: null
|
||||
}
|
||||
this.query(1)
|
||||
},
|
||||
showAddWarp() {
|
||||
this.$refs.editModal.show({
|
||||
name: '',
|
||||
duration: '',
|
||||
days: 0,
|
||||
price: 0,
|
||||
original_price: null,
|
||||
unit: '元',
|
||||
discount: '',
|
||||
features: '[]',
|
||||
featured: 0,
|
||||
is_active: 1,
|
||||
sort_order: 0
|
||||
})
|
||||
},
|
||||
edit(row) {
|
||||
// 解析 JSON 字段
|
||||
let features = row.features || '[]'
|
||||
|
||||
// 如果是字符串,尝试解析并格式化
|
||||
if (typeof features === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(features)
|
||||
features = JSON.stringify(parsed, null, 2)
|
||||
} catch (e) {
|
||||
// 保持原样
|
||||
}
|
||||
} else {
|
||||
features = JSON.stringify(features, null, 2)
|
||||
}
|
||||
|
||||
this.$refs.editModal.editShow({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
duration: row.duration || '',
|
||||
days: row.days,
|
||||
price: row.price,
|
||||
original_price: row.original_price,
|
||||
unit: row.unit || '元',
|
||||
discount: row.discount || '',
|
||||
features: features,
|
||||
featured: row.featured,
|
||||
is_active: row.is_active,
|
||||
sort_order: row.sort_order || 0
|
||||
})
|
||||
},
|
||||
del(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除价格套餐"${row.name}"吗?`,
|
||||
onOk: () => {
|
||||
pricingPlansServer.del(row).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$Message.success('删除成功')
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || '删除失败')
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error('删除失败:' + (err.message || '未知错误'))
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleSaveSuccess(data) {
|
||||
// 处理 JSON 字段
|
||||
const formData = { ...data }
|
||||
|
||||
// 处理 features
|
||||
if (formData.features) {
|
||||
try {
|
||||
const parsed = typeof formData.features === 'string'
|
||||
? JSON.parse(formData.features)
|
||||
: formData.features
|
||||
if (!Array.isArray(parsed)) {
|
||||
this.$Message.warning('功能特性必须是数组格式,将使用空数组')
|
||||
formData.features = []
|
||||
} else {
|
||||
formData.features = parsed
|
||||
}
|
||||
} catch (e) {
|
||||
this.$Message.warning('功能特性格式错误,将使用空数组')
|
||||
formData.features = []
|
||||
}
|
||||
}
|
||||
|
||||
const apiMethod = formData.id ? pricingPlansServer.update : pricingPlansServer.add
|
||||
apiMethod(formData).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$Message.success(formData.id ? '更新成功' : '添加成功')
|
||||
this.$refs.editModal.hide()
|
||||
this.query(this.gridOption.param.pageOption.page)
|
||||
} else {
|
||||
this.$Message.error(res.message || (formData.id ? '更新失败' : '添加失败'))
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$Message.error((formData.id ? '更新失败' : '添加失败') + ':' + (err.message || '未知错误'))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-view {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
455
api/controller_admin/pricing_plans.js
Normal file
455
api/controller_admin/pricing_plans.js
Normal file
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* 价格套餐管理API - 后台管理
|
||||
* 提供价格套餐的增删改查功能
|
||||
*/
|
||||
|
||||
const Framework = require("../../framework/node-core-framework.js");
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* @swagger
|
||||
* /admin_api/pricing_plans/list:
|
||||
* post:
|
||||
* summary: 获取价格套餐列表
|
||||
* description: 分页获取所有价格套餐
|
||||
* tags: [后台-价格套餐管理]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* pageOption:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* description: 页码
|
||||
* pageSize:
|
||||
* type: integer
|
||||
* description: 每页数量
|
||||
* seachOption:
|
||||
* type: object
|
||||
* properties:
|
||||
* key:
|
||||
* type: string
|
||||
* description: 搜索字段
|
||||
* value:
|
||||
* type: string
|
||||
* description: 搜索值
|
||||
* is_active:
|
||||
* type: integer
|
||||
* description: 状态筛选(1=启用,0=禁用)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
*/
|
||||
'POST /pricing_plans/list': async (ctx) => {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { pricing_plans, op } = models;
|
||||
const body = ctx.getBody();
|
||||
|
||||
// 获取分页参数
|
||||
const { limit, offset } = ctx.getPageSize();
|
||||
|
||||
// 构建查询条件
|
||||
const where = { is_delete: 0 };
|
||||
|
||||
// 搜索条件
|
||||
if (body.seachOption) {
|
||||
const { key, value, is_active } = body.seachOption;
|
||||
|
||||
if (value && key) {
|
||||
if (key === 'name') {
|
||||
where.name = { [op.like]: `%${value}%` };
|
||||
} else if (key === 'duration') {
|
||||
where.duration = { [op.like]: `%${value}%` };
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (is_active !== undefined && is_active !== null && is_active !== '') {
|
||||
where.is_active = is_active;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await pricing_plans.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['sort_order', 'ASC'], ['id', 'ASC']]
|
||||
});
|
||||
|
||||
return ctx.success(result);
|
||||
} catch (error) {
|
||||
console.error('获取价格套餐列表失败:', error);
|
||||
return ctx.fail('获取价格套餐列表失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin_api/pricing_plans/detail:
|
||||
* get:
|
||||
* summary: 获取价格套餐详情
|
||||
* description: 根据ID获取价格套餐详细信息
|
||||
* tags: [后台-价格套餐管理]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 价格套餐ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
*/
|
||||
'GET /pricing_plans/detail': async (ctx) => {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { pricing_plans } = models;
|
||||
const { id } = ctx.getQuery();
|
||||
|
||||
if (!id) {
|
||||
return ctx.fail('价格套餐ID不能为空');
|
||||
}
|
||||
|
||||
const plan = await pricing_plans.findOne({
|
||||
where: { id, is_delete: 0 }
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return ctx.fail('价格套餐不存在');
|
||||
}
|
||||
|
||||
return ctx.success(plan);
|
||||
} catch (error) {
|
||||
console.error('获取价格套餐详情失败:', error);
|
||||
return ctx.fail('获取价格套餐详情失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin_api/pricing_plans/create:
|
||||
* post:
|
||||
* summary: 创建价格套餐
|
||||
* description: 创建新的价格套餐
|
||||
* tags: [后台-价格套餐管理]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - duration
|
||||
* - days
|
||||
* - price
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: 套餐名称
|
||||
* duration:
|
||||
* type: string
|
||||
* description: 时长描述
|
||||
* days:
|
||||
* type: integer
|
||||
* description: 天数(-1表示永久)
|
||||
* price:
|
||||
* type: number
|
||||
* description: 售价
|
||||
* original_price:
|
||||
* type: number
|
||||
* description: 原价
|
||||
* unit:
|
||||
* type: string
|
||||
* description: 单位
|
||||
* discount:
|
||||
* type: string
|
||||
* description: 折扣描述
|
||||
* features:
|
||||
* type: array
|
||||
* description: 功能特性列表
|
||||
* featured:
|
||||
* type: integer
|
||||
* description: 是否推荐(1=推荐,0=普通)
|
||||
* is_active:
|
||||
* type: integer
|
||||
* description: 是否启用(1=启用,0=禁用)
|
||||
* sort_order:
|
||||
* type: integer
|
||||
* description: 排序顺序
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 创建成功
|
||||
*/
|
||||
'POST /pricing_plans/create': async (ctx) => {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { pricing_plans } = models;
|
||||
const body = ctx.getBody();
|
||||
const { name, duration, days, price, original_price, unit, discount, features, featured, is_active, sort_order } = body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!name) {
|
||||
return ctx.fail('套餐名称不能为空');
|
||||
}
|
||||
if (!duration) {
|
||||
return ctx.fail('时长描述不能为空');
|
||||
}
|
||||
if (days === undefined || days === null) {
|
||||
return ctx.fail('天数不能为空');
|
||||
}
|
||||
if (!price && price !== 0) {
|
||||
return ctx.fail('价格不能为空');
|
||||
}
|
||||
|
||||
// 验证价格
|
||||
if (price < 0) {
|
||||
return ctx.fail('价格不能为负数');
|
||||
}
|
||||
|
||||
// 验证天数
|
||||
if (days < -1) {
|
||||
return ctx.fail('天数不能小于-1(-1表示永久)');
|
||||
}
|
||||
|
||||
// 处理 features 字段:转换为 JSON 字符串
|
||||
let featuresStr = '[]';
|
||||
if (features) {
|
||||
try {
|
||||
if (Array.isArray(features)) {
|
||||
featuresStr = JSON.stringify(features);
|
||||
} else if (typeof features === 'string') {
|
||||
// 验证是否为有效 JSON
|
||||
const parsed = JSON.parse(features);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return ctx.fail('功能特性必须是数组格式');
|
||||
}
|
||||
featuresStr = features;
|
||||
} else {
|
||||
return ctx.fail('功能特性格式错误');
|
||||
}
|
||||
} catch (e) {
|
||||
return ctx.fail('功能特性JSON格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建套餐
|
||||
const plan = await pricing_plans.create({
|
||||
name,
|
||||
duration,
|
||||
days,
|
||||
price,
|
||||
original_price: original_price !== undefined ? original_price : null,
|
||||
unit: unit || '元',
|
||||
discount: discount || null,
|
||||
features: featuresStr,
|
||||
featured: featured !== undefined ? featured : 0,
|
||||
is_active: is_active !== undefined ? is_active : 1,
|
||||
sort_order: sort_order !== undefined ? sort_order : 0,
|
||||
is_delete: 0,
|
||||
create_time: new Date(),
|
||||
last_modify_time: new Date()
|
||||
});
|
||||
|
||||
return ctx.success(plan);
|
||||
} catch (error) {
|
||||
console.error('创建价格套餐失败:', error);
|
||||
return ctx.fail('创建价格套餐失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin_api/pricing_plans/update:
|
||||
* post:
|
||||
* summary: 更新价格套餐
|
||||
* description: 更新价格套餐信息
|
||||
* tags: [后台-价格套餐管理]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - id
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 价格套餐ID
|
||||
* name:
|
||||
* type: string
|
||||
* description: 套餐名称
|
||||
* duration:
|
||||
* type: string
|
||||
* description: 时长描述
|
||||
* days:
|
||||
* type: integer
|
||||
* description: 天数
|
||||
* price:
|
||||
* type: number
|
||||
* description: 售价
|
||||
* original_price:
|
||||
* type: number
|
||||
* description: 原价
|
||||
* unit:
|
||||
* type: string
|
||||
* description: 单位
|
||||
* discount:
|
||||
* type: string
|
||||
* description: 折扣描述
|
||||
* features:
|
||||
* type: array
|
||||
* description: 功能特性列表
|
||||
* featured:
|
||||
* type: integer
|
||||
* description: 是否推荐
|
||||
* is_active:
|
||||
* type: integer
|
||||
* description: 是否启用
|
||||
* sort_order:
|
||||
* type: integer
|
||||
* description: 排序顺序
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
*/
|
||||
'POST /pricing_plans/update': async (ctx) => {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { pricing_plans } = models;
|
||||
const body = ctx.getBody();
|
||||
const { id, name, duration, days, price, original_price, unit, discount, features, featured, is_active, sort_order } = body;
|
||||
|
||||
if (!id) {
|
||||
return ctx.fail('价格套餐ID不能为空');
|
||||
}
|
||||
|
||||
const plan = await pricing_plans.findOne({
|
||||
where: { id, is_delete: 0 }
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return ctx.fail('价格套餐不存在');
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
const updateData = {
|
||||
last_modify_time: new Date()
|
||||
};
|
||||
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (duration !== undefined) updateData.duration = duration;
|
||||
if (days !== undefined) {
|
||||
if (days < -1) {
|
||||
return ctx.fail('天数不能小于-1(-1表示永久)');
|
||||
}
|
||||
updateData.days = days;
|
||||
}
|
||||
if (price !== undefined) {
|
||||
if (price < 0) {
|
||||
return ctx.fail('价格不能为负数');
|
||||
}
|
||||
updateData.price = price;
|
||||
}
|
||||
if (original_price !== undefined) updateData.original_price = original_price;
|
||||
if (unit !== undefined) updateData.unit = unit;
|
||||
if (discount !== undefined) updateData.discount = discount;
|
||||
if (featured !== undefined) updateData.featured = featured;
|
||||
if (is_active !== undefined) updateData.is_active = is_active;
|
||||
if (sort_order !== undefined) updateData.sort_order = sort_order;
|
||||
|
||||
// 处理 features 字段
|
||||
if (features !== undefined) {
|
||||
try {
|
||||
if (Array.isArray(features)) {
|
||||
updateData.features = JSON.stringify(features);
|
||||
} else if (typeof features === 'string') {
|
||||
const parsed = JSON.parse(features);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return ctx.fail('功能特性必须是数组格式');
|
||||
}
|
||||
updateData.features = features;
|
||||
} else {
|
||||
return ctx.fail('功能特性格式错误');
|
||||
}
|
||||
} catch (e) {
|
||||
return ctx.fail('功能特性JSON格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
await pricing_plans.update(updateData, {
|
||||
where: { id, is_delete: 0 }
|
||||
});
|
||||
|
||||
return ctx.success({ message: '价格套餐更新成功' });
|
||||
} catch (error) {
|
||||
console.error('更新价格套餐失败:', error);
|
||||
return ctx.fail('更新价格套餐失败: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin_api/pricing_plans/delete:
|
||||
* post:
|
||||
* summary: 删除价格套餐
|
||||
* description: 软删除指定的价格套餐
|
||||
* tags: [后台-价格套餐管理]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - id
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 价格套餐ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 删除成功
|
||||
*/
|
||||
'POST /pricing_plans/delete': async (ctx) => {
|
||||
try {
|
||||
const models = Framework.getModels();
|
||||
const { pricing_plans } = models;
|
||||
const { id } = ctx.getBody();
|
||||
|
||||
if (!id) {
|
||||
return ctx.fail('价格套餐ID不能为空');
|
||||
}
|
||||
|
||||
const plan = await pricing_plans.findOne({
|
||||
where: { id, is_delete: 0 }
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return ctx.fail('价格套餐不存在');
|
||||
}
|
||||
|
||||
// 软删除
|
||||
await pricing_plans.update(
|
||||
{
|
||||
is_delete: 1,
|
||||
last_modify_time: new Date()
|
||||
},
|
||||
{ where: { id } }
|
||||
);
|
||||
|
||||
return ctx.success({ message: '价格套餐删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除价格套餐失败:', error);
|
||||
return ctx.fail('删除价格套餐失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -88,84 +88,41 @@ module.exports = {
|
||||
* description: 获取成功
|
||||
*/
|
||||
'GET /config/pricing-plans': async (ctx) => {
|
||||
try {
|
||||
// 写死4条价格套餐数据
|
||||
// 价格计算规则:2小时 = 1天
|
||||
const pricingPlans = [
|
||||
{
|
||||
id: 1,
|
||||
name: '体验套餐',
|
||||
duration: '7天',
|
||||
days: 7,
|
||||
price: 28,
|
||||
originalPrice: 28,
|
||||
unit: '元',
|
||||
features: [
|
||||
'7天使用权限',
|
||||
'全功能体验',
|
||||
'技术支持'
|
||||
],
|
||||
featured: false
|
||||
|
||||
const models = Framework.getModels();
|
||||
const { pricing_plans } = models;
|
||||
|
||||
// 查询所有启用且未删除的套餐,按排序顺序返回
|
||||
const plans = await pricing_plans.findAll({
|
||||
where: {
|
||||
is_active: 1,
|
||||
is_delete: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '月度套餐',
|
||||
duration: '30天',
|
||||
days: 30,
|
||||
price: 99,
|
||||
originalPrice: 120,
|
||||
unit: '元',
|
||||
discount: '约8.3折',
|
||||
features: [
|
||||
'30天使用权限',
|
||||
'全功能使用',
|
||||
'优先技术支持',
|
||||
'性价比最高'
|
||||
],
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '季度套餐',
|
||||
duration: '90天',
|
||||
days: 90,
|
||||
price: 269,
|
||||
originalPrice: 360,
|
||||
unit: '元',
|
||||
discount: '7.5折',
|
||||
features: [
|
||||
'90天使用权限',
|
||||
'全功能使用',
|
||||
'优先技术支持',
|
||||
'更优惠价格'
|
||||
],
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '终生套餐',
|
||||
duration: '永久',
|
||||
days: -1,
|
||||
price: 888,
|
||||
originalPrice: null,
|
||||
unit: '元',
|
||||
discount: '超值',
|
||||
features: [
|
||||
'永久使用权限',
|
||||
'全功能使用',
|
||||
'终身技术支持',
|
||||
'一次购买,终身使用',
|
||||
'最划算选择'
|
||||
],
|
||||
featured: false
|
||||
order: [['sort_order', 'ASC'], ['id', 'ASC']],
|
||||
attributes: [
|
||||
'id', 'name', 'duration', 'days', 'price',
|
||||
'original_price', 'unit', 'discount', 'features', 'featured'
|
||||
]
|
||||
});
|
||||
|
||||
// 转换数据格式以匹配前端期望
|
||||
const pricingPlans = plans.map(plan => {
|
||||
const planData = plan.toJSON();
|
||||
|
||||
// 重命名字段以匹配前端期望(camelCase)
|
||||
if (planData.original_price !== null) {
|
||||
planData.originalPrice = planData.original_price;
|
||||
}
|
||||
];
|
||||
delete planData.original_price;
|
||||
|
||||
// 转换 featured 为布尔值
|
||||
planData.featured = planData.featured === 1;
|
||||
|
||||
return planData;
|
||||
});
|
||||
|
||||
return ctx.success(pricingPlans);
|
||||
} catch (error) {
|
||||
console.error('获取价格套餐失败:', error);
|
||||
return ctx.fail('获取价格套餐失败: ' + error.message);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
97
api/model/pricing_plans.js
Normal file
97
api/model/pricing_plans.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
/**
|
||||
* 价格套餐表模型
|
||||
* 存储各种价格套餐的配置信息,支持管理员在后台配置和管理
|
||||
*/
|
||||
module.exports = (db) => {
|
||||
const pricing_plans = db.define("pricing_plans", {
|
||||
name: {
|
||||
comment: '套餐名称(如:体验套餐、月度套餐等)',
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
},
|
||||
duration: {
|
||||
comment: '时长描述(如:7天、30天、永久)',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
},
|
||||
days: {
|
||||
comment: '天数(-1表示永久,0表示无限制)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
price: {
|
||||
comment: '售价(元)',
|
||||
type: Sequelize.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 0.00
|
||||
},
|
||||
original_price: {
|
||||
comment: '原价(元),可为空表示无原价',
|
||||
type: Sequelize.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
unit: {
|
||||
comment: '价格单位',
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: false,
|
||||
defaultValue: '元'
|
||||
},
|
||||
discount: {
|
||||
comment: '折扣描述(如:8.3折、超值)',
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
features: {
|
||||
comment: '功能特性列表(JSON字符串数组)',
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: '[]'
|
||||
},
|
||||
featured: {
|
||||
comment: '是否为推荐套餐(1=推荐,0=普通)',
|
||||
type: Sequelize.TINYINT(1),
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
is_active: {
|
||||
comment: '是否启用(1=启用,0=禁用)',
|
||||
type: Sequelize.TINYINT(1),
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
},
|
||||
sort_order: {
|
||||
comment: '排序顺序(越小越靠前)',
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
|
||||
}, {
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
unique: false,
|
||||
fields: ['is_active']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['is_delete']
|
||||
},
|
||||
{
|
||||
unique: false,
|
||||
fields: ['sort_order']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// pricing_plans.sync({ force: true });
|
||||
|
||||
return pricing_plans;
|
||||
}
|
||||
138
scripts/add_pricing_plans_menu.js
Normal file
138
scripts/add_pricing_plans_menu.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 添加"价格套餐管理"菜单项到用户管理菜单下
|
||||
* 执行 SQL 插入操作
|
||||
*/
|
||||
|
||||
const Framework = require('../framework/node-core-framework.js');
|
||||
const frameworkConfig = require('../config/framework.config.js');
|
||||
|
||||
async function addPricingPlansMenu() {
|
||||
console.log('🔄 开始添加"价格套餐管理"菜单项...\n');
|
||||
|
||||
try {
|
||||
// 初始化框架
|
||||
console.log('正在初始化框架...');
|
||||
const framework = await Framework.init(frameworkConfig);
|
||||
const models = Framework.getModels();
|
||||
|
||||
if (!models) {
|
||||
throw new Error('无法获取模型列表');
|
||||
}
|
||||
|
||||
// 从任意模型获取 sequelize 实例
|
||||
const Sequelize = require('sequelize');
|
||||
const firstModel = Object.values(models)[0];
|
||||
if (!firstModel || !firstModel.sequelize) {
|
||||
throw new Error('无法获取数据库连接');
|
||||
}
|
||||
const sequelize = firstModel.sequelize;
|
||||
|
||||
// 查找用户管理菜单的ID
|
||||
const [userMenu] = await sequelize.query(
|
||||
`SELECT id FROM sys_menu WHERE name = '用户管理' AND parent_id = 0 AND is_delete = 0 LIMIT 1`,
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
let parentId = 120; // 默认 fallback 值
|
||||
if (userMenu && userMenu.id) {
|
||||
parentId = userMenu.id;
|
||||
console.log(`找到用户管理菜单,ID: ${parentId}`);
|
||||
} else {
|
||||
console.log(`未找到用户管理菜单,使用默认 parent_id: ${parentId}`);
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
const [existing] = await sequelize.query(
|
||||
`SELECT id, name FROM sys_menu WHERE path = 'pricing_plans' AND is_delete = 0`,
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
console.log(`⚠️ 菜单项已存在 (ID: ${existing.id}, 名称: ${existing.name})`);
|
||||
console.log('✅ 无需重复添加\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最大排序值
|
||||
const [maxSort] = await sequelize.query(
|
||||
`SELECT MAX(sort) as maxSort FROM sys_menu WHERE parent_id = ${parentId} AND is_delete = 0`,
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
const nextSort = (maxSort && maxSort.maxSort ? maxSort.maxSort : 0) + 1;
|
||||
|
||||
// 执行插入
|
||||
await sequelize.query(
|
||||
`INSERT INTO sys_menu (
|
||||
name,
|
||||
parent_id,
|
||||
model_id,
|
||||
form_id,
|
||||
icon,
|
||||
path,
|
||||
component,
|
||||
api_path,
|
||||
is_show_menu,
|
||||
is_show,
|
||||
type,
|
||||
sort,
|
||||
create_time,
|
||||
last_modify_time,
|
||||
is_delete
|
||||
) VALUES (
|
||||
'价格套餐管理',
|
||||
${parentId},
|
||||
0,
|
||||
0,
|
||||
'md-pricetags',
|
||||
'pricing_plans',
|
||||
'system/pricing_plans.vue',
|
||||
'system/pricing_plans_server.js',
|
||||
1,
|
||||
1,
|
||||
'页面',
|
||||
${nextSort},
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
)`,
|
||||
{ type: Sequelize.QueryTypes.INSERT }
|
||||
);
|
||||
|
||||
console.log('✅ "价格套餐管理"菜单项添加成功!\n');
|
||||
|
||||
// 验证插入结果
|
||||
const [menu] = await sequelize.query(
|
||||
`SELECT id, name, parent_id, path, component, api_path, sort
|
||||
FROM sys_menu
|
||||
WHERE path = 'pricing_plans' AND is_delete = 0`,
|
||||
{ type: Sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (menu) {
|
||||
console.log('📋 菜单项详情:');
|
||||
console.log(` ID: ${menu.id}`);
|
||||
console.log(` 名称: ${menu.name}`);
|
||||
console.log(` 父菜单ID: ${menu.parent_id}`);
|
||||
console.log(` 路由路径: ${menu.path}`);
|
||||
console.log(` 组件路径: ${menu.component}`);
|
||||
console.log(` API路径: ${menu.api_path}`);
|
||||
console.log(` 排序: ${menu.sort}\n`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 添加失败:', error.message);
|
||||
console.error('\n详细错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行添加
|
||||
addPricingPlansMenu()
|
||||
.then(() => {
|
||||
console.log('✨ 操作完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n💥 执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
135
scripts/migrate_pricing_plans_data.js
Normal file
135
scripts/migrate_pricing_plans_data.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 迁移现有价格套餐数据到数据库
|
||||
* 将 config.js 中硬编码的 4 个套餐数据导入到 pricing_plans 表
|
||||
*/
|
||||
|
||||
const Framework = require('../framework/node-core-framework.js');
|
||||
const frameworkConfig = require('../config/framework.config.js');
|
||||
|
||||
async function migratePricingPlans() {
|
||||
console.log('🔄 开始迁移价格套餐数据...\n');
|
||||
|
||||
try {
|
||||
// 初始化框架
|
||||
console.log('正在初始化框架...');
|
||||
const framework = await Framework.init(frameworkConfig);
|
||||
const models = Framework.getModels();
|
||||
|
||||
if (!models || !models.pricing_plans) {
|
||||
throw new Error('无法获取 pricing_plans 模型');
|
||||
}
|
||||
|
||||
const { pricing_plans } = models;
|
||||
|
||||
// 检查是否已有数据
|
||||
const existingCount = await pricing_plans.count({ where: { is_delete: 0 } });
|
||||
if (existingCount > 0) {
|
||||
console.log(`⚠️ 已存在 ${existingCount} 条套餐数据,跳过迁移\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 现有的4个套餐数据(来自 api/controller_front/config.js)
|
||||
const plans = [
|
||||
{
|
||||
name: '体验套餐',
|
||||
duration: '7天',
|
||||
days: 7,
|
||||
price: 28.00,
|
||||
original_price: 28.00,
|
||||
unit: '元',
|
||||
discount: null,
|
||||
features: JSON.stringify(['7天使用权限', '全功能体验', '技术支持']),
|
||||
featured: 0,
|
||||
is_active: 1,
|
||||
sort_order: 1,
|
||||
is_delete: 0,
|
||||
create_time: new Date(),
|
||||
last_modify_time: new Date()
|
||||
},
|
||||
{
|
||||
name: '月度套餐',
|
||||
duration: '30天',
|
||||
days: 30,
|
||||
price: 99.00,
|
||||
original_price: 120.00,
|
||||
unit: '元',
|
||||
discount: '约8.3折',
|
||||
features: JSON.stringify(['30天使用权限', '全功能使用', '优先技术支持', '性价比最高']),
|
||||
featured: 1,
|
||||
is_active: 1,
|
||||
sort_order: 2,
|
||||
is_delete: 0,
|
||||
create_time: new Date(),
|
||||
last_modify_time: new Date()
|
||||
},
|
||||
{
|
||||
name: '季度套餐',
|
||||
duration: '90天',
|
||||
days: 90,
|
||||
price: 269.00,
|
||||
original_price: 360.00,
|
||||
unit: '元',
|
||||
discount: '7.5折',
|
||||
features: JSON.stringify(['90天使用权限', '全功能使用', '优先技术支持', '更优惠价格']),
|
||||
featured: 0,
|
||||
is_active: 1,
|
||||
sort_order: 3,
|
||||
is_delete: 0,
|
||||
create_time: new Date(),
|
||||
last_modify_time: new Date()
|
||||
},
|
||||
{
|
||||
name: '终生套餐',
|
||||
duration: '永久',
|
||||
days: -1,
|
||||
price: 888.00,
|
||||
original_price: null,
|
||||
unit: '元',
|
||||
discount: '超值',
|
||||
features: JSON.stringify(['永久使用权限', '全功能使用', '终身技术支持', '一次购买,终身使用', '最划算选择']),
|
||||
featured: 0,
|
||||
is_active: 1,
|
||||
sort_order: 4,
|
||||
is_delete: 0,
|
||||
create_time: new Date(),
|
||||
last_modify_time: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
// 批量插入
|
||||
await pricing_plans.bulkCreate(plans);
|
||||
console.log('✅ 成功迁移 4 条价格套餐数据!\n');
|
||||
|
||||
// 验证插入结果
|
||||
const result = await pricing_plans.findAll({
|
||||
where: { is_delete: 0 },
|
||||
order: [['sort_order', 'ASC']]
|
||||
});
|
||||
|
||||
console.log('📋 已迁移的套餐:');
|
||||
result.forEach(plan => {
|
||||
const planData = plan.toJSON();
|
||||
console.log(` ${planData.id}. ${planData.name} - ${planData.duration} - ¥${planData.price}`);
|
||||
if (planData.featured === 1) {
|
||||
console.log(` [推荐套餐]`);
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 迁移失败:', error.message);
|
||||
console.error('\n详细错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
migratePricingPlans()
|
||||
.then(() => {
|
||||
console.log('✨ 迁移完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n💥 执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user