Compare commits

..

12 Commits

Author SHA1 Message Date
张成
43382668a3 1 2025-12-27 17:40:19 +08:00
张成
6e38ba6b38 1 2025-12-27 17:39:14 +08:00
张成
b17d08ffa8 1 2025-12-26 14:26:04 +08:00
张成
54644dbb72 1 2025-12-26 14:22:33 +08:00
张成
1d8d2ea6e8 1 2025-12-26 14:01:47 +08:00
张成
3f4acc5e1d 1 2025-12-26 13:49:07 +08:00
张成
69f2f87f4b 1 2025-12-26 13:44:57 +08:00
张成
0cfff98edf 1 2025-12-26 13:39:27 +08:00
张成
77789446f3 1 2025-12-26 13:30:20 +08:00
张成
2530f25b86 1 2025-12-26 13:26:11 +08:00
张成
6efd77d2b5 1 2025-12-26 13:12:53 +08:00
张成
6253abc617 1 2025-12-26 11:38:41 +08:00
23 changed files with 1765 additions and 197 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

@@ -48,6 +48,15 @@ class ResumeInfoServer {
analyzeWithAI(resumeId) {
return window.framework.http.post('/resume/analyze-with-ai', { resumeId })
}
/**
* 同步在线简历
* @param {String} resumeId - 简历ID
* @returns {Promise}
*/
syncOnline(resumeId) {
return window.framework.http.post('/resume/sync-online', { resumeId })
}
}
export default new ResumeInfoServer()

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -8,6 +8,7 @@
@back="handleBack"
>
<template #header-right>
<Button type="info" @click="handleSyncOnline" :loading="syncing" style="margin-right: 8px;">同步在线简历</Button>
<Button type="primary" @click="handleAnalyzeAI" :loading="analyzing">AI 分析</Button>
</template>
@@ -272,6 +273,7 @@ export default {
return {
loading: false,
analyzing: false,
syncing: false,
resumeData: null,
skillTags: [],
workExperience: [],
@@ -317,6 +319,32 @@ export default {
}
return field
},
async handleSyncOnline() {
if (!this.resumeData || !this.resumeData.resumeId) {
this.$Message.warning('简历ID不存在')
return
}
if (!this.resumeData.sn_code) {
this.$Message.warning('该简历未绑定设备,无法同步在线简历')
return
}
this.syncing = true
try {
const res = await resumeInfoServer.syncOnline(this.resumeData.resumeId)
this.$Message.success(res.message || '同步在线简历成功')
// 重新加载数据
await this.loadResumeData(this.resumeData.resumeId)
} catch (error) {
console.error('同步在线简历失败:', error)
// 优先从 error.response.data.message 获取,然后是 error.message
const errorMsg = error.response?.data?.message || error.message || '请稍后重试'
this.$Message.error(errorMsg)
} finally {
this.syncing = false
}
},
async handleAnalyzeAI() {
if (!this.resumeData || !this.resumeData.resumeId) {
this.$Message.warning('简历ID不存在')

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: '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>

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

@@ -358,6 +358,100 @@ return ctx.success({ message: '简历删除成功' });
console.error('AI 分析失败:', error);
return ctx.fail('AI 分析失败: ' + error.message);
}
},
/**
* @swagger
* /admin_api/resume/sync-online:
* post:
* summary: 同步在线简历
* description: 通过MQTT指令获取用户在线简历并更新到数据库
* tags: [后台-简历管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - resumeId
* properties:
* resumeId:
* type: string
* description: 简历ID
* responses:
* 200:
* description: 同步成功
*/
'POST /resume/sync-online': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const { resumeId } = ctx.getBody();
if (!resumeId) {
return ctx.fail('简历ID不能为空');
}
const resume = await resume_info.findOne({ where: { resumeId } });
if (!resume) {
return ctx.fail('简历不存在');
}
const { sn_code, platform } = resume;
if (!sn_code) {
return ctx.fail('该简历未绑定设备SN码');
}
try {
const scheduleManager = require('../middleware/schedule');
const resumeManager = require('../middleware/job/resumeManager');
// 检查 MQTT 客户端是否已初始化
if (!scheduleManager.mqttClient) {
return ctx.fail('MQTT客户端未初始化请检查调度系统是否正常启动');
}
// 检查设备是否在线
// const deviceManager = require('../middleware/schedule/deviceManager');
// if (!deviceManager.isDeviceOnline(sn_code)) {
// return ctx.fail('设备离线,无法同步在线简历');
// }
// 调用简历管理器获取并保存简历
const resumeData = await resumeManager.get_online_resume(sn_code, scheduleManager.mqttClient, {
platform: platform || 'boss'
});
// 重新获取更新后的简历数据
const updatedResume = await resume_info.findOne({ where: { resumeId } });
if (!updatedResume) {
return ctx.fail('同步成功但未找到更新后的简历记录');
}
const resumeDetail = updatedResume.toJSON();
// 解析 JSON 字段
const jsonFields = ['skills', 'certifications', 'projectExperience', 'workExperience', 'aiSkillTags'];
jsonFields.forEach(field => {
if (resumeDetail[field]) {
try {
resumeDetail[field] = JSON.parse(resumeDetail[field]);
} catch (e) {
console.error(`解析字段 ${field} 失败:`, e);
}
}
});
return ctx.success({
message: '同步在线简历成功',
data: resumeDetail
});
} catch (error) {
console.error('同步在线简历失败:', error);
return ctx.fail('同步在线简历失败: ' + (error.message || '未知错误'));
}
}
};

