Merge remote-tracking branch 'origin/main' into dev
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 || '未知错误'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -88,84 +88,47 @@ 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;
|
||||
|
||||
if (planData.features) {
|
||||
planData.features = JSON.parse(planData.features);
|
||||
}
|
||||
|
||||
// 转换 featured 为布尔值
|
||||
planData.featured = planData.featured === 1;
|
||||
|
||||
return planData;
|
||||
});
|
||||
|
||||
|
||||
|
||||
return ctx.success(pricingPlans);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,8 +54,8 @@ class ScheduleManager {
|
||||
console.log('[调度管理器] 心跳监听已启动');
|
||||
|
||||
// 5. 启动定时任务
|
||||
// this.scheduledJobs.start();
|
||||
// console.log('[调度管理器] 定时任务已启动');
|
||||
this.scheduledJobs.start();
|
||||
console.log('[调度管理器] 定时任务已启动');
|
||||
|
||||
this.isInitialized = true;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user