feat: 智能排班人员预测系统

- 新增人员预测tab,基于多维度产能指标生成招聘/优化建议
- 规则引擎:7条优化规则 + 7条招聘规则 + 4步决策逻辑
- 动态标准值:基于全部门店中位数计算,替代硬编码阈值
- 互斥逻辑:有优化信号时抑制所有招聘信号
- 占比超配时不触发招聘信号(R9/R11/R12/R13)
- 离职缺口需出勤不足才触发招聘(R10)
- 管理岗满编时不触发出勤不足招聘(R9)
- 前端可折叠规则引擎面板,展示全部规则和触发条件
- 产能指标对比展示实际值 vs 中位数标准
This commit is contained in:
freedakgmail
2026-07-29 23:41:56 +08:00
parent 80d67ccb73
commit 75a483bfdb
17 changed files with 3497 additions and 3 deletions
+2
View File
@@ -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)
+64
View File
@@ -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