View File

@@ -71,6 +71,20 @@ module.exports = {
where.feedbackStatus = seachOption.feedbackStatus;
}
// 时间范围筛选
console.log(seachOption.startTime, seachOption.endTime);
if (seachOption.startTime || seachOption.endTime) {
where.create_time = {};
if (seachOption.startTime) {
where.create_time[op.gte] = new Date(seachOption.startTime);
}
if (seachOption.endTime) {
const endTime = new Date(seachOption.endTime);
endTime.setHours(23, 59, 59, 999); // 设置为当天的最后一刻
where.create_time[op.lte] = endTime;
}
}
// 搜索:岗位名称、公司名称
if (seachOption.key && seachOption.value) {
const key = seachOption.key;
@@ -93,7 +107,7 @@ module.exports = {
where,
limit,
offset,
order: [['applyTime', 'DESC']]
order: [['create_time', 'DESC']]
});
return ctx.success({
@@ -109,7 +123,7 @@ module.exports = {
* /api/apply/statistics:
* get:
* summary: 获取投递统计
* description: 根据设备SN码获取投递统计数据包含今日、本周、本月统计
* description: 根据设备SN码获取投递统计数据包含今日、本周、本月统计,支持时间范围筛选
* tags: [前端-投递管理]
* parameters:
* - in: query
@@ -118,21 +132,50 @@ module.exports = {
* schema:
* type: string
* description: 设备SN码
* - in: query
* name: startTime
* schema:
* type: string
* format: date-time
* description: 开始时间(可选)
* - in: query
* name: endTime
* schema:
* type: string
* format: date-time
* description: 结束时间(可选)
* responses:
* 200:
* description: 获取成功
*/
'GET /apply/statistics': async (ctx) => {
'POST /apply/statistics': async (ctx) => {
const models = Framework.getModels();
const { apply_records, op } = models;
const { sn_code } = ctx.query;
const { apply_records, op, job_postings } = models;
const { sn_code, startTime, endTime } = ctx.getBody();
console.log(startTime, endTime);
const final_sn_code = sn_code;
if (!final_sn_code) {
return ctx.fail('请提供设备SN码');
}
// 计算时间范围
// 构建基础查询条件
const baseWhere = { sn_code: final_sn_code };
// 如果提供了时间范围,则添加到查询条件中
if (startTime || endTime) {
baseWhere.create_time = {};
if (startTime) {
baseWhere.create_time[op.gte] = new Date(startTime);
}
if (endTime) {
const endTimeDate = new Date(endTime);
endTimeDate.setHours(23, 59, 59, 999); // 设置为当天的最后一刻
baseWhere.create_time[op.lte] = endTimeDate;
}
}
// 计算时间范围(如果未提供时间范围,则使用默认的今日、本周、本月)
const now = new Date();
// 今天的开始时间00:00:00
@@ -150,6 +193,8 @@ module.exports = {
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
monthStart.setHours(0, 0, 0, 0);
const [
totalCount,
successCount,
@@ -158,35 +203,44 @@ module.exports = {
interviewCount,
todayCount,
weekCount,
monthCount
monthCount,
totalJobCount
] = await Promise.all([
// 总计
apply_records.count({ where: { sn_code: final_sn_code } }),
apply_records.count({ where: { sn_code: final_sn_code, applyStatus: 'success' } }),
apply_records.count({ where: { sn_code: final_sn_code, applyStatus: 'failed' } }),
apply_records.count({ where: { sn_code: final_sn_code, applyStatus: 'pending' } }),
apply_records.count({ where: { sn_code: final_sn_code, feedbackStatus: 'interview' } }),
// 今日
apply_records.count({
// 总计(如果提供了时间范围,则只统计该范围内的)
apply_records.count({ where: baseWhere }),
apply_records.count({ where: { ...baseWhere, applyStatus: 'success' } }),
apply_records.count({ where: { ...baseWhere, applyStatus: 'failed' } }),
apply_records.count({ where: { ...baseWhere, applyStatus: 'pending' } }),
apply_records.count({ where: { ...baseWhere, feedbackStatus: 'interview' } }),
// 今日如果提供了时间范围则返回0否则统计今日
startTime || endTime ? 0 : apply_records.count({
where: {
sn_code: final_sn_code,
applyTime: { [op.gte]: todayStart }
create_time: { [op.gte]: todayStart }
}
}),
// 本周
apply_records.count({
// 本周如果提供了时间范围则返回0否则统计本周
startTime || endTime ? 0 : apply_records.count({
where: {
sn_code: final_sn_code,
applyTime: { [op.gte]: weekStart }
create_time: { [op.gte]: weekStart }
}
}),
// 本月
apply_records.count({
// 本月如果提供了时间范围则返回0否则统计本月
startTime || endTime ? 0 : apply_records.count({
where: {
sn_code: final_sn_code,
applyTime: { [op.gte]: monthStart }
create_time: { [op.gte]: monthStart }
}
})
}),
// 总职位数
job_postings.count({
where: {
sn_code: final_sn_code,
create_time: { [op.gte]: todayStart }
}
}),
]);
return ctx.success({
@@ -198,6 +252,7 @@ module.exports = {
todayCount,
weekCount,
monthCount,
totalJobCount,
successRate: totalCount > 0 ? ((successCount / totalCount) * 100).toFixed(2) : 0,
interviewRate: totalCount > 0 ? ((interviewCount / totalCount) * 100).toFixed(2) : 0
});
@@ -279,12 +334,12 @@ module.exports = {
const records = await apply_records.findAll({
where: {
sn_code: sn_code,
applyTime: {
create_time: {
[op.gte]: sevenDaysAgo,
[op.lte]: today
}
},
attributes: ['applyTime'],
attributes: ['create_time'],
raw: true
});
@@ -299,7 +354,7 @@ module.exports = {
// 统计当天的投递数量
const count = records.filter(record => {
const recordDate = new Date(record.applyTime);
const recordDate = new Date(record.create_time);
recordDate.setHours(0, 0, 0, 0);
return recordDate.getTime() === date.getTime();
}).length;

View File

@@ -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
},
{
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);
} catch (error) {
console.error('获取价格套餐失败:', error);
return ctx.fail('获取价格套餐失败: ' + error.message);
}
const models = Framework.getModels();
const { pricing_plans } = models;
// 查询所有启用且未删除的套餐,按排序顺序返回
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

@@ -54,8 +54,8 @@ class ScheduleManager {
console.log('[调度管理器] 心跳监听已启动');
// 5. 启动定时任务
// this.scheduledJobs.start();
// console.log('[调度管理器] 定时任务已启动');
this.scheduledJobs.start();
console.log('[调度管理器] 定时任务已启动');
this.isInitialized = true;

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

@@ -1 +1 @@
import{a as t}from"./index-BEa_v6Fs.js";class s{async getConfig(r){try{return await t.post("/user/delivery-config/get",{sn_code:r})}catch(e){throw console.error("获取投递配置失败:",e),e}}async saveConfig(r,e){try{return await t.post("/user/delivery-config/save",{sn_code:r,deliver_config:e})}catch(o){throw console.error("保存投递配置失败:",o),o}}}const i=new s;export{i as default};
import{a as t}from"./index---wtnUW1.js";class s{async getConfig(r){try{return await t.post("/user/delivery-config/get",{sn_code:r})}catch(e){throw console.error("获取投递配置失败:",e),e}}async saveConfig(r,e){try{return await t.post("/user/delivery-config/save",{sn_code:r,deliver_config:e})}catch(o){throw console.error("保存投递配置失败:",o),o}}}const i=new s;export{i as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>boss - 远程监听服务</title>
<script type="module" crossorigin src="/app/assets/index-BEa_v6Fs.js"></script>
<link rel="stylesheet" crossorigin href="/app/assets/index-BHUtbpCz.css">
<script type="module" crossorigin src="/app/assets/index---wtnUW1.js"></script>
<link rel="stylesheet" crossorigin href="/app/assets/index-yg6NAGeT.css">
</head>
<body>

View File

@@ -25,7 +25,7 @@ module.exports = {
acquire: 30000,
idle: 10000
},
logging: false
logging: true
},
// API 路径配置(必需)

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