feat: 智能排班人员预测系统
- 新增人员预测tab,基于多维度产能指标生成招聘/优化建议 - 规则引擎:7条优化规则 + 7条招聘规则 + 4步决策逻辑 - 动态标准值:基于全部门店中位数计算,替代硬编码阈值 - 互斥逻辑:有优化信号时抑制所有招聘信号 - 占比超配时不触发招聘信号(R9/R11/R12/R13) - 离职缺口需出勤不足才触发招聘(R10) - 管理岗满编时不触发出勤不足招聘(R9) - 前端可折叠规则引擎面板,展示全部规则和触发条件 - 产能指标对比展示实际值 vs 中位数标准
This commit is contained in:
@@ -8,6 +8,7 @@ import dataRoutes from './routes/data.js'
|
||||
import taskRoutes from './routes/tasks.js'
|
||||
import costAnalysisRoutes from './routes/cost-analysis.js'
|
||||
import storeExpenseRoutes from './routes/store-expense.js'
|
||||
import smartSchedulingRoutes from './routes/smart-scheduling.js'
|
||||
|
||||
const app = express()
|
||||
const PORT = parseInt(process.env.PORT || '3333')
|
||||
@@ -35,6 +36,7 @@ app.use('/api', dataRoutes)
|
||||
app.use('/api/tasks', taskRoutes)
|
||||
app.use('/api/cost-analysis', costAnalysisRoutes)
|
||||
app.use('/api/store-expense', storeExpenseRoutes)
|
||||
app.use('/api/smart-scheduling', smartSchedulingRoutes)
|
||||
|
||||
app.use(notFoundHandler)
|
||||
app.use(errorHandler)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
const AI_API_KEY = process.env.ZHIPU_API_KEY || process.env.DASHSCOPE_API_KEY || ''
|
||||
const AI_API_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
|
||||
|
||||
interface AIMessage {
|
||||
role: 'system' | 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
export async function callAI(messages: AIMessage[], temperature = 0.3): Promise<string> {
|
||||
const resp = await fetch(AI_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${AI_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'qwen-plus',
|
||||
messages,
|
||||
temperature,
|
||||
max_tokens: 4096,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text()
|
||||
throw new Error(`AI API error ${resp.status}: ${errText}`)
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
return data.choices?.[0]?.message?.content || ''
|
||||
}
|
||||
|
||||
export async function callAIJSON(messages: AIMessage[], temperature = 0.3): Promise<any> {
|
||||
const raw = await callAI(messages, temperature)
|
||||
console.log('[AI] raw response length:', raw.length)
|
||||
// 提取JSON块(兼容```json包裹)
|
||||
let jsonStr = raw.trim()
|
||||
const codeBlockMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/)
|
||||
if (codeBlockMatch) {
|
||||
jsonStr = codeBlockMatch[1].trim()
|
||||
} else {
|
||||
// 找第一个[或{到最后一个]或}
|
||||
const firstBracket = raw.search(/[[{]/)
|
||||
if (firstBracket >= 0) {
|
||||
const lastBracket = Math.max(raw.lastIndexOf(']'), raw.lastIndexOf('}'))
|
||||
if (lastBracket > firstBracket) {
|
||||
jsonStr = raw.substring(firstBracket, lastBracket + 1).trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(jsonStr)
|
||||
} catch (e) {
|
||||
// AI可能返回多个独立JSON对象(非数组格式),尝试包装成数组
|
||||
try {
|
||||
// 将相邻的}{替换为},{
|
||||
const arrayStr = '[' + jsonStr.replace(/\}\s*\{/g, '},{') + ']'
|
||||
return JSON.parse(arrayStr)
|
||||
} catch (e2) {
|
||||
console.error('[AI] JSON parse failed. Raw (500 chars):', raw.substring(0, 500))
|
||||
throw e2
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user