This commit is contained in:
张成
2025-12-27 17:39:14 +08:00
parent b17d08ffa8
commit 6e38ba6b38
11 changed files with 1450 additions and 77 deletions

View 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=未删除
);

View 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='价格套餐表';

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

View 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()

View File

@@ -21,6 +21,7 @@ import TaskStatus from '@/views/task/task_status.vue'
import SystemConfig from '@/views/system/system_config.vue' import SystemConfig from '@/views/system/system_config.vue'
import Version from '@/views/system/version.vue' import Version from '@/views/system/version.vue'
import JobTypes from '@/views/work/job_types.vue' import JobTypes from '@/views/work/job_types.vue'
import PricingPlans from '@/views/system/pricing_plans.vue'
// 首页模块 // 首页模块
import HomeIndex from '@/views/home/index.vue' import HomeIndex from '@/views/home/index.vue'
@@ -53,6 +54,8 @@ const componentMap = {
'system/system_config': SystemConfig, 'system/system_config': SystemConfig,
'system/version': Version, 'system/version': Version,
'work/job_types': JobTypes, 'work/job_types': JobTypes,
'system/pricing_plans': PricingPlans,
'system/pricing_plans.vue': PricingPlans,
'home/index': HomeIndex, 'home/index': HomeIndex,

View 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: 'select',
required: true,
options: [
{ value: 1, label: '推荐' },
{ value: 0, label: '普通' }
]
},
{
title: '是否启用',
key: 'is_active',
type: 'select',
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>

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

View File

@@ -88,84 +88,41 @@ module.exports = {
* description: 获取成功 * description: 获取成功
*/ */
'GET /config/pricing-plans': async (ctx) => { '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
},
{
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
}
];
return ctx.success(pricingPlans); const models = Framework.getModels();
} catch (error) { const { pricing_plans } = models;
console.error('获取价格套餐失败:', error);
return ctx.fail('获取价格套餐失败: ' + error.message); // 查询所有启用且未删除的套餐,按排序顺序返回
} const plans = await pricing_plans.findAll({
where: {
is_active: 1,
is_delete: 0
},
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);
}, },
/** /**

View 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;
}

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

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