This commit is contained in:
张成
2026-02-01 22:00:19 +08:00
parent 933f1618ca
commit 68b4db0aee
8 changed files with 1762 additions and 708 deletions

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 AiCallRecords from '@/views/system/ai_call_records.vue'
// 首页模块 // 首页模块
import HomeIndex from '@/views/home/index.vue' import HomeIndex from '@/views/home/index.vue'
@@ -53,6 +54,7 @@ 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/ai_call_records': AiCallRecords,
'home/index': HomeIndex, 'home/index': HomeIndex,

View File

@@ -1,307 +0,0 @@
const axios = require('axios');
const config = require('../../../config/config');
const logs = require('../logProxy');
/**
* Qwen 2.5 大模型服务
* 集成阿里云 DashScope API提供智能化的岗位筛选、聊天生成、简历分析等功能
*/
class aiService {
constructor() {
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
// 使用 DashScope 兼容 OpenAI 格式的接口
this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
// Qwen 2.5 模型qwen-turbo快速、qwen-plus增强、qwen-max最强
this.model = config.ai?.model || 'qwen-turbo';
this.maxRetries = 3;
}
/**
* 调用 Qwen 2.5 API
* @param {string} prompt - 提示词
* @param {object} options - 配置选项
* @returns {Promise<object>} API响应结果
*/
async callAPI(prompt, options = {}) {
const requestData = {
model: this.model,
messages: [
{
role: 'system',
content: options.systemPrompt || '你是一个专业的招聘顾问,擅长分析岗位信息、生成聊天内容和分析简历匹配度。'
},
{
role: 'user',
content: prompt
}
],
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2000,
top_p: options.topP || 0.9
};
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const response = await axios.post(this.apiUrl, requestData, {
headers: {
'Authorization': `Bearer ${this.apiKey}`, // DashScope 使用 Bearer token 格式
'Content-Type': 'application/json'
},
timeout: 30000
});
return {
data: response.data,
content: response.data.choices?.[0]?.message?.content || ''
};
} catch (error) {
console.log(`Qwen 2.5 API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`);
if (attempt === this.maxRetries) {
throw new Error(error.message);
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
/**
* 岗位智能筛选
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @returns {Promise<object>} 筛选结果
*/
async analyzeJob(jobInfo, resumeInfo) {
const prompt = `
请分析以下岗位信息,并给出详细的评估结果:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 薪资范围:${jobInfo.salary || '未知'}
- 工作地点:${jobInfo.location || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 教育背景:${resumeInfo.education || '未知'}
- 期望薪资:${resumeInfo.expectedSalary || '未知'}
请从以下维度进行分析:
1. 技能匹配度0-100分
2. 经验匹配度0-100分
3. 薪资合理性0-100分
4. 公司质量评估0-100分
5. 是否为外包岗位(是/否)
6. 综合推荐指数0-100分
7. 详细分析说明
8. 投递建议
请以JSON格式返回结果。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。',
temperature: 0.3
});
try {
// 尝试解析JSON响应
const analysis = JSON.parse(result.content);
return {
analysis: analysis
};
} catch (parseError) {
// 如果解析失败,返回原始内容
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成个性化聊天内容
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型 (greeting/interview/followup)
* @returns {Promise<object>} 聊天内容
*/
async generateChatContent(jobInfo, resumeInfo, chatType = 'greeting') {
const chatTypeMap = {
'greeting': '初次打招呼',
'interview': '面试邀约',
'followup': '跟进沟通'
};
const prompt = `
请为以下场景生成个性化的聊天内容:
聊天类型:${chatTypeMap[chatType] || chatType}
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 项目经验:${resumeInfo.projects || '未知'}
要求:
1. 内容要自然、专业、个性化
2. 突出简历与岗位的匹配点
3. 避免过于机械化的表达
4. 长度控制在100-200字
5. 体现求职者的诚意和热情
请直接返回聊天内容,不需要其他格式。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘沟通专家,擅长生成自然、专业的求职聊天内容。',
temperature: 0.8
});
return result;
}
/**
* 分析简历要素
* @param {string} resumeText - 简历文本内容
* @returns {Promise<object>} 简历分析结果
*/
async analyzeResume(resumeText) {
const prompt = `
请分析以下简历内容,并返回 JSON 格式的分析结果:
简历内容:
${resumeText}
请按以下格式返回 JSON 结果:
{
"skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
"strengths": "核心优势描述", // 简历的优势和亮点
"weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
"careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
"competitiveness": 75 // 竞争力评分0-100的整数综合考虑工作年限、技能、经验等因素
}
要求:
1. skillTags 必须是字符串数组
2. strengths、weaknesses、careerSuggestion 是字符串描述
3. competitiveness 必须是 0-100 之间的整数
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
temperature: 0.3,
maxTokens: 1500
});
try {
// 尝试从返回内容中提取 JSON
let content = result.content.trim();
// 如果返回内容被代码块包裹,提取其中的 JSON
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || content.match(/(\{[\s\S]*\})/);
if (jsonMatch) {
content = jsonMatch[1];
}
const analysis = JSON.parse(content);
return {
analysis: analysis
};
} catch (parseError) {
console.error(`[AI服务] 简历分析结果解析失败:`, parseError);
console.error(`[AI服务] 原始内容:`, result.content);
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成面试邀约内容
* @param {object} jobInfo - 岗位信息
* @param {object} chatHistory - 聊天历史
* @returns {Promise<object>} 面试邀约内容
*/
async generateInterviewInvitation(jobInfo, chatHistory) {
const prompt = `
请基于以下信息生成面试邀约内容:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 工作地点:${jobInfo.location || '未知'}
聊天历史:
${chatHistory || '无'}
要求:
1. 表达面试邀约的诚意
2. 提供灵活的时间选择
3. 说明面试形式和地点
4. 体现对候选人的重视
5. 语言自然、专业
请直接返回面试邀约内容。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的HR擅长生成面试邀约内容。',
temperature: 0.6
});
return result;
}
/**
* 识别外包岗位
* @param {object} jobInfo - 岗位信息
* @returns {Promise<object>} 外包识别结果
*/
async identifyOutsourcingJob(jobInfo) {
const prompt = `
请分析以下岗位信息,判断是否为外包岗位:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
- 工作地点:${jobInfo.location || '未知'}
外包岗位特征:
1. 公司名称包含"外包"、"派遣"、"人力"等关键词
2. 岗位描述提到"项目外包"、"驻场开发"等
3. 技能要求过于宽泛或具体
4. 工作地点频繁变动
5. 薪资结构不明确
请判断是否为外包岗位,并给出详细分析。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的岗位分析师,擅长识别外包岗位的特征。',
temperature: 0.3
});
return result;
}
}
module.exports = new aiService();

View File

@@ -0,0 +1,144 @@
const Sequelize = require('sequelize');
const { DataTypes } = Sequelize;
module.exports = (sequelize) => {
const AiCallRecords = sequelize.define('ai_call_records', {
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '主键ID'
},
user_id: {
type: DataTypes.INTEGER(11),
allowNull: true,
comment: '用户ID'
},
sn_code: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '设备SN码'
},
service_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '服务类型chat/completion/embedding'
},
model_name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '模型名称'
},
prompt_tokens: {
type: DataTypes.INTEGER(11),
allowNull: false,
defaultValue: 0,
comment: '输入Token数'
},
completion_tokens: {
type: DataTypes.INTEGER(11),
allowNull: false,
defaultValue: 0,
comment: '输出Token数'
},
total_tokens: {
type: DataTypes.INTEGER(11),
allowNull: false,
defaultValue: 0,
comment: '总Token数'
},
request_content: {
type: DataTypes.TEXT,
allowNull: true,
comment: '请求内容'
},
response_content: {
type: DataTypes.TEXT,
allowNull: true,
comment: '响应内容'
},
cost_amount: {
type: DataTypes.DECIMAL(10, 4),
allowNull: true,
comment: '费用(元)'
},
status: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'success',
comment: '状态success/failed'
},
error_message: {
type: DataTypes.TEXT,
allowNull: true,
comment: '错误信息'
},
response_time: {
type: DataTypes.INTEGER(11),
allowNull: true,
comment: '响应时间(毫秒)'
},
api_provider: {
type: DataTypes.STRING(50),
allowNull: true,
defaultValue: 'qwen',
comment: 'API提供商'
},
business_type: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '业务类型'
},
reference_id: {
type: DataTypes.STRING(100),
allowNull: true,
comment: '关联业务ID'
},
create_time: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
comment: '创建时间'
},
last_modify_time: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
comment: '最后修改时间'
},
is_delete: {
type: DataTypes.TINYINT(1),
allowNull: false,
defaultValue: 0,
comment: '是否删除0-否1-是'
}
}, {
tableName: 'ai_call_records',
timestamps: false,
indexes: [
{
name: 'idx_user_id',
fields: ['user_id']
},
{
name: 'idx_sn_code',
fields: ['sn_code']
},
{
name: 'idx_create_time',
fields: ['create_time']
},
{
name: 'idx_is_delete',
fields: ['is_delete']
},
{
name: 'idx_business_type',
fields: ['business_type']
}
]
});
return AiCallRecords;
};

View File

@@ -0,0 +1,168 @@
const Framework = require('node-core-framework');
/**
* AI调用记录服务
* 负责记录所有AI API调用的详细信息
*/
class AiCallRecorder {
/**
* 记录AI调用
* @param {Object} params - 调用参数
* @param {Number} params.user_id - 用户ID
* @param {String} params.sn_code - 设备SN码
* @param {String} params.service_type - 服务类型chat/completion/embedding
* @param {String} params.model_name - 模型名称
* @param {Number} params.prompt_tokens - 输入Token数
* @param {Number} params.completion_tokens - 输出Token数
* @param {Number} params.total_tokens - 总Token数
* @param {String} params.request_content - 请求内容
* @param {String} params.response_content - 响应内容
* @param {Number} params.cost_amount - 费用(元)
* @param {String} params.status - 状态success/failed
* @param {String} params.error_message - 错误信息
* @param {Number} params.response_time - 响应时间(毫秒)
* @param {String} params.api_provider - API提供商
* @param {String} params.business_type - 业务类型
* @param {String} params.reference_id - 关联业务ID
* @returns {Promise<Object>} 记录结果
*/
static async record(params) {
try {
const models = Framework.getModels();
const { ai_call_records } = models;
if (!ai_call_records) {
console.warn('[AI记录] ai_call_records 模型未加载');
return null;
}
const now = new Date();
const record = await ai_call_records.create({
user_id: params.user_id || null,
sn_code: params.sn_code || null,
service_type: params.service_type,
model_name: params.model_name,
prompt_tokens: params.prompt_tokens || 0,
completion_tokens: params.completion_tokens || 0,
total_tokens: params.total_tokens || 0,
request_content: params.request_content || null,
response_content: params.response_content || null,
cost_amount: params.cost_amount || null,
status: params.status || 'success',
error_message: params.error_message || null,
response_time: params.response_time || null,
api_provider: params.api_provider || 'qwen',
business_type: params.business_type || null,
reference_id: params.reference_id || null,
create_time: now,
last_modify_time: now,
is_delete: 0
});
return record;
} catch (error) {
console.error('[AI记录] 记录失败:', error.message);
throw error;
}
}
/**
* 获取用户Token使用统计
* @param {Number} userId - 用户ID
* @param {Object} options - 查询选项
* @param {Date} options.startDate - 开始日期
* @param {Date} options.endDate - 结束日期
* @returns {Promise<Object>} 统计数据
*/
static async getUserTokenStats(userId, options = {}) {
try {
const models = Framework.getModels();
const { ai_call_records } = models;
const { Op } = Framework.getSequelize();
if (!ai_call_records) {
return null;
}
const where = {
user_id: userId,
is_delete: 0
};
if (options.startDate && options.endDate) {
where.create_time = {
[Op.between]: [options.startDate, options.endDate]
};
}
const stats = await ai_call_records.findOne({
where,
attributes: [
[Framework.getSequelize().fn('COUNT', Framework.getSequelize().col('id')), 'total_calls'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('prompt_tokens')), 'total_prompt_tokens'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('completion_tokens')), 'total_completion_tokens'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('total_tokens')), 'total_tokens'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('cost_amount')), 'total_cost'],
[Framework.getSequelize().fn('AVG', Framework.getSequelize().col('response_time')), 'avg_response_time']
],
raw: true
});
return stats;
} catch (error) {
console.error('[AI记录] 获取用户统计失败:', error.message);
throw error;
}
}
/**
* 获取设备Token使用统计
* @param {String} snCode - 设备SN码
* @param {Object} options - 查询选项
* @param {Date} options.startDate - 开始日期
* @param {Date} options.endDate - 结束日期
* @returns {Promise<Object>} 统计数据
*/
static async getDeviceTokenStats(snCode, options = {}) {
try {
const models = Framework.getModels();
const { ai_call_records } = models;
const { Op } = Framework.getSequelize();
if (!ai_call_records) {
return null;
}
const where = {
sn_code: snCode,
is_delete: 0
};
if (options.startDate && options.endDate) {
where.create_time = {
[Op.between]: [options.startDate, options.endDate]
};
}
const stats = await ai_call_records.findOne({
where,
attributes: [
[Framework.getSequelize().fn('COUNT', Framework.getSequelize().col('id')), 'total_calls'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('prompt_tokens')), 'total_prompt_tokens'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('completion_tokens')), 'total_completion_tokens'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('total_tokens')), 'total_tokens'],
[Framework.getSequelize().fn('SUM', Framework.getSequelize().col('cost_amount')), 'total_cost'],
[Framework.getSequelize().fn('AVG', Framework.getSequelize().col('response_time')), 'avg_response_time']
],
raw: true
});
return stats;
} catch (error) {
console.error('[AI记录] 获取设备统计失败:', error.message);
throw error;
}
}
}
module.exports = AiCallRecorder;

View File

@@ -1,416 +1,393 @@
/**
* AI智能服务
* 提供岗位筛选、简历分析、聊天生成等AI功能
*/
const axios = require('axios'); const axios = require('axios');
const config = require('../../config/config');
const AiCallRecorder = require('./ai_call_recorder.js');
class AIService { /**
constructor(config = {}) { * Qwen 2.5 大模型服务
this.apiKey = config.apiKey || process.env.AI_API_KEY || ''; * 集成阿里云 DashScope API提供智能化的岗位筛选、聊天生成、简历分析等功能
this.baseURL = config.baseURL || process.env.AI_BASE_URL || 'https://api.deepseek.com'; */
this.model = config.model || 'deepseek-chat'; class aiService {
this.timeout = config.timeout || 30000; constructor() {
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
// 创建axios实例 // 使用 DashScope 兼容 OpenAI 格式的接口
this.client = axios.create({ this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
baseURL: this.baseURL, // Qwen 2.5 模型qwen-turbo快速、qwen-plus增强、qwen-max最强
timeout: this.timeout, this.model = config.ai?.model || 'qwen-turbo';
headers: { this.maxRetries = 3;
'Content-Type': 'application/json',
'Authorization': `${this.apiKey}`
}
});
}
/**
* 调用AI接口
* @param {Array} messages - 消息数组
* @param {Object} options - 额外选项
* @returns {Promise<String>} AI响应内容
*/
async chat(messages, options = {}) {
try {
const response = await this.client.post('/v1/chat/completions', {
model: this.model,
messages,
temperature: options.temperature || 0.7,
max_tokens: options.max_tokens || 2000,
...options
});
return response.data.choices[0].message.content;
} catch (error) {
console.warn('AI服务调用失败:', error.message);
throw new Error(`AI服务调用失败: ${error.message}`);
} }
}
/** /**
* 分析简历竞争力 * 调用 Qwen 2.5 API
* @param {Object} resumeData - 简历数据 * @param {string} prompt - 提示词
* @returns {Promise<Object>} 分析结果 * @param {object} options - 配置选项
*/ * @returns {Promise<object>} API响应结果
async analyzeResume(resumeData) { */
const prompt = `请分析以下简历的竞争力,并提供详细评估: async callAPI(prompt, options = {}) {
const startTime = Date.now();
const requestData = {
model: this.model,
messages: [
{
role: 'system',
content: options.systemPrompt || '你是一个专业的招聘顾问,擅长分析岗位信息、生成聊天内容和分析简历匹配度。'
},
{
role: 'user',
content: prompt
}
],
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2000,
top_p: options.topP || 0.9
};
const requestContent = JSON.stringify(requestData.messages);
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const response = await axios.post(this.apiUrl, requestData, {
headers: {
'Authorization': `Bearer ${this.apiKey}`, // DashScope 使用 Bearer token 格式
'Content-Type': 'application/json'
},
timeout: 30000
});
const responseTime = Date.now() - startTime;
const responseContent = response.data.choices?.[0]?.message?.content || '';
const usage = response.data.usage || {};
// 记录AI调用异步不阻塞主流程
this.recordAiCall({
user_id: options.user_id,
sn_code: options.sn_code,
service_type: options.service_type || 'completion',
model_name: this.model,
prompt_tokens: usage.prompt_tokens || 0,
completion_tokens: usage.completion_tokens || 0,
total_tokens: usage.total_tokens || 0,
request_content: requestContent,
response_content: responseContent,
cost_amount: this.calculateCost(usage.total_tokens || 0),
status: 'success',
response_time: responseTime,
api_provider: 'qwen',
business_type: options.business_type,
reference_id: options.reference_id
}).catch(err => {
console.warn('记录AI调用失败不影响主流程:', err.message);
});
return {
data: response.data,
content: responseContent
};
} catch (error) {
const responseTime = Date.now() - startTime;
console.log(`Qwen 2.5 API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`);
// 记录失败的调用
if (attempt === this.maxRetries) {
this.recordAiCall({
user_id: options.user_id,
sn_code: options.sn_code,
service_type: options.service_type || 'completion',
model_name: this.model,
request_content: requestContent,
status: 'failed',
error_message: error.message,
response_time: responseTime,
api_provider: 'qwen',
business_type: options.business_type,
reference_id: options.reference_id
}).catch(err => {
console.warn('记录失败调用失败:', err.message);
});
throw new Error(error.message);
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
/**
* 记录AI调用
* @param {Object} params - 调用参数
* @returns {Promise}
*/
async recordAiCall(params) {
try {
await AiCallRecorder.record(params);
} catch (error) {
// 记录失败不应影响主流程
console.warn('AI调用记录失败:', error.message);
}
}
/**
* 计算调用费用
* @param {Number} totalTokens - 总Token数
* @returns {Number} 费用(元)
*/
calculateCost(totalTokens) {
// 阿里云 Qwen 价格(元/1000 tokens
// qwen-turbo: ¥0.003, qwen-plus: ¥0.004, qwen-max: ¥0.12
// 这里使用 qwen-turbo 的价格作为默认值
const pricePerThousand = 0.003;
return (totalTokens / 1000) * pricePerThousand;
}
/**
* 岗位智能筛选
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @returns {Promise<object>} 筛选结果
*/
async analyzeJob(jobInfo, resumeInfo) {
const prompt = `
请分析以下岗位信息,并给出详细的评估结果:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 薪资范围:${jobInfo.salary || '未知'}
- 工作地点:${jobInfo.location || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息: 简历信息:
- 姓名: ${resumeData.fullName || '未知'} - 技能标签:${resumeInfo.skills || '未知'}
- 工作年限: ${resumeData.workYears || '未知'} - 工作经验:${resumeInfo.experience || '未知'}
- 教育背景: ${resumeData.education || '未知'} - 教育背景${resumeInfo.education || '未知'}
- 期望职位: ${resumeData.expectedPosition || '未知'} - 期望薪资:${resumeInfo.expectedSalary || '未知'}
- 期望薪资: ${resumeData.expectedSalary || '未知'}
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
- 工作经历: ${resumeData.workExperience || '未提供'}
- 项目经历: ${resumeData.projectExperience || '未提供'}
请从以下维度进行评估1-100分 请从以下维度进行分析
1. 技术能力 1. 技能匹配度0-100分
2. 项目经验 2. 经验匹配度0-100分
3. 教育背景 3. 薪资合理性0-100分
4. 工作年限匹配度 4. 公司质量评估0-100分
5. 综合竞争力 5. 是否为外包岗位(是/否)
6. 综合推荐指数0-100分
7. 详细分析说明
8. 投递建议
返回JSON格式 请以JSON格式返回结果。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。',
temperature: 0.3,
business_type: 'job_analysis',
service_type: 'completion'
});
try {
// 尝试解析JSON响应
const analysis = JSON.parse(result.content);
return {
analysis: analysis
};
} catch (parseError) {
// 如果解析失败,返回原始内容
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成个性化聊天内容
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型 (greeting/interview/followup)
* @returns {Promise<object>} 聊天内容
*/
async generateChatContent(jobInfo, resumeInfo, chatType = 'greeting') {
const chatTypeMap = {
'greeting': '初次打招呼',
'interview': '面试邀约',
'followup': '跟进沟通'
};
const prompt = `
请为以下场景生成个性化的聊天内容:
聊天类型:${chatTypeMap[chatType] || chatType}
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 项目经验:${resumeInfo.projects || '未知'}
要求:
1. 内容要自然、专业、个性化
2. 突出简历与岗位的匹配点
3. 避免过于机械化的表达
4. 长度控制在100-200字
5. 体现求职者的诚意和热情
请直接返回聊天内容,不需要其他格式。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘沟通专家,擅长生成自然、专业的求职聊天内容。',
temperature: 0.8,
business_type: 'chat_generation',
service_type: 'chat',
reference_id: jobInfo.jobId || jobInfo.id
});
return result;
}
/**
* 分析简历要素
* @param {string} resumeText - 简历文本内容
* @returns {Promise<object>} 简历分析结果
*/
async analyzeResume(resumeText) {
const prompt = `
请分析以下简历内容,并返回 JSON 格式的分析结果:
简历内容:
${resumeText}
请按以下格式返回 JSON 结果:
{ {
"overallScore": 总分(1-100), "skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
"technicalScore": 技术能力分(1-100), "strengths": "核心优势描述", // 简历的优势和亮点
"projectScore": 项目经验分(1-100), "weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
"educationScore": 教育背景分(1-100), "careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
"experienceScore": 工作年限分(1-100), "competitiveness": 75 // 竞争力评分0-100的整数综合考虑工作年限、技能、经验等因素
"strengths": ["优势1", "优势2", "优势3"],
"weaknesses": ["不足1", "不足2"],
"suggestions": ["建议1", "建议2", "建议3"],
"keySkills": ["核心技能1", "核心技能2"],
"marketCompetitiveness": "市场竞争力描述"
}`;
const messages = [
{ role: 'system', content: '你是一个专业的HR和招聘顾问擅长分析简历和评估候选人竞争力。' },
{ role: 'user', content: prompt }
];
try {
const response = await this.chat(messages, { temperature: 0.3 });
// 提取JSON部分
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error('AI返回格式不正确');
} catch (error) {
console.warn('简历分析失败:', error);
// 返回默认值
return {
overallScore: 60,
technicalScore: 60,
projectScore: 60,
educationScore: 60,
experienceScore: 60,
strengths: ['待AI分析'],
weaknesses: ['待AI分析'],
suggestions: ['请稍后重试'],
keySkills: Array.isArray(resumeData.skills) ? resumeData.skills.slice(0, 5) : [],
marketCompetitiveness: '待AI分析'
};
}
}
/**
* 岗位匹配度评估
* @param {Object} jobData - 岗位数据
* @param {Object} resumeData - 简历数据
* @returns {Promise<Object>} 匹配结果
*/
async matchJobWithResume(jobData, resumeData) {
const prompt = `请评估以下岗位与简历的匹配度:
【岗位信息】
- 职位名称: ${jobData.jobTitle || '未知'}
- 公司名称: ${jobData.companyName || '未知'}
- 薪资范围: ${jobData.salary || '未知'}
- 工作地点: ${jobData.location || '未知'}
- 工作经验要求: ${jobData.experienceRequired || '未知'}
- 学历要求: ${jobData.educationRequired || '未知'}
- 岗位描述: ${jobData.jobDescription || '未提供'}
- 技能要求: ${jobData.skillsRequired || '未提供'}
【简历信息】
- 工作年限: ${resumeData.workYears || '未知'}
- 教育背景: ${resumeData.education || '未知'}
- 技能标签: ${Array.isArray(resumeData.skills) ? resumeData.skills.join('、') : '未知'}
- 期望职位: ${resumeData.expectedPosition || '未知'}
- 期望薪资: ${resumeData.expectedSalary || '未知'}
请分析:
1. 技能匹配度
2. 经验匹配度
3. 薪资匹配度
4. 是否为外包岗位(根据公司名称、岗位描述判断)
5. 综合推荐度
返回JSON格式
{
"matchScore": 匹配度分数(1-100),
"skillMatch": 技能匹配度(1-100),
"experienceMatch": 经验匹配度(1-100),
"salaryMatch": 薪资匹配度(1-100),
"isOutsourcing": 是否外包(true/false),
"outsourcingConfidence": 外包判断置信度(0-1),
"recommendLevel": "推荐等级(excellent/good/medium/low)",
"matchReasons": ["匹配原因1", "匹配原因2"],
"concerns": ["顾虑点1", "顾虑点2"],
"applyAdvice": "投递建议"
}`;
const messages = [
{ role: 'system', content: '你是一个专业的职业规划师,擅长评估岗位与求职者的匹配度。' },
{ role: 'user', content: prompt }
];
try {
const response = await this.chat(messages, { temperature: 0.3 });
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error('AI返回格式不正确');
} catch (error) {
console.warn('岗位匹配分析失败:', error);
// 返回默认值
return {
matchScore: 50,
skillMatch: 50,
experienceMatch: 50,
salaryMatch: 50,
isOutsourcing: false,
outsourcingConfidence: 0,
recommendLevel: 'medium',
matchReasons: ['待AI分析'],
concerns: ['待AI分析'],
applyAdvice: '建议人工审核'
};
}
}
/**
* 批量评估岗位(用于智能筛选)
* @param {Array} jobs - 岗位列表
* @param {Object} resumeData - 简历数据
* @returns {Promise<Array>} 评估结果列表
*/
async batchMatchJobs(jobs, resumeData) {
const results = [];
// 限制并发数量避免API限流
const concurrency = 3;
for (let i = 0; i < jobs.length; i += concurrency) {
const batch = jobs.slice(i, i + concurrency);
const batchPromises = batch.map(job =>
this.matchJobWithResume(job, resumeData).catch(err => {
console.warn(`岗位${job.jobId}匹配失败:`, err.message);
return {
jobId: job.jobId,
matchScore: 0,
error: err.message
};
})
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// 避免请求过快,休眠一下
if (i + concurrency < jobs.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}
/**
* 生成聊天内容
* @param {Object} context - 聊天上下文
* @returns {Promise<String>} 生成的聊天内容
*/
async generateChatContent(context) {
const { jobInfo, resumeInfo, chatType = 'greeting', previousMessages = [] } = context;
let prompt = '';
switch (chatType) {
case 'greeting':
prompt = `作为求职者向HR发送第一条消息表达对以下岗位的兴趣
岗位: ${jobInfo.jobTitle}
公司: ${jobInfo.companyName}
要求: 简洁、专业、突出自己的优势不超过100字`;
break;
case 'follow_up':
prompt = `HR已查看简历但未回复需要发送一条礼貌的跟进消息
岗位: ${jobInfo.jobTitle}
要求: 礼貌、不唐突、展现持续兴趣不超过80字`;
break;
case 'interview_confirm':
prompt = `HR发出面试邀约需要确认并表达感谢
岗位: ${jobInfo.jobTitle}
面试时间: ${context.interviewTime || '待定'}
要求: 专业、感谢、确认参加不超过60字`;
break;
case 'reply':
prompt = `HR说: "${context.hrMessage}"
请作为求职者回复,要求: 自然、专业、回答问题不超过100字`;
break;
default:
prompt = `生成一条针对${jobInfo.jobTitle}岗位的沟通消息`;
}
const messages = [
{ role: 'system', content: '你是一个求职者需要与HR进行专业、礼貌的沟通。回复要简洁、真诚、突出优势。' },
...previousMessages.map(msg => ({
role: msg.role,
content: msg.content
})),
{ role: 'user', content: prompt }
];
try {
const response = await this.chat(messages, { temperature: 0.8, max_tokens: 200 });
return response.trim();
} catch (error) {
console.warn('生成聊天内容失败:', error);
// 返回默认模板
switch (chatType) {
case 'greeting':
return `您好,我对贵司的${jobInfo.jobTitle}岗位非常感兴趣,我有相关工作经验,期待能有机会详细沟通。`;
case 'follow_up':
return `您好,不知道您对我的简历是否感兴趣?期待您的回复。`;
case 'interview_confirm':
return `好的,感谢您的面试邀约,我会准时参加。`;
default:
return `您好,期待与您沟通。`;
}
}
}
/**
* 判断是否为面试邀约
* @param {String} message - HR消息内容
* @returns {Promise<Object>} 判断结果
*/
async detectInterviewInvitation(message) {
const prompt = `判断以下HR消息是否为面试邀约并提取关键信息
消息内容: "${message}"
返回JSON格式
{
"isInterview": 是否为面试邀约(true/false),
"confidence": 置信度(0-1),
"interviewType": "面试类型(phone/video/onsite/unknown)",
"interviewTime": "面试时间(如果提到)",
"interviewLocation": "面试地点(如果提到)",
"needReply": 是否需要回复确认(true/false)
}`;
const messages = [
{ role: 'system', content: '你是一个文本分析专家,擅长识别面试邀约信息。' },
{ role: 'user', content: prompt }
];
try {
const response = await this.chat(messages, { temperature: 0.1 });
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error('AI返回格式不正确');
} catch (error) {
console.warn('面试邀约判断失败:', error);
// 简单的关键词判断作为降级方案
const keywords = ['面试', '邀请', '来面', '约您', '见面', '电话沟通', '视频面试'];
const isInterview = keywords.some(kw => message.includes(kw));
return {
isInterview,
confidence: isInterview ? 0.7 : 0.3,
interviewType: 'unknown',
interviewTime: null,
interviewLocation: null,
needReply: isInterview
};
}
}
/**
* 分析HR反馈情感
* @param {String} message - HR消息内容
* @returns {Promise<Object>} 情感分析结果
*/
async analyzeSentiment(message) {
const prompt = `分析以下HR消息的情感倾向
消息: "${message}"
返回JSON格式
{
"sentiment": "情感倾向(positive/neutral/negative)",
"interest": "兴趣程度(high/medium/low)",
"urgency": "紧急程度(high/medium/low)",
"keywords": ["关键词1", "关键词2"]
}`;
const messages = [
{ role: 'system', content: '你是一个情感分析专家。' },
{ role: 'user', content: prompt }
];
try {
const response = await this.chat(messages, { temperature: 0.1 });
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error('AI返回格式不正确');
} catch (error) {
console.warn('情感分析失败:', error);
return {
sentiment: 'neutral',
interest: 'medium',
urgency: 'low',
keywords: []
};
}
}
} }
// 导出单例 要求:
let instance = null; 1. skillTags 必须是字符串数组
2. strengths、weaknesses、careerSuggestion 是字符串描述
3. competitiveness 必须是 0-100 之间的整数
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
`;
module.exports = { const result = await this.callAPI(prompt, {
/** systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
* 获取AI服务实例 temperature: 0.3,
* @param {Object} config - 配置选项 maxTokens: 1500,
* @returns {AIService} business_type: 'resume_analysis',
*/ service_type: 'completion'
getInstance(config) { });
if (!instance) {
instance = new AIService(config); try {
// 尝试从返回内容中提取 JSON
let content = result.content.trim();
// 如果返回内容被代码块包裹,提取其中的 JSON
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || content.match(/(\{[\s\S]*\})/);
if (jsonMatch) {
content = jsonMatch[1];
}
const analysis = JSON.parse(content);
return {
analysis: analysis
};
} catch (parseError) {
console.error(`[AI服务] 简历分析结果解析失败:`, parseError);
console.error(`[AI服务] 原始内容:`, result.content);
return {
analysis: {
content: result.content,
parseError: true
}
};
}
} }
return instance;
},
/** /**
* 创建新的AI服务实例 * 生成面试邀约内容
* @param {Object} config - 配置选项 * @param {object} jobInfo - 岗位信息
* @returns {AIService} * @param {object} chatHistory - 聊天历史
*/ * @returns {Promise<object>} 面试邀约内容
createInstance(config) { */
return new AIService(config); async generateInterviewInvitation(jobInfo, chatHistory) {
} const prompt = `
}; 请基于以下信息生成面试邀约内容:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 工作地点:${jobInfo.location || '未知'}
聊天历史:
${chatHistory || '无'}
要求:
1. 表达面试邀约的诚意
2. 提供灵活的时间选择
3. 说明面试形式和地点
4. 体现对候选人的重视
5. 语言自然、专业
请直接返回面试邀约内容。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的HR擅长生成面试邀约内容。',
temperature: 0.6,
business_type: 'interview_invitation',
service_type: 'chat'
});
return result;
}
/**
* 识别外包岗位
* @param {object} jobInfo - 岗位信息
* @returns {Promise<object>} 外包识别结果
*/
async identifyOutsourcingJob(jobInfo) {
const prompt = `
请分析以下岗位信息,判断是否为外包岗位:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
- 工作地点:${jobInfo.location || '未知'}
外包岗位特征:
1. 公司名称包含"外包"、"派遣"、"人力"等关键词
2. 岗位描述提到"项目外包"、"驻场开发"等
3. 技能要求过于宽泛或具体
4. 工作地点频繁变动
5. 薪资结构不明确
请判断是否为外包岗位,并给出详细分析。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的岗位分析师,擅长识别外包岗位的特征。',
temperature: 0.3,
business_type: 'outsourcing_detection',
service_type: 'completion'
});
return result;
}
}
module.exports = new aiService();

145
docs/ai_service_config.md Normal file
View File

@@ -0,0 +1,145 @@
# AI 服务配置说明
## 环境变量配置
AI 服务需要通过环境变量进行配置,支持阿里云 DashScope API。
### 必需的环境变量
`.env` 文件或系统环境变量中配置以下参数:
```bash
# AI API 密钥(阿里云 DashScope API Key
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
# AI API 基础 URL阿里云 DashScope 兼容 OpenAI 格式接口)
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# AI 模型名称
# 可选值:
# - qwen-turbo快速推荐日常使用
# - qwen-plus增强平衡性能和成本
# - qwen-max最强高质量输出
# - qwen-long长文本支持超长上下文
AI_MODEL=qwen-turbo
```
### 配置示例
#### 1. 开发环境配置 (.env)
```bash
# 阿里云 DashScope 配置
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
AI_MODEL=qwen-turbo
```
#### 2. 生产环境配置 (.env.production)
```bash
# 阿里云 DashScope 配置(生产环境使用增强版)
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
AI_MODEL=qwen-plus
```
### 阿里云模型对比
| 模型 | 速度 | 质量 | 成本 | 适用场景 |
|------|------|------|------|----------|
| qwen-turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 日常对话、简单分析 |
| qwen-plus | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 复杂分析、专业任务 |
| qwen-max | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 高质量输出、关键任务 |
| qwen-long | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 长文本处理、文档分析 |
### 代码中使用
```javascript
// 使用默认配置(从环境变量读取)
const AIService = require('./services/ai_service.js');
const aiService = AIService.getInstance();
// 使用自定义配置
const aiService = AIService.createInstance({
apiKey: 'sk-custom-key',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: 'qwen-plus',
timeout: 60000
});
```
### API 认证格式
阿里云 DashScope API 使用标准的 Bearer Token 认证:
```
Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxx
```
### 注意事项
1. **API Key 安全**
- 不要将 API Key 硬编码在代码中
- 不要将 `.env` 文件提交到版本控制
- 生产环境使用独立的 API Key
2. **模型选择建议**
- 开发/测试:使用 `qwen-turbo`(成本低)
- 生产环境:使用 `qwen-plus`(性能平衡)
- 关键业务:使用 `qwen-max`(质量最高)
3. **速率限制**
- 注意 API 的 QPM每分钟请求数限制
- 根据套餐调整并发数量
- 实现重试和错误处理机制
4. **成本控制**
- 监控 Token 使用量
- 设置合理的 `max_tokens` 限制
- 定期查看账单和用量统计
### 获取 API Key
1. 访问阿里云控制台https://dashscope.console.aliyun.com/
2. 进入 API-KEY 管理
3. 创建新的 API Key
4. 复制 API Key 并保存到环境变量
### 故障排查
#### 问题 1Authentication Fails
```
错误auth header format should be Bearer sk-...
解决:检查 AI_API_KEY 是否正确配置
```
#### 问题 2连接超时
```
错误timeout of 30000ms exceeded
解决:
1. 检查网络连接
2. 增加 timeout 配置
3. 检查 AI_BASE_URL 是否正确
```
#### 问题 3模型不存在
```
错误model not found
解决:检查 AI_MODEL 配置,确保使用支持的模型名称
```
### 迁移指南
如果之前使用其他 AI 服务(如 DeepSeek迁移步骤
1. 更新环境变量配置
2. 修改 API_BASE_URL
3. 更新模型名称
4. 测试 AI 调用功能
5. 验证响应格式
---
**配置更新时间**: 2025-12-27
**维护者**: 系统管理员

364
docs/ai_service_unified.md Normal file
View File

@@ -0,0 +1,364 @@
# AI 服务统一说明
**更新时间**: 2025-12-27
---
## 统一后的 AI 服务架构
系统已完成 AI 服务的统一整理,现在只保留一个标准的 AI 服务实现。
---
## 文件位置
### ✅ 保留的文件(唯一 AI 服务实现)
**核心服务:**
- **`api/services/ai_service.js`** - AI 服务主文件(基于阿里云 Qwen 2.5
- **`api/services/ai_call_recorder.js`** - AI 调用记录服务
**导出管理:**
- **`api/services/index.js`** - 服务统一导出
**数据库层:**
- **`api/model/ai_call_records.js`** - AI 调用记录模型
**后台管理:**
- **`api/controller_admin/ai_call_records.js`** - 后台管理 API
**前端界面:**
- **`admin/src/views/system/ai_call_records.vue`** - 管理界面
- **`admin/src/api/system/ai_call_records_server.js`** - API 服务
### ❌ 已删除的文件
- ~~`api/middleware/job/aiService.js`~~ - 已删除(内容已迁移到 `services/ai_service.js`
---
## 使用方式
### 1. 直接引用(推荐)
```javascript
const aiService = require('./services/ai_service');
// 使用 AI 服务
const result = await aiService.analyzeJob(jobInfo, resumeInfo);
```
### 2. 通过服务管理器
```javascript
const { AIService } = require('./services');
// 使用 AI 服务
const result = await AIService.analyzeJob(jobInfo, resumeInfo);
```
---
## AI 服务功能列表
### 核心方法
| 方法 | 说明 | 业务类型 |
|------|------|---------|
| `callAPI(prompt, options)` | 基础 API 调用 | 自定义 |
| `analyzeJob(jobInfo, resumeInfo)` | 岗位智能筛选 | `job_analysis` |
| `generateChatContent(jobInfo, resumeInfo, chatType)` | 生成个性化聊天 | `chat_generation` |
| `analyzeResume(resumeText)` | 简历分析 | `resume_analysis` |
| `generateInterviewInvitation(jobInfo, chatHistory)` | 生成面试邀约 | `interview_invitation` |
| `identifyOutsourcingJob(jobInfo)` | 识别外包岗位 | `outsourcing_detection` |
### 辅助方法
| 方法 | 说明 |
|------|------|
| `recordAiCall(params)` | 记录 AI 调用 |
| `calculateCost(totalTokens)` | 计算调用费用 |
---
## Token 自动记录
所有通过 `callAPI()` 方法的调用都会自动记录以下信息:
- **Token 使用量**prompt_tokens, completion_tokens, total_tokens
- **成本信息**:基于模型计算的费用
- **性能指标**:响应时间(毫秒)
- **状态跟踪**:成功/失败状态
- **业务关联**business_type, reference_id
- **请求追踪**:完整的请求和响应内容
记录过程是异步非阻塞的,不会影响 AI 调用的主流程。
---
## 配置说明
### 环境变量
`.env` 文件中配置:
```bash
# 阿里云 DashScope API Key
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
# 模型选择(可选)
AI_MODEL=qwen-turbo # qwen-turbo, qwen-plus, qwen-max, qwen-long
```
### 代码配置
```javascript
// 在 config/config.js 中
module.exports = {
ai: {
apiKey: process.env.AI_API_KEY,
model: process.env.AI_MODEL || 'qwen-turbo'
}
}
```
---
## 模型选择
| 模型 | 速度 | 质量 | 成本 | 价格(元/1000 tokens|
|------|------|------|------|---------------------|
| qwen-turbo | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ¥0.003 |
| qwen-plus | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ¥0.004 |
| qwen-max | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ¥0.12 |
| qwen-long | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | - |
---
## 使用示例
### 示例 1岗位分析
```javascript
const aiService = require('./services/ai_service');
const jobInfo = {
companyName: '阿里巴巴',
jobTitle: 'Node.js 高级工程师',
salary: '30-50K',
location: '杭州',
description: '负责后端服务开发...',
skills: 'Node.js, MySQL, Redis'
};
const resumeInfo = {
skills: 'Node.js, JavaScript, MySQL',
experience: '5年后端开发经验',
education: '本科',
expectedSalary: '35K'
};
const result = await aiService.analyzeJob(jobInfo, resumeInfo);
console.log(result.analysis);
```
### 示例 2生成聊天内容
```javascript
const result = await aiService.generateChatContent(
jobInfo,
resumeInfo,
'greeting' // greeting, interview, followup
);
console.log(result.content);
```
### 示例 3简历分析
```javascript
const resumeText = `
姓名:张三
技能Node.js, React, MySQL
工作经验3年全栈开发
...
`;
const result = await aiService.analyzeResume(resumeText);
console.log(result.analysis);
// {
// skillTags: ['Node.js', 'React', 'MySQL'],
// strengths: '...',
// weaknesses: '...',
// careerSuggestion: '...',
// competitiveness: 75
// }
```
### 示例 4自定义 AI 调用
```javascript
const result = await aiService.callAPI(
'请帮我分析这个岗位的发展前景...',
{
systemPrompt: '你是一个职业规划专家...',
temperature: 0.7,
maxTokens: 2000,
user_id: 123,
business_type: 'career_analysis'
}
);
console.log(result.content);
```
---
## 错误处理
AI 服务内置了重试机制(最多 3 次)和错误处理:
```javascript
try {
const result = await aiService.analyzeJob(jobInfo, resumeInfo);
} catch (error) {
console.error('AI 调用失败:', error.message);
// 错误会自动记录到 ai_call_records 表
}
```
---
## 监控与统计
### 查看调用记录
登录后台管理系统:**系统设置** → **AI调用记录**
### 统计信息
- 总调用次数
- Token 总使用量
- 总费用统计
- 平均响应时间
- 成功率
### 编程方式获取统计
```javascript
const AiCallRecorder = require('./services/ai_call_recorder');
// 获取用户统计
const userStats = await AiCallRecorder.getUserTokenStats(userId, {
startDate: new Date('2025-01-01'),
endDate: new Date('2025-12-31')
});
console.log(userStats);
// {
// total_calls: 100,
// total_prompt_tokens: 5000,
// total_completion_tokens: 3000,
// total_tokens: 8000,
// total_cost: 24.00,
// avg_response_time: 1500
// }
```
---
## 注意事项
### 1. API Key 安全
- ❌ 不要将 API Key 硬编码在代码中
- ❌ 不要将 `.env` 文件提交到版本控制
- ✅ 使用环境变量管理 API Key
- ✅ 生产环境使用独立的 API Key
### 2. 成本控制
- 选择合适的模型(开发用 turbo生产用 plus
- 设置合理的 `maxTokens` 限制
- 监控 Token 使用量
- 定期查看费用统计
### 3. 性能优化
- 重试机制已内置3 次)
- 超时设置为 30 秒
- Token 记录是异步的,不阻塞主流程
### 4. 数据隐私
- 请求和响应内容会完整记录到数据库
- 注意敏感信息的处理
- 定期清理历史记录
---
## 迁移指南
如果你的代码之前引用了 `middleware/job/aiService.js`,请修改为:
```javascript
// ❌ 旧代码
const aiService = require('../middleware/job/aiService');
// ✅ 新代码
const aiService = require('../services/ai_service');
```
功能保持完全一致,只是路径发生了变化。
---
## 故障排查
### 问题 1模型未加载
**错误信息:** `Cannot read property 'findAll' of undefined`
**解决方法:**
1. 确认已执行建表 SQL`_sql/create_ai_call_records_table.sql`
2. 重启 Node.js 服务
3. 检查 `api/model/ai_call_records.js` 是否存在
### 问题 2认证失败
**错误信息:** `auth header format should be Bearer sk-...`
**解决方法:**
1. 检查 `.env` 文件中的 `AI_API_KEY`
2. 确认 API Key 格式正确(以 `sk-` 开头)
3. 验证 API Key 有效性
### 问题 3记录失败
**警告信息:** `记录AI调用失败不影响主流程`
**解决方法:**
1. 检查数据库连接
2. 确认 `ai_call_records` 表存在
3. 查看详细错误日志
### 问题 4费用计算不准确
**解决方法:**
1. 检查 `calculateCost()` 方法中的价格配置
2. 根据实际使用的模型调整价格
3. 定期对账单进行核对
---
## 相关文档
- [AI 服务配置说明](ai_service_config.md) - 详细的环境配置指南
- [功能实施总结](implementation_summary.md) - 完整的功能实施文档
- [API 文档](../api/controller_admin/ai_call_records.js) - 后台 API 接口说明
---
**文档版本**: 1.0
**最后更新**: 2025-12-27
**维护者**: 开发团队

View File

@@ -0,0 +1,561 @@
# 自动找工作系统 - 功能实施总结
**更新时间**: 2025-12-27
---
## 已完成功能概览
本文档总结了最近完成的两个主要功能模块:
1. **价格套餐管理系统**
2. **AI调用记录与Token追踪系统**
---
## 一、价格套餐管理系统
### 功能描述
将原本硬编码在 `api/controller_front/config.js` 中的价格套餐数据迁移到数据库,并提供完整的后台管理界面。
### 实施文件清单
#### 数据库层
-`_sql/create_pricing_plans_table.sql` - 数据表创建脚本
-`_sql/insert_pricing_plans_data.sql` - 初始数据插入脚本
-`api/model/pricing_plans.js` - Sequelize 数据模型
#### 后端API层
-`api/controller_admin/pricing_plans.js` - 后台管理API5个端点
- `POST /pricing_plans/list` - 分页查询
- `GET /pricing_plans/detail` - 获取详情
- `POST /pricing_plans/create` - 创建套餐
- `POST /pricing_plans/update` - 更新套餐
- `POST /pricing_plans/delete` - 删除套餐(软删除)
-`api/controller_front/config.js` (修改第90-136行) - 前端API改为数据库查询
#### 前端层
-`admin/src/api/system/pricing_plans_server.js` - API服务层
-`admin/src/views/system/pricing_plans.vue` - 管理界面组件
-`admin/src/router/component-map.js` (新增映射) - 组件注册
### 数据库表结构
```sql
CREATE TABLE `pricing_plans` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL COMMENT '套餐名称',
`duration` VARCHAR(50) NOT NULL COMMENT '时长描述',
`days` INT(11) NOT NULL COMMENT '天数(-1表示永久',
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
`original_price` DECIMAL(10,2) NULL COMMENT '原价',
`unit` VARCHAR(20) NOT NULL DEFAULT '',
`discount` VARCHAR(50) NULL COMMENT '折扣描述',
`features` TEXT NOT NULL COMMENT '功能列表JSON格式',
`featured` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否推荐',
`is_active` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT '排序顺序',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 初始数据
系统预置了4个价格套餐
1. **体验套餐** - 7天 - ¥28
2. **月度套餐** - 30天 - ¥99推荐
3. **季度套餐** - 90天 - ¥269
4. **终生套餐** - 永久 - ¥888
### 菜单位置
**用户管理****价格套餐管理**
### 关键实现细节
#### 前端接口向后兼容
`GET /api/config/pricing-plans` 接口保持原有响应格式:
```javascript
[
{
"id": 1,
"name": "体验套餐",
"duration": "7天",
"days": 7,
"price": 28,
"originalPrice": 28,
"unit": "元",
"features": ["7天使用权限", "全功能体验", "技术支持"],
"featured": false
}
]
```
#### 状态控制逻辑
- 前端API仅返回 `is_active = 1``is_delete = 0` 的套餐
- 按照 `sort_order` ASC, `id` ASC 排序
- Features字段从JSON字符串自动解析为数组
#### 表单组件修复
修复了单选按钮组件使用方式(重要):
```javascript
// ❌ 错误写法
{
title: '是否推荐',
key: 'featured',
type: 'radio', // 错误
options: [...]
}
// ✅ 正确写法
{
title: '是否推荐',
key: 'featured',
com: 'Radio', // 正确
options: [
{ value: 1, label: '推荐' },
{ value: 0, label: '普通' }
]
}
```
---
## 二、AI调用记录与Token追踪系统
### 功能描述
为所有AI调用添加自动记录功能追踪Token使用量、调用成本、响应时间等关键指标并提供后台管理和统计分析界面。
### 实施文件清单
#### 数据库层
-`_sql/create_ai_call_records_table.sql` - 数据表创建脚本
-`api/model/ai_call_records.js` - Sequelize 数据模型
#### 服务层
-`api/services/ai_call_recorder.js` - AI调用记录服务
-`api/services/ai_service.js` - AI服务集成Token记录
#### 后端API层
-`api/controller_admin/ai_call_records.js` - 后台管理API5个端点
- `POST /ai_call_records/list` - 分页查询
- `GET /ai_call_records/detail` - 获取详情
- `GET /ai_call_records/stats` - 统计分析
- `POST /ai_call_records/delete` - 删除记录
- `POST /ai_call_records/batch_delete` - 批量删除
#### 前端层
-`admin/src/api/system/ai_call_records_server.js` - API服务层
-`admin/src/views/system/ai_call_records.vue` - 管理界面组件
-`admin/src/router/component-map.js` (新增映射) - 组件注册
### 数据库表结构
```sql
CREATE TABLE `ai_call_records` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NULL COMMENT '用户ID',
`sn_code` VARCHAR(100) NULL COMMENT '设备SN码',
`service_type` VARCHAR(50) NOT NULL COMMENT '服务类型chat/completion/embedding',
`model_name` VARCHAR(100) NOT NULL COMMENT '模型名称',
`prompt_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '输入Token数',
`completion_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '输出Token数',
`total_tokens` INT(11) NOT NULL DEFAULT 0 COMMENT '总Token数',
`request_content` TEXT NULL COMMENT '请求内容',
`response_content` TEXT NULL COMMENT '响应内容',
`cost_amount` DECIMAL(10,4) NULL COMMENT '费用(元)',
`status` VARCHAR(20) NOT NULL DEFAULT 'success' COMMENT '状态success/failed',
`error_message` TEXT NULL COMMENT '错误信息',
`response_time` INT(11) NULL COMMENT '响应时间(毫秒)',
`api_provider` VARCHAR(50) NULL DEFAULT 'qwen' COMMENT 'API提供商',
`business_type` VARCHAR(50) NULL COMMENT '业务类型',
`reference_id` VARCHAR(100) NULL COMMENT '关联业务ID',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_delete` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### AI服务集成
#### 使用的AI服务
系统使用 **阿里云DashScope API (Qwen 2.5)**而非DeepSeek。
**关键配置:**
```javascript
// api/services/ai_service.js
class aiService {
constructor() {
this.apiKey = config.ai?.apiKey || process.env.DASHSCOPE_API_KEY;
this.apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
this.model = config.ai?.model || 'qwen-turbo';
this.maxRetries = 3;
}
}
```
#### 环境变量配置
`.env` 文件中配置:
```bash
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
AI_MODEL=qwen-turbo # 可选qwen-turbo, qwen-plus, qwen-max, qwen-long
```
详细配置说明见:[docs/ai_service_config.md](ai_service_config.md:1)
#### Token记录集成
AI服务的 `callAPI()` 方法自动记录所有调用:
```javascript
async callAPI(prompt, options = {}) {
const startTime = Date.now();
// ... 调用API ...
// 成功时自动记录Token使用量
this.recordAiCall({
user_id: options.user_id,
sn_code: options.sn_code,
service_type: options.service_type || 'completion',
model_name: this.model,
prompt_tokens: usage.prompt_tokens || 0,
completion_tokens: usage.completion_tokens || 0,
total_tokens: usage.total_tokens || 0,
cost_amount: this.calculateCost(usage.total_tokens || 0),
status: 'success',
response_time: responseTime,
api_provider: 'qwen',
business_type: options.business_type,
reference_id: options.reference_id
}).catch(err => {
console.warn('记录AI调用失败不影响主流程:', err.message);
});
}
```
#### 业务类型分类
系统中的AI调用按业务类型分类
| 业务类型 | 说明 | 调用方法 |
|---------|------|---------|
| `job_analysis` | 岗位分析 | `analyzeJob()` |
| `chat_generation` | 聊天内容生成 | `generateChatContent()` |
| `resume_analysis` | 简历分析 | `analyzeResume()` |
| `interview_invitation` | 面试邀约 | `generateInterviewInvitation()` |
| `outsourcing_detection` | 外包检测 | `identifyOutsourcingJob()` |
#### 成本计算
基于Qwen模型定价
```javascript
calculateCost(totalTokens) {
// qwen-turbo: ¥0.003/1000 tokens
// qwen-plus: ¥0.004/1000 tokens
// qwen-max: ¥0.12/1000 tokens
const pricePerThousand = 0.003;
return (totalTokens / 1000) * pricePerThousand;
}
```
### 管理界面功能
#### 筛选功能
- 按用户ID搜索
- 按设备SN码搜索
- 按业务类型筛选
- 按服务类型筛选
- 按状态筛选
- 按时间范围筛选
#### 统计功能
- 总调用次数
- Token总使用量输入/输出/总计)
- 总费用统计
- 平均响应时间
- 成功率统计
#### 操作功能
- 查看详情(完整请求/响应内容)
- 单条删除
- 批量删除
### 菜单位置
**系统设置****AI调用记录**
---
## 三、关键技术点总结
### 1. 组件注册规范
前端Vue组件必须在 `admin/src/router/component-map.js` 中注册:
```javascript
// 导入组件
import PricingPlans from '@/views/system/pricing_plans.vue'
import AiCallRecords from '@/views/system/ai_call_records.vue'
// 注册映射
const componentMap = {
'system/pricing_plans': PricingPlans,
'system/ai_call_records': AiCallRecords,
}
```
### 2. 表单控件使用规范
使用 `com` 字段而非 `type` 字段:
```javascript
// 单选按钮
{ com: 'Radio', options: [...] }
// 文本输入框
{ com: 'Input' }
// 文本域
{ com: 'TextArea' }
// 数字输入框
{ com: 'InputNumber' }
// 下拉选择
{ com: 'Select', options: [...] }
```
### 3. 数据库软删除模式
所有查询必须包含 `is_delete = 0` 条件:
```javascript
const list = await model.findAll({
where: {
is_delete: 0,
// ... 其他条件
}
});
```
### 4. 时间戳管理
使用手动管理而非Sequelize自动管理
```javascript
// 模型定义
{
timestamps: false, // 禁用自动时间戳
// 手动定义时间字段
create_time: { type: DataTypes.DATE },
last_modify_time: { type: DataTypes.DATE }
}
// 创建时手动设置
const now = new Date();
await model.create({
// ... 其他字段
create_time: now,
last_modify_time: now
});
```
### 5. JSON字段处理
数据库存储为TEXT类型应用层处理JSON序列化
```javascript
// 保存时
const data = {
features: JSON.stringify(['功能1', '功能2'])
};
// 读取时
const features = JSON.parse(record.features || '[]');
```
### 6. 异步记录模式
日志/记录类操作使用异步非阻塞模式:
```javascript
// 使用 .catch() 而非 try-catch避免阻塞主流程
this.recordAiCall(params).catch(err => {
console.warn('记录失败(不影响主流程):', err.message);
});
// 主流程继续执行
return result;
```
---
## 四、部署检查清单
### 数据库层
- [ ] 执行 `create_pricing_plans_table.sql`
- [ ] 执行 `insert_pricing_plans_data.sql`
- [ ] 执行 `create_ai_call_records_table.sql`
- [ ] 验证表创建成功
### 后端层
- [ ] 重启Node.js服务以加载新模型
- [ ] 验证模型加载:`Framework.getModels()`
- [ ] 配置环境变量 `AI_API_KEY``AI_MODEL`
- [ ] 测试后台API端点
### 前端层
- [ ] 重新编译前端代码
- [ ] 验证组件注册成功
- [ ] 刷新浏览器缓存
- [ ] 测试管理界面功能
### 菜单系统
- [ ] 验证"价格套餐管理"菜单显示
- [ ] 验证"AI调用记录"菜单显示
- [ ] 测试菜单跳转功能
### 功能测试
- [ ] 价格套餐CRUD操作
- [ ] 前端API `/config/pricing-plans` 返回数据库数据
- [ ] AI调用自动记录Token
- [ ] AI调用记录管理界面
- [ ] 统计功能准确性
---
## 五、已知问题与注意事项
### 1. 组件热更新
修改组件映射后需要:
- 重启前端开发服务器
- 清除浏览器缓存
- 刷新页面
### 2. AI服务配置
**重要**系统使用阿里云DashScope API不是DeepSeek。
必须配置正确的环境变量:
```bash
AI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx # 阿里云API Key
AI_MODEL=qwen-turbo # Qwen模型
```
### 3. Token记录异步
Token记录失败不会影响AI调用主流程但会打印警告日志。如果需要确保记录成功应检查数据库连接和表结构。
### 4. 成本计算
当前成本计算使用固定价格qwen-turbo: ¥0.003/1000 tokens如果切换到其他模型需要修改 `calculateCost()` 方法。
### 5. 数据备份
AI调用记录表会快速增长建议
- 定期归档历史数据
- 设置数据保留策略如只保留最近90天
- 建立定期备份机制
---
## 六、文件路径索引
### 价格套餐系统
**数据库**
- `_sql/create_pricing_plans_table.sql`
- `_sql/insert_pricing_plans_data.sql`
**后端**
- `api/model/pricing_plans.js`
- `api/controller_admin/pricing_plans.js`
- `api/controller_front/config.js` (修改第90-136行)
**前端**
- `admin/src/api/system/pricing_plans_server.js`
- `admin/src/views/system/pricing_plans.vue`
### AI调用记录系统
**数据库**
- `_sql/create_ai_call_records_table.sql`
**后端**
- `api/model/ai_call_records.js`
- `api/services/ai_call_recorder.js`
- `api/services/ai_service.js` (完全重写)
- `api/controller_admin/ai_call_records.js`
**前端**
- `admin/src/api/system/ai_call_records_server.js`
- `admin/src/views/system/ai_call_records.vue`
### 公共配置
- `admin/src/router/component-map.js` (新增两个组件映射)
- `config/config.js` (AI配置项)
- `.env` (环境变量)
### 文档
- `docs/ai_service_config.md` (AI服务配置说明)
- `docs/implementation_summary.md` (本文档)
---
## 七、维护建议
### 日常维护
1. **监控Token使用量**
- 定期查看AI调用记录统计
- 关注异常高额调用
- 优化高频调用场景
2. **价格套餐调整**
- 根据市场情况调整价格
- 通过后台界面快速上下架套餐
- 使用排序功能控制显示顺序
3. **数据清理**
- 定期归档或删除历史AI调用记录
- 清理已删除的套餐数据(物理删除)
### 性能优化
1. **数据库索引**
- AI调用记录表按需添加索引user_id, sn_code, create_time
- 定期分析查询性能
2. **缓存策略**
- 考虑对前端API `/config/pricing-plans` 添加缓存
- 缓存有效期建议5-10分钟
3. **日志归档**
- 建立AI调用记录的归档机制
- 超过一定时间的数据转移到历史表
---
**文档版本**: 1.0
**最后更新**: 2025-12-27
**维护者**: 开发团队