Initial commit: HR Position System
- Database schema with MySQL support - LLM API integration (Gemini 2.5 Flash, DeepSeek, OpenAI) - Error handling with copyable error messages - CORS fix for API calls - Complete setup documentation 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
82
.gitignore
vendored
Normal file
82
.gitignore
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
|
||||
# Node modules (if using any frontend build tools)
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
backup/
|
||||
315
CORS_FIX_GUIDE.md
Normal file
315
CORS_FIX_GUIDE.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# CORS 錯誤修正指南
|
||||
|
||||
## 🔴 問題說明
|
||||
|
||||
您遇到的錯誤有兩個:
|
||||
|
||||
### 1. CORS (跨域資源共享) 錯誤
|
||||
```
|
||||
Access to fetch at 'https://api.anthropic.com/v1/messages' from origin 'http://127.0.0.1:5000'
|
||||
has been blocked by CORS policy: Response to preflight request doesn't pass access control check:
|
||||
No 'Access-Control-Allow-Origin' header is present on the requested resource.
|
||||
```
|
||||
|
||||
**原因**: 前端代碼直接從瀏覽器調用 Claude API,但 Claude API 不允許來自瀏覽器的直接請求(出於安全考量)。
|
||||
|
||||
### 2. Storage 訪問錯誤
|
||||
```
|
||||
Uncaught (in promise) Error: Access to storage is not allowed from this context.
|
||||
```
|
||||
|
||||
**原因**: 瀏覽器的本地存儲權限問題,通常在使用 `file://` 協議或某些安全限制下出現。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解決方案
|
||||
|
||||
### 方案 1: 修改 index.html 中的 callClaudeAPI 函數(推薦)
|
||||
|
||||
**步驟 1**: 打開 [index.html](./index.html)
|
||||
|
||||
**步驟 2**: 找到第 1264 行的 `callClaudeAPI` 函數
|
||||
|
||||
**步驟 3**: 將整個函數替換為以下代碼:
|
||||
|
||||
```javascript
|
||||
// ==================== AI Generation Functions ====================
|
||||
async function callClaudeAPI(prompt, api = 'gemini') {
|
||||
try {
|
||||
// 調用後端 Flask API,避免 CORS 錯誤
|
||||
const response = await fetch("/api/llm/generate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api: api, // 使用指定的 LLM API (gemini, deepseek, openai)
|
||||
prompt: prompt,
|
||||
max_tokens: 2000
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'API 調用失敗');
|
||||
}
|
||||
|
||||
// 解析返回的文字為 JSON
|
||||
let responseText = data.text;
|
||||
responseText = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
|
||||
return JSON.parse(responseText);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error calling LLM API:", error);
|
||||
|
||||
// 顯示友好的錯誤訊息
|
||||
alert(`AI 生成錯誤: ${error.message}\n\n請確保:\n1. Flask 後端已啟動\n2. 已在 .env 文件中配置 LLM API Key\n3. 網路連線正常`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**步驟 4**: 保存文件
|
||||
|
||||
**步驟 5**: 確保使用更新版的 Flask 後端
|
||||
|
||||
```bash
|
||||
# 使用包含 LLM 端點的版本
|
||||
python app_updated.py
|
||||
```
|
||||
|
||||
**步驟 6**: 重新載入瀏覽器頁面
|
||||
|
||||
---
|
||||
|
||||
### 方案 2: 使用已修正的文件(快速方案)
|
||||
|
||||
我已經創建了包含完整修正的文件,您可以選擇以下方式之一:
|
||||
|
||||
#### 選項 A: 手動修改(最安全)
|
||||
|
||||
1. 打開 `index.html`
|
||||
2. 搜尋 `async function callClaudeAPI(prompt)`
|
||||
3. 用上面方案 1 的代碼替換
|
||||
|
||||
#### 選項 B: 使用腳本自動修正
|
||||
|
||||
參考 [fix_cors.js](./fix_cors.js) 文件中的完整說明。
|
||||
|
||||
---
|
||||
|
||||
## 📋 完整修正步驟
|
||||
|
||||
### 1. 確認後端已啟動並包含 LLM 端點
|
||||
|
||||
```bash
|
||||
# 停止當前運行的 Flask(如果有)
|
||||
# 按 Ctrl+C
|
||||
|
||||
# 啟動更新版後端
|
||||
python app_updated.py
|
||||
```
|
||||
|
||||
您應該看到:
|
||||
```
|
||||
✓ LLM 功能已啟用
|
||||
已配置的 API: gemini, deepseek, openai
|
||||
```
|
||||
|
||||
### 2. 配置 LLM API Key
|
||||
|
||||
編輯 [.env](./.env) 文件,添加至少一個 API Key:
|
||||
|
||||
```env
|
||||
# 至少配置其中一個
|
||||
GEMINI_API_KEY=your_actual_gemini_api_key_here
|
||||
DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here
|
||||
OPENAI_API_KEY=your_actual_openai_api_key_here
|
||||
```
|
||||
|
||||
**獲取 API Key:**
|
||||
- **Gemini**: https://makersuite.google.com/app/apikey
|
||||
- **DeepSeek**: https://www.deepseek.com/
|
||||
- **OpenAI**: https://platform.openai.com/api-keys
|
||||
|
||||
### 3. 測試 LLM API 連線
|
||||
|
||||
訪問測試頁面:http://localhost:5000/api-test
|
||||
|
||||
點擊「測試連線」按鈕,確保至少一個 API 顯示「✓ 連線成功」。
|
||||
|
||||
### 4. 修改 index.html
|
||||
|
||||
按照方案 1 的步驟修改 `callClaudeAPI` 函數。
|
||||
|
||||
### 5. 重新載入頁面
|
||||
|
||||
在瀏覽器中按 `Ctrl+F5` 強制重新載入頁面(清除緩存)。
|
||||
|
||||
### 6. 測試 AI 功能
|
||||
|
||||
1. 在主頁面點擊「新增崗位」
|
||||
2. 填寫部分欄位(例如崗位名稱)
|
||||
3. 點擊「✨ I'm feeling lucky」按鈕
|
||||
4. 應該會成功生成內容,不再出現 CORS 錯誤
|
||||
|
||||
---
|
||||
|
||||
## 🔍 驗證修正是否成功
|
||||
|
||||
打開瀏覽器的開發者工具(F12),切換到 Console 標籤:
|
||||
|
||||
### ✅ 成功的表現:
|
||||
- 沒有 CORS 錯誤
|
||||
- 看到 `POST http://localhost:5000/api/llm/generate` 請求成功 (200 OK)
|
||||
- AI 自動填充正常工作
|
||||
|
||||
### ❌ 仍有問題:
|
||||
|
||||
#### 如果看到 `403 LLM 功能未啟用`
|
||||
**原因**: 沒有使用 `app_updated.py`
|
||||
**解決**:
|
||||
```bash
|
||||
python app_updated.py
|
||||
```
|
||||
|
||||
#### 如果看到 `API Key 未設定`
|
||||
**原因**: .env 文件中沒有配置 API Key
|
||||
**解決**: 編輯 `.env` 文件,添加有效的 API Key
|
||||
|
||||
#### 如果看到 `連線逾時`
|
||||
**原因**: 網路連線問題或 API 伺服器問題
|
||||
**解決**:
|
||||
1. 檢查網路連線
|
||||
2. 嘗試使用不同的 LLM API(修改 `callClaudeAPI(prompt, 'deepseek')`)
|
||||
|
||||
#### 如果看到 `API Key 無效`
|
||||
**原因**: API Key 錯誤或已過期
|
||||
**解決**: 重新獲取有效的 API Key
|
||||
|
||||
---
|
||||
|
||||
## 🎯 架構變更說明
|
||||
|
||||
### 修正前(錯誤):
|
||||
```
|
||||
瀏覽器 → 直接調用 → Claude API (❌ CORS 錯誤)
|
||||
```
|
||||
|
||||
### 修正後(正確):
|
||||
```
|
||||
瀏覽器 → Flask 後端 → Gemini/DeepSeek/OpenAI API (✅ 正常)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 為什麼這樣修改
|
||||
|
||||
### 1. 安全性
|
||||
- **API Key 保護**: API Key 只存儲在服務器端(.env 文件),不暴露給前端
|
||||
- **防止濫用**: 用戶無法看到或竊取 API Key
|
||||
|
||||
### 2. CORS 問題
|
||||
- **同源請求**: 前端只調用同域的 Flask API(http://localhost:5000)
|
||||
- **服務器端請求**: Flask 服務器調用外部 API,不受 CORS 限制
|
||||
|
||||
### 3. 靈活性
|
||||
- **多 API 支持**: 可以輕鬆切換不同的 LLM API
|
||||
- **統一接口**: 前端代碼無需關心使用哪個 LLM API
|
||||
|
||||
---
|
||||
|
||||
## 📝 其他建議
|
||||
|
||||
### 1. 添加錯誤處理 UI
|
||||
|
||||
在 `<head>` 標籤中添加錯誤處理腳本:
|
||||
|
||||
```html
|
||||
<script src="error_handler.js"></script>
|
||||
```
|
||||
|
||||
### 2. 選擇 LLM API
|
||||
|
||||
如果想使用特定的 LLM API,可以修改調用:
|
||||
|
||||
```javascript
|
||||
// 使用 Gemini(默認)
|
||||
const result = await callClaudeAPI(prompt, 'gemini');
|
||||
|
||||
// 使用 DeepSeek
|
||||
const result = await callClaudeAPI(prompt, 'deepseek');
|
||||
|
||||
// 使用 OpenAI
|
||||
const result = await callClaudeAPI(prompt, 'openai');
|
||||
```
|
||||
|
||||
### 3. 添加 API 選擇器
|
||||
|
||||
可以在前端添加一個下拉選單,讓用戶選擇使用哪個 LLM API:
|
||||
|
||||
```html
|
||||
<select id="llm-selector">
|
||||
<option value="gemini">Google Gemini</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
然後在調用時使用:
|
||||
```javascript
|
||||
const api = document.getElementById('llm-selector').value;
|
||||
const result = await callClaudeAPI(prompt, api);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 仍然遇到問題?
|
||||
|
||||
### 檢查清單
|
||||
|
||||
- [ ] Flask 後端是否正在運行?
|
||||
- [ ] 是否使用 `app_updated.py` 而不是 `app.py`?
|
||||
- [ ] .env 文件中是否配置了至少一個 API Key?
|
||||
- [ ] API Key 是否有效?(可以在 http://localhost:5000/api-test 測試)
|
||||
- [ ] 是否已修改 index.html 中的 `callClaudeAPI` 函數?
|
||||
- [ ] 瀏覽器是否已重新載入頁面(Ctrl+F5)?
|
||||
- [ ] 瀏覽器控制台是否還有其他錯誤?
|
||||
|
||||
### 調試步驟
|
||||
|
||||
1. **測試後端 API**:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/llm/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"api":"gemini","prompt":"測試","max_tokens":100}'
|
||||
```
|
||||
|
||||
2. **檢查 Flask 日誌**: 查看終端中 Flask 的輸出
|
||||
|
||||
3. **瀏覽器控制台**: 查看詳細的錯誤訊息
|
||||
|
||||
4. **測試頁面**: 訪問 http://localhost:5000/api-test 確認 API 配置
|
||||
|
||||
---
|
||||
|
||||
## 📚 相關文件
|
||||
|
||||
- [SETUP.md](./SETUP.md) - 完整安裝指南
|
||||
- [fix_cors.js](./fix_cors.js) - 詳細修正代碼
|
||||
- [error_handler.js](./error_handler.js) - 全局錯誤處理
|
||||
- [app_updated.py](./app_updated.py) - 包含 LLM 端點的 Flask 後端
|
||||
|
||||
---
|
||||
|
||||
**文件版本**: 1.0
|
||||
**最後更新**: 2024-12-04
|
||||
**問題類型**: CORS 跨域請求錯誤
|
||||
**解決狀態**: ✅ 已提供完整解決方案
|
||||
236
GEMINI_API_FIX.md
Normal file
236
GEMINI_API_FIX.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Gemini API Referrer 錯誤解決方案
|
||||
|
||||
## 🔴 錯誤訊息
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": 403,
|
||||
"message": "Requests from referer \u003cempty\u003e are blocked.",
|
||||
"status": "PERMISSION_DENIED",
|
||||
"details": [
|
||||
{
|
||||
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
||||
"reason": "API_KEY_HTTP_REFERRER_BLOCKED"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 問題原因
|
||||
|
||||
Gemini API Key 有 **HTTP Referrer 限制**,這是 Google 的安全機制。當從服務器端調用時,HTTP Referrer 為空,導致請求被阻擋。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解決方案
|
||||
|
||||
### 方案 1: 修改 Gemini API Key 設定(推薦)
|
||||
|
||||
1. **訪問 Google AI Studio**
|
||||
https://makersuite.google.com/app/apikey
|
||||
|
||||
2. **找到您的 API Key**
|
||||
|
||||
3. **點擊「Edit API Key」或創建新的 API Key**
|
||||
|
||||
4. **設定 Application restrictions**
|
||||
- 選擇 **"None"** 或 **"IP addresses"**
|
||||
- **不要**選擇 "HTTP referrers (websites)"
|
||||
|
||||
5. **保存設定**
|
||||
|
||||
6. **更新 .env 文件**
|
||||
```env
|
||||
GEMINI_API_KEY=your_new_unrestricted_api_key
|
||||
```
|
||||
|
||||
7. **重啟 Flask 服務器**
|
||||
```bash
|
||||
# 按 Ctrl+C 停止服務器
|
||||
python start_server.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案 2: 使用其他 LLM API(臨時解決)
|
||||
|
||||
如果您有其他 LLM API Key,可以暫時使用:
|
||||
|
||||
#### 選項 A: 使用 DeepSeek
|
||||
|
||||
1. **獲取 DeepSeek API Key**
|
||||
https://www.deepseek.com/
|
||||
|
||||
2. **添加到 .env**
|
||||
```env
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key
|
||||
```
|
||||
|
||||
3. **修改默認 API**
|
||||
編輯 `index.html`,找到 `callClaudeAPI` 調用,改為:
|
||||
```javascript
|
||||
const result = await callClaudeAPI(prompt, 'deepseek');
|
||||
```
|
||||
|
||||
#### 選項 B: 使用 OpenAI
|
||||
|
||||
1. **獲取 OpenAI API Key**
|
||||
https://platform.openai.com/api-keys
|
||||
|
||||
2. **添加到 .env**
|
||||
```env
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
3. **修改默認 API**
|
||||
編輯 `index.html`,找到 `callClaudeAPI` 調用,改為:
|
||||
```javascript
|
||||
const result = await callClaudeAPI(prompt, 'openai');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案 3: 創建 API 選擇器(最佳長期方案)
|
||||
|
||||
讓用戶在前端選擇要使用的 LLM API。
|
||||
|
||||
我可以幫您添加一個下拉選單,讓用戶可以在 Gemini、DeepSeek 和 OpenAI 之間切換。
|
||||
|
||||
---
|
||||
|
||||
## 🔍 驗證修正
|
||||
|
||||
### 測試 API 連線
|
||||
|
||||
訪問測試頁面:http://127.0.0.1:5000/api-test
|
||||
|
||||
點擊每個 API 的「🧪 測試連線」按鈕,查看哪些 API 可用。
|
||||
|
||||
### 成功的表現
|
||||
|
||||
- ✅ API 測試顯示「✓ 連線成功」
|
||||
- ✅ 瀏覽器不再顯示 403 錯誤
|
||||
- ✅ AI 自動填充功能正常工作
|
||||
|
||||
---
|
||||
|
||||
## 📝 詳細錯誤訊息說明
|
||||
|
||||
您在截圖中看到的錯誤:
|
||||
|
||||
```
|
||||
"reason": "API_KEY_HTTP_REFERRER_BLOCKED"
|
||||
```
|
||||
|
||||
這表示:
|
||||
- Gemini API Key 設定了 HTTP Referrer 限制
|
||||
- 服務器端請求沒有 Referrer,被 Google 阻擋
|
||||
- 需要移除 Referrer 限制或使用其他 API
|
||||
|
||||
---
|
||||
|
||||
## 🎯 推薦操作順序
|
||||
|
||||
### 立即操作(5 分鐘)
|
||||
|
||||
1. **使用 DeepSeek 或 OpenAI**(臨時解決)
|
||||
```bash
|
||||
# 編輯 .env 添加其他 API Key
|
||||
# 重啟服務器
|
||||
python start_server.py
|
||||
```
|
||||
|
||||
2. **測試頁面重新載入**
|
||||
- 按 Ctrl+F5 刷新
|
||||
- 測試 AI 功能
|
||||
|
||||
### 長期解決(10 分鐘)
|
||||
|
||||
1. **修改 Gemini API Key 設定**
|
||||
- 訪問 Google AI Studio
|
||||
- 移除 HTTP Referrer 限制
|
||||
- 創建新的無限制 API Key
|
||||
|
||||
2. **更新配置**
|
||||
- 更新 .env 文件
|
||||
- 重啟服務器
|
||||
|
||||
3. **全面測試**
|
||||
- 測試所有 API
|
||||
- 確保都能正常工作
|
||||
|
||||
---
|
||||
|
||||
## 💡 補充說明
|
||||
|
||||
### 為什麼服務器端請求沒有 Referrer?
|
||||
|
||||
當從 Python Flask 後端調用 API 時:
|
||||
- HTTP 請求是由服務器發出的
|
||||
- 沒有瀏覽器上下文
|
||||
- Referer header 為空或不存在
|
||||
- Google 的安全機制會阻擋這類請求
|
||||
|
||||
### 如何避免這個問題?
|
||||
|
||||
1. **使用無限制的 API Key**(推薦)
|
||||
2. **使用 IP 地址限制**而非 Referrer 限制
|
||||
3. **使用服務帳戶**(企業方案)
|
||||
|
||||
---
|
||||
|
||||
## 🆘 仍然有問題?
|
||||
|
||||
### 如果修改 API Key 後還是不行
|
||||
|
||||
1. **檢查 API Key 是否生效**
|
||||
- 等待 1-2 分鐘
|
||||
- Google 的設定更新需要時間
|
||||
|
||||
2. **確認 .env 文件正確**
|
||||
```bash
|
||||
# 查看 .env 內容
|
||||
type .env
|
||||
```
|
||||
|
||||
3. **重啟服務器**
|
||||
```bash
|
||||
# 完全停止後重新啟動
|
||||
python start_server.py
|
||||
```
|
||||
|
||||
4. **清除瀏覽器緩存**
|
||||
- 按 Ctrl+Shift+Delete
|
||||
- 清除緩存和 Cookie
|
||||
- 重新載入頁面
|
||||
|
||||
---
|
||||
|
||||
## 📊 API 對比
|
||||
|
||||
| API | 優點 | 缺點 | 推薦度 |
|
||||
|-----|------|------|--------|
|
||||
| **Gemini** | 免費額度高,速度快 | Referrer 限制問題 | ⭐⭐⭐ |
|
||||
| **DeepSeek** | 便宜,中文支持好 | 需要付費 | ⭐⭐⭐⭐ |
|
||||
| **OpenAI** | 穩定,功能強大 | 價格較高 | ⭐⭐⭐⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完成檢查清單
|
||||
|
||||
修正完成後,請檢查:
|
||||
|
||||
- [ ] 至少一個 LLM API 測試成功
|
||||
- [ ] AI 自動填充功能正常
|
||||
- [ ] 沒有 403 錯誤
|
||||
- [ ] 錯誤訊息可以完整顯示和複製
|
||||
- [ ] 瀏覽器控制台沒有錯誤
|
||||
|
||||
---
|
||||
|
||||
**文件版本**: 1.0
|
||||
**最後更新**: 2024-12-04
|
||||
**問題類型**: Gemini API HTTP Referrer 限制
|
||||
**解決狀態**: ✅ 已提供完整解決方案
|
||||
844
SDD.md
Normal file
844
SDD.md
Normal file
@@ -0,0 +1,844 @@
|
||||
# HR 基礎資料維護系統 - 軟體設計文件 (SDD)
|
||||
|
||||
**文件版本**:1.0
|
||||
**建立日期**:2024-12-03
|
||||
**文件狀態**:Released
|
||||
|
||||
---
|
||||
|
||||
## 1. 文件概述
|
||||
|
||||
### 1.1 目的
|
||||
|
||||
本文件為 HR 基礎資料維護系統之軟體設計文件 (Software Design Document),詳細描述系統架構、模組設計、資料結構、介面規格與 AI 整合功能,供開發人員、測試人員及維護人員參考。
|
||||
|
||||
### 1.2 範圍
|
||||
|
||||
本系統涵蓋以下三大功能模組:
|
||||
|
||||
| 模組 | 功能說明 |
|
||||
|------|----------|
|
||||
| 崗位基礎資料 | 崗位主檔維護,含基礎資料與招聘要求 |
|
||||
| 職務基礎資料 | 職務類別與屬性設定維護 |
|
||||
| 崗位描述 | 職責描述、崗位要求與任職條件維護 |
|
||||
|
||||
### 1.3 參考文件
|
||||
|
||||
- 用戶需求規格書 (URD)
|
||||
- 系統功能規格書 (FSD)
|
||||
- API 設計規範
|
||||
|
||||
### 1.4 術語定義
|
||||
|
||||
| 術語 | 定義 |
|
||||
|------|------|
|
||||
| 崗位 (Position) | 組織架構中的職位單位,具有編號、級別、編制人數等屬性 |
|
||||
| 職務 (Job Title) | 職務類別分類,如管理職、技術職、業務職等 |
|
||||
| 崗位描述 (Job Description) | 詳細描述崗位職責、要求與任職條件的文件 |
|
||||
| AI 自動填充 | 利用大型語言模型自動生成表單欄位內容的功能 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 系統架構
|
||||
|
||||
### 2.1 整體架構圖
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 使用者介面層 (UI Layer) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 崗位基礎資料 │ │ 職務基礎資料 │ │ 崗位描述 │ │
|
||||
│ │ 模組 │ │ 模組 │ │ 模組 │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ AI 自動填充服務 (Claude API) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 應用服務層 (Application Layer) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Flask RESTful API │ │
|
||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ Position │ │ Job │ │ Reference │ │ │
|
||||
│ │ │ API │ │ API │ │ API │ │ │
|
||||
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 資料存取層 (Data Layer) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ In-Memory Database / Future DB │ │
|
||||
│ │ (positions_db, jobs_db, job_desc_db) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 技術堆疊
|
||||
|
||||
| 層級 | 技術選型 | 說明 |
|
||||
|------|----------|------|
|
||||
| 前端 | HTML5 + CSS3 + JavaScript | 純前端實作,無框架依賴 |
|
||||
| 樣式 | Custom CSS + Google Fonts | Noto Sans TC 字型、CSS Variables |
|
||||
| 後端 | Python Flask | RESTful API 服務 |
|
||||
| AI 服務 | Claude API (Anthropic) | 智能表單填充 |
|
||||
| 資料庫 | In-Memory (Dict) | 可擴展至 MySQL/PostgreSQL |
|
||||
|
||||
### 2.3 部署架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 使用者瀏覽器 │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ index.html (前端應用) │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐
|
||||
│ Flask Server │ │ Claude API │
|
||||
│ localhost:5000 │ │ api.anthropic │
|
||||
└───────────────────┘ └───────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 模組設計
|
||||
|
||||
### 3.1 崗位基礎資料模組
|
||||
|
||||
#### 3.1.1 模組概述
|
||||
|
||||
提供崗位主檔的 CRUD 操作,分為「基礎資料」與「招聘要求資料」兩個頁籤。
|
||||
|
||||
#### 3.1.2 基礎資料頁籤欄位規格
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 必填 | 說明 |
|
||||
|----------|--------|----------|------|------|
|
||||
| 崗位編號 | positionCode | String(20) | ✓ | 唯一識別碼,格式:XXX-NNN |
|
||||
| 崗位名稱 | positionName | String(100) | ✓ | 崗位中文名稱 |
|
||||
| 崗位類別 | positionCategory | Enum | | 01/02/03/04 |
|
||||
| 崗位類別名稱 | positionCategoryName | String(50) | | 自動帶出,唯讀 |
|
||||
| 崗位性質 | positionNature | Enum | | FT/PT/CT/IN |
|
||||
| 崗位性質名稱 | positionNatureName | String(50) | | 自動帶出,唯讀 |
|
||||
| 編制人數 | headcount | Integer | | 0-9999 |
|
||||
| 崗位級別 | positionLevel | Enum | | L1-L7 |
|
||||
| 生效日期 | effectiveDate | Date | | 預設 2001-01-01 |
|
||||
| 崗位描述 | positionDesc | Text | | 多行文字 |
|
||||
| 崗位備注 | positionRemark | Text | | 多行文字 |
|
||||
|
||||
#### 3.1.3 招聘要求資料頁籤欄位規格
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||
|----------|--------|----------|------|
|
||||
| 最低學歷 | minEducation | Enum | HS/JC/BA/MA/PHD |
|
||||
| 要求性別 | requiredGender | Enum | 空值=不限, M=男, F=女 |
|
||||
| 薪酬范圍 | salaryRange | Enum | A/B/C/D/E/N |
|
||||
| 工作經驗 | workExperience | Enum | 0/1/3/5/10 (年) |
|
||||
| 最小年齡 | minAge | Integer | 18-65 |
|
||||
| 最大年齡 | maxAge | Integer | 18-65 |
|
||||
| 工作性質 | jobType | Enum | FT/PT/CT/DP |
|
||||
| 招聘職位 | recruitPosition | Enum | ENG/MGR/AST/OP/SAL |
|
||||
| 職位名稱 | jobTitle | String(100) | |
|
||||
| 職位描述 | jobDesc | Text | |
|
||||
| 崗位要求 | positionReq | Text | |
|
||||
| 職稱要求 | titleReq | Enum | NONE/CERT/LIC |
|
||||
| 專業要求 | majorReq | String(200) | 多選,逗號分隔 |
|
||||
| 技能要求 | skillReq | String(200) | |
|
||||
| 語言要求 | langReq | String(100) | |
|
||||
| 其他要求 | otherReq | String(200) | |
|
||||
| 上級崗位編號 | superiorPosition | String(20) | |
|
||||
| 備注說明 | recruitRemark | Text | |
|
||||
|
||||
#### 3.1.4 類別代碼對照表
|
||||
|
||||
**崗位類別 (positionCategory)**
|
||||
|
||||
| 代碼 | 名稱 |
|
||||
|------|------|
|
||||
| 01 | 技術職 |
|
||||
| 02 | 管理職 |
|
||||
| 03 | 業務職 |
|
||||
| 04 | 行政職 |
|
||||
|
||||
**崗位性質 (positionNature)**
|
||||
|
||||
| 代碼 | 名稱 |
|
||||
|------|------|
|
||||
| FT | 全職 |
|
||||
| PT | 兼職 |
|
||||
| CT | 約聘 |
|
||||
| IN | 實習 |
|
||||
|
||||
**崗位級別 (positionLevel)**
|
||||
|
||||
| 代碼 | 名稱 |
|
||||
|------|------|
|
||||
| L1 | 基層員工 |
|
||||
| L2 | 資深員工 |
|
||||
| L3 | 主管 |
|
||||
| L4 | 經理 |
|
||||
| L5 | 總監 |
|
||||
| L6 | 副總 |
|
||||
| L7 | 總經理 |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 職務基礎資料模組
|
||||
|
||||
#### 3.2.1 模組概述
|
||||
|
||||
提供職務類別主檔的維護功能,包含職務屬性設定。
|
||||
|
||||
#### 3.2.2 欄位規格
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 必填 | 說明 |
|
||||
|----------|--------|----------|------|------|
|
||||
| 職務類別編號 | jobCategoryCode | Enum | ✓ | MGR/TECH/SALE/ADMIN/RD/PROD |
|
||||
| 職務類別名稱 | jobCategoryName | String(50) | | 自動帶出,唯讀 |
|
||||
| 職務編號 | jobCode | String(20) | ✓ | 唯一識別碼 |
|
||||
| 職務名稱 | jobName | String(100) | ✓ | 職務中文名稱 |
|
||||
| 職務英文 | jobNameEn | String(100) | | 職務英文名稱 |
|
||||
| 生效日期 | jobEffectiveDate | Date | | |
|
||||
| 編制人數 | jobHeadcount | Integer | | 0-9999 |
|
||||
| 排列順序 | jobSortOrder | Integer | | 顯示排序 |
|
||||
| 備注說明 | jobRemark | Text | | |
|
||||
| 職務層級 | jobLevel | String(50) | | 可設為 *保密* |
|
||||
| 是否有全勤 | hasAttendanceBonus | Boolean | | Toggle 開關 |
|
||||
| 是否住房補貼 | hasHousingAllowance | Boolean | | Toggle 開關 |
|
||||
|
||||
#### 3.2.3 職務類別代碼對照表
|
||||
|
||||
| 代碼 | 名稱 |
|
||||
|------|------|
|
||||
| MGR | 管理職 |
|
||||
| TECH | 技術職 |
|
||||
| SALE | 業務職 |
|
||||
| ADMIN | 行政職 |
|
||||
| RD | 研發職 |
|
||||
| PROD | 生產職 |
|
||||
|
||||
---
|
||||
|
||||
### 3.3 崗位描述模組
|
||||
|
||||
#### 3.3.1 模組概述
|
||||
|
||||
提供完整的崗位描述書 (Job Description) 維護功能,包含崗位基本信息、職責描述與崗位要求三大區塊。
|
||||
|
||||
#### 3.3.2 頂部區域欄位
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||
|----------|--------|----------|------|
|
||||
| 工號 | jd_empNo | String(20) | 員工編號 |
|
||||
| 姓名 | jd_empName | String(50) | 自動帶出,唯讀 |
|
||||
| 崗位代碼 | jd_positionCode | String(20) | |
|
||||
| 版本更新日期 | jd_versionDate | Date | |
|
||||
|
||||
#### 3.3.3 崗位基本信息區塊
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||
|----------|--------|----------|------|
|
||||
| 崗位名稱 | jd_positionName | String(100) | |
|
||||
| 所屬部門 | jd_department | String(100) | |
|
||||
| 崗位生效日期 | jd_positionEffectiveDate | Date | |
|
||||
| 直接領導職務 | jd_directSupervisor | String(100) | |
|
||||
| 崗位職等&職務 | jd_positionGradeJob | String(100) | 選擇按鈕 |
|
||||
| 匯報對象職務 | jd_reportTo | String(100) | 選擇按鈕 |
|
||||
| 直接下級 | jd_directReports | String(200) | 格式:職位 x 人數 |
|
||||
| 任職地點 | jd_workLocation | Enum | HQ/TPE/TYC/KHH/SH/SZ |
|
||||
| 員工屬性 | jd_empAttribute | Enum | FT/CT/PT/IN/DP |
|
||||
|
||||
#### 3.3.4 職責描述區塊
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||
|----------|--------|----------|------|
|
||||
| 崗位設置目的 | jd_positionPurpose | String(500) | 單行文字 |
|
||||
| 主要崗位職責 | jd_mainResponsibilities | Text | 編號格式 1、2、3、... |
|
||||
|
||||
#### 3.3.5 崗位要求區塊
|
||||
|
||||
| 欄位名稱 | 欄位ID | 資料類型 | 說明 |
|
||||
|----------|--------|----------|------|
|
||||
| 教育程度 | jd_education | String(200) | |
|
||||
| 基本技能 | jd_basicSkills | Text | |
|
||||
| 專業知識 | jd_professionalKnowledge | Text | |
|
||||
| 工作經驗 | jd_workExperienceReq | Text | |
|
||||
| 其他 | jd_otherRequirements | Text | |
|
||||
|
||||
#### 3.3.6 任職地點代碼對照表
|
||||
|
||||
| 代碼 | 名稱 |
|
||||
|------|------|
|
||||
| HQ | 總部 |
|
||||
| TPE | 台北辦公室 |
|
||||
| TYC | 桃園廠區 |
|
||||
| KHH | 高雄廠區 |
|
||||
| SH | 上海辦公室 |
|
||||
| SZ | 深圳辦公室 |
|
||||
|
||||
#### 3.3.7 員工屬性代碼對照表
|
||||
|
||||
| 代碼 | 名稱 |
|
||||
|------|------|
|
||||
| FT | 正式員工 |
|
||||
| CT | 約聘人員 |
|
||||
| PT | 兼職人員 |
|
||||
| IN | 實習生 |
|
||||
| DP | 派遣人員 |
|
||||
|
||||
---
|
||||
|
||||
## 4. API 設計
|
||||
|
||||
### 4.1 API 端點總覽
|
||||
|
||||
#### 4.1.1 崗位資料 API
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/positions` | 獲取所有崗位(支援分頁、搜尋) |
|
||||
| GET | `/api/positions/{id}` | 獲取單一崗位 |
|
||||
| POST | `/api/positions` | 新增崗位 |
|
||||
| PUT | `/api/positions/{id}` | 更新崗位 |
|
||||
| DELETE | `/api/positions/{id}` | 刪除崗位 |
|
||||
| POST | `/api/positions/{id}/change-code` | 更改崗位編號 |
|
||||
|
||||
#### 4.1.2 職務資料 API
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/jobs` | 獲取所有職務(支援分頁、搜尋、類別篩選) |
|
||||
| GET | `/api/jobs/{id}` | 獲取單一職務 |
|
||||
| POST | `/api/jobs` | 新增職務 |
|
||||
| PUT | `/api/jobs/{id}` | 更新職務 |
|
||||
| DELETE | `/api/jobs/{id}` | 刪除職務 |
|
||||
| POST | `/api/jobs/{id}/change-code` | 更改職務編號 |
|
||||
|
||||
#### 4.1.3 參照資料 API
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/reference/categories` | 崗位類別選項 |
|
||||
| GET | `/api/reference/job-categories` | 職務類別選項 |
|
||||
| GET | `/api/reference/natures` | 崗位性質選項 |
|
||||
| GET | `/api/reference/education` | 學歷選項 |
|
||||
| GET | `/api/reference/majors` | 專業選項 |
|
||||
|
||||
### 4.2 API 請求/回應範例
|
||||
|
||||
#### 4.2.1 新增崗位
|
||||
|
||||
**Request**
|
||||
```http
|
||||
POST /api/positions
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"basicInfo": {
|
||||
"positionCode": "ENG-001",
|
||||
"positionName": "資深軟體工程師",
|
||||
"positionCategory": "01",
|
||||
"positionNature": "FT",
|
||||
"headcount": "5",
|
||||
"positionLevel": "L3",
|
||||
"effectiveDate": "2024-01-01",
|
||||
"positionDesc": "負責系統架構設計與核心模組開發"
|
||||
},
|
||||
"recruitInfo": {
|
||||
"minEducation": "BA",
|
||||
"salaryRange": "D",
|
||||
"workExperience": "5",
|
||||
"skillReq": "Python, JavaScript, SQL"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "崗位資料新增成功",
|
||||
"data": {
|
||||
"id": "ENG-001",
|
||||
"basicInfo": { ... },
|
||||
"recruitInfo": { ... },
|
||||
"createdAt": "2024-12-03T10:30:00",
|
||||
"updatedAt": "2024-12-03T10:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.2 查詢崗位列表
|
||||
|
||||
**Request**
|
||||
```http
|
||||
GET /api/positions?page=1&size=20&search=工程師
|
||||
```
|
||||
|
||||
**Response (200 OK)**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "ENG-001",
|
||||
"basicInfo": { ... },
|
||||
"recruitInfo": { ... }
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"total": 1,
|
||||
"totalPages": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 錯誤處理
|
||||
|
||||
| HTTP 狀態碼 | 錯誤類型 | 說明 |
|
||||
|-------------|----------|------|
|
||||
| 400 | Bad Request | 請求參數錯誤或缺少必填欄位 |
|
||||
| 404 | Not Found | 找不到指定資源 |
|
||||
| 409 | Conflict | 資源衝突(如編號已存在) |
|
||||
| 500 | Internal Server Error | 伺服器內部錯誤 |
|
||||
|
||||
**錯誤回應格式**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "錯誤訊息描述"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. AI 自動填充功能設計
|
||||
|
||||
### 5.1 功能概述
|
||||
|
||||
系統整合 Claude API,提供智能表單填充功能。用戶點擊「✨ I'm feeling lucky」按鈕後,AI 會根據已填寫的內容,自動補充尚未填寫的欄位。
|
||||
|
||||
### 5.2 填充邏輯
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ AI 填充流程 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 1. 用戶點擊「I'm feeling lucky」按鈕 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 2. 檢查表單欄位狀態 │
|
||||
│ ├── 收集已填寫欄位 (作為上下文) │
|
||||
│ └── 識別空白欄位 (待填充目標) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 3. 若無空白欄位 → 顯示「所有欄位都已填寫完成!」 │
|
||||
│ 若有空白欄位 → 繼續 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 4. 組裝 Prompt │
|
||||
│ ├── 包含已填寫資料作為參考 │
|
||||
│ ├── 指定只生成空白欄位 │
|
||||
│ └── 要求返回 JSON 格式 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 5. 呼叫 Claude API (claude-sonnet-4-20250514) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 6. 解析回應並填充空白欄位 │
|
||||
│ └── 已有內容的欄位保持不變 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 7. 顯示「✨ AI 已補充 N 個欄位!」 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 核心函數
|
||||
|
||||
#### 5.3.1 fillIfEmpty
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 只在欄位為空時填入值
|
||||
* @param {string} elementId - DOM 元素 ID
|
||||
* @param {string} value - 要填入的值
|
||||
* @returns {boolean} - 是否有填入值
|
||||
*/
|
||||
function fillIfEmpty(elementId, value) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (el && !el.value.trim() && value) {
|
||||
el.value = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3.2 getEmptyFields
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 獲取空白欄位列表
|
||||
* @param {string[]} fieldIds - 欄位 ID 陣列
|
||||
* @returns {string[]} - 空白欄位 ID 陣列
|
||||
*/
|
||||
function getEmptyFields(fieldIds) {
|
||||
return fieldIds.filter(id => !getFieldValue(id));
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 API 呼叫規格
|
||||
|
||||
**Endpoint**
|
||||
```
|
||||
POST https://api.anthropic.com/v1/messages
|
||||
```
|
||||
|
||||
**Request Body**
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"max_tokens": 2000,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Prompt 內容..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 Prompt 設計範例
|
||||
|
||||
```
|
||||
請為HR崗位管理系統生成崗位基礎資料。請用繁體中文回覆。
|
||||
|
||||
已填寫的資料(請參考這些內容來生成相關的資料):
|
||||
{
|
||||
"positionName": "資深前端工程師",
|
||||
"positionLevel": "L4"
|
||||
}
|
||||
|
||||
請「只生成」以下這些尚未填寫的欄位:positionCode, positionCategory, positionNature, headcount, positionDesc
|
||||
|
||||
欄位說明:
|
||||
- positionCode: 崗位編號(格式如 ENG-001)
|
||||
- positionCategory: 崗位類別代碼(01=技術職, 02=管理職...)
|
||||
...
|
||||
|
||||
請直接返回JSON格式,只包含需要生成的欄位:
|
||||
{
|
||||
"positionCode": "...",
|
||||
"positionCategory": "...",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 使用者介面設計
|
||||
|
||||
### 6.1 色彩規範
|
||||
|
||||
| 變數名稱 | 色碼 | 用途 |
|
||||
|----------|------|------|
|
||||
| --primary | #1a5276 | 主色(崗位模組) |
|
||||
| --primary-light | #2980b9 | 主色亮色 |
|
||||
| --primary-dark | #0e3a53 | 主色暗色 |
|
||||
| --accent | #e67e22 | 強調色(職務模組) |
|
||||
| --green | #27ae60 | 綠色(崗位描述模組) |
|
||||
| --success | #27ae60 | 成功狀態 |
|
||||
| --warning | #f39c12 | 警告狀態 |
|
||||
| --danger | #e74c3c | 錯誤/必填標示 |
|
||||
|
||||
### 6.2 模組主題色
|
||||
|
||||
| 模組 | Header 背景 | 按鈕樣式 |
|
||||
|------|-------------|----------|
|
||||
| 崗位基礎資料 | 藍色漸層 | primary |
|
||||
| 職務基礎資料 | 橘色漸層 | accent |
|
||||
| 崗位描述 | 綠色漸層 | green |
|
||||
|
||||
### 6.3 元件規格
|
||||
|
||||
#### 6.3.1 輸入框
|
||||
|
||||
```css
|
||||
input, select, textarea {
|
||||
padding: 10px 14px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.3.2 按鈕
|
||||
|
||||
| 類型 | Class | 用途 |
|
||||
|------|-------|------|
|
||||
| 主要按鈕 | .btn-primary | 保存操作 |
|
||||
| 次要按鈕 | .btn-secondary | 保存並新增 |
|
||||
| 取消按鈕 | .btn-cancel | 取消操作 |
|
||||
| AI 按鈕 | .ai-generate-btn | AI 自動填充 |
|
||||
|
||||
#### 6.3.3 Toggle 開關
|
||||
|
||||
```css
|
||||
.toggle-switch {
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 響應式斷點
|
||||
|
||||
| 斷點 | 寬度 | 佈局調整 |
|
||||
|------|------|----------|
|
||||
| Desktop | > 768px | 雙欄表單佈局 |
|
||||
| Mobile | ≤ 768px | 單欄表單佈局 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 資料結構
|
||||
|
||||
### 7.1 崗位資料 (Position)
|
||||
|
||||
```typescript
|
||||
interface Position {
|
||||
id: string; // 崗位編號 (PK)
|
||||
basicInfo: {
|
||||
positionCode: string; // 崗位編號
|
||||
positionName: string; // 崗位名稱
|
||||
positionCategory: string; // 崗位類別
|
||||
positionCategoryName: string;// 崗位類別名稱
|
||||
positionNature: string; // 崗位性質
|
||||
positionNatureName: string; // 崗位性質名稱
|
||||
headcount: string; // 編制人數
|
||||
positionLevel: string; // 崗位級別
|
||||
effectiveDate: string; // 生效日期
|
||||
positionDesc: string; // 崗位描述
|
||||
positionRemark: string; // 崗位備注
|
||||
};
|
||||
recruitInfo: {
|
||||
minEducation: string; // 最低學歷
|
||||
requiredGender: string; // 要求性別
|
||||
salaryRange: string; // 薪酬范圍
|
||||
workExperience: string; // 工作經驗
|
||||
minAge: string; // 最小年齡
|
||||
maxAge: string; // 最大年齡
|
||||
jobType: string; // 工作性質
|
||||
recruitPosition: string; // 招聘職位
|
||||
jobTitle: string; // 職位名稱
|
||||
jobDesc: string; // 職位描述
|
||||
positionReq: string; // 崗位要求
|
||||
titleReq: string; // 職稱要求
|
||||
majorReq: string; // 專業要求
|
||||
skillReq: string; // 技能要求
|
||||
langReq: string; // 語言要求
|
||||
otherReq: string; // 其他要求
|
||||
superiorPosition: string; // 上級崗位編號
|
||||
recruitRemark: string; // 備注說明
|
||||
};
|
||||
createdAt: string; // 建立時間
|
||||
updatedAt: string; // 更新時間
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 職務資料 (Job)
|
||||
|
||||
```typescript
|
||||
interface Job {
|
||||
id: string; // 職務編號 (PK)
|
||||
jobCategoryCode: string; // 職務類別編號
|
||||
jobCategoryName: string; // 職務類別名稱
|
||||
jobCode: string; // 職務編號
|
||||
jobName: string; // 職務名稱
|
||||
jobNameEn: string; // 職務英文名稱
|
||||
jobEffectiveDate: string; // 生效日期
|
||||
jobHeadcount: number; // 編制人數
|
||||
jobSortOrder: number; // 排列順序
|
||||
jobRemark: string; // 備注說明
|
||||
jobLevel: string; // 職務層級
|
||||
hasAttendanceBonus: boolean; // 是否有全勤
|
||||
hasHousingAllowance: boolean; // 是否住房補貼
|
||||
createdAt: string; // 建立時間
|
||||
updatedAt: string; // 更新時間
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 崗位描述資料 (JobDescription)
|
||||
|
||||
```typescript
|
||||
interface JobDescription {
|
||||
id: string; // ID (PK)
|
||||
basicInfo: {
|
||||
empNo: string; // 工號
|
||||
empName: string; // 姓名
|
||||
positionCode: string; // 崗位代碼
|
||||
versionDate: string; // 版本更新日期
|
||||
};
|
||||
positionInfo: {
|
||||
positionName: string; // 崗位名稱
|
||||
department: string; // 所屬部門
|
||||
positionEffectiveDate: string; // 崗位生效日期
|
||||
directSupervisor: string; // 直接領導職務
|
||||
positionGradeJob: string; // 崗位職等&職務
|
||||
reportTo: string; // 匯報對象職務
|
||||
directReports: string; // 直接下級
|
||||
workLocation: string; // 任職地點
|
||||
empAttribute: string; // 員工屬性
|
||||
};
|
||||
responsibilities: {
|
||||
positionPurpose: string; // 崗位設置目的
|
||||
mainResponsibilities: string;// 主要崗位職責
|
||||
};
|
||||
requirements: {
|
||||
education: string; // 教育程度
|
||||
basicSkills: string; // 基本技能
|
||||
professionalKnowledge: string; // 專業知識
|
||||
workExperienceReq: string; // 工作經驗
|
||||
otherRequirements: string; // 其他要求
|
||||
};
|
||||
createdAt: string; // 建立時間
|
||||
updatedAt: string; // 更新時間
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 快捷鍵設計
|
||||
|
||||
| 快捷鍵 | 功能 | 適用範圍 |
|
||||
|--------|------|----------|
|
||||
| `Ctrl + S` | 保存并退出 | 所有模組 |
|
||||
| `Ctrl + N` | 保存并新增 | 所有模組 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 檔案結構
|
||||
|
||||
```
|
||||
hr_position_form/
|
||||
├── index.html # 前端應用主檔
|
||||
├── app.py # Flask 後端 API
|
||||
├── requirements.txt # Python 依賴套件
|
||||
├── README.md # 使用說明文件
|
||||
└── SDD.md # 軟體設計文件(本文件)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 未來擴展建議
|
||||
|
||||
### 10.1 資料庫整合
|
||||
|
||||
將 In-Memory 儲存替換為正式資料庫:
|
||||
|
||||
```python
|
||||
# 建議使用 SQLAlchemy ORM
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://user:pass@host/db'
|
||||
db = SQLAlchemy(app)
|
||||
```
|
||||
|
||||
### 10.2 權限控制整合
|
||||
|
||||
```python
|
||||
# 建議整合 Azure AD
|
||||
from flask_azure_oauth import AzureOAuth
|
||||
|
||||
azure_oauth = AzureOAuth(app)
|
||||
|
||||
@app.route('/api/positions', methods=['POST'])
|
||||
@azure_oauth.require_auth
|
||||
def create_position():
|
||||
...
|
||||
```
|
||||
|
||||
### 10.3 審計日誌
|
||||
|
||||
```python
|
||||
# 記錄所有資料變更
|
||||
def log_audit(action, entity_type, entity_id, old_data, new_data, user_id):
|
||||
audit_log = {
|
||||
'action': action, # CREATE/UPDATE/DELETE
|
||||
'entity_type': entity_type,
|
||||
'entity_id': entity_id,
|
||||
'old_data': old_data,
|
||||
'new_data': new_data,
|
||||
'user_id': user_id,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
# 儲存至審計日誌表
|
||||
```
|
||||
|
||||
### 10.4 多語言支援
|
||||
|
||||
```javascript
|
||||
// i18n 國際化
|
||||
const i18n = {
|
||||
'zh-TW': {
|
||||
'position.code': '崗位編號',
|
||||
'position.name': '崗位名稱',
|
||||
...
|
||||
},
|
||||
'en-US': {
|
||||
'position.code': 'Position Code',
|
||||
'position.name': 'Position Name',
|
||||
...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 版本歷史
|
||||
|
||||
| 版本 | 日期 | 作者 | 變更說明 |
|
||||
|------|------|------|----------|
|
||||
| 1.0 | 2024-12-03 | System | 初始版本,包含三大模組設計與 AI 功能 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 附錄
|
||||
|
||||
### 附錄 A:JSON 資料預覽格式
|
||||
|
||||
系統提供即時 JSON 預覽功能,格式如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "崗位基礎資料",
|
||||
"basicInfo": {
|
||||
"positionCode": "ENG-001",
|
||||
"positionName": "資深軟體工程師",
|
||||
"positionCategory": "01",
|
||||
"positionCategoryName": "技術職",
|
||||
"positionNature": "FT",
|
||||
"positionNatureName": "全職",
|
||||
"headcount": "5",
|
||||
"positionLevel": "L3"
|
||||
},
|
||||
"recruitInfo": {
|
||||
"minEducation": "BA",
|
||||
"salaryRange": "D",
|
||||
"workExperience": "5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 附錄 B:UI 截圖參考
|
||||
|
||||
(請參考系統實際畫面)
|
||||
|
||||
---
|
||||
|
||||
**文件結束**
|
||||
438
SETUP.md
Normal file
438
SETUP.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# HR 基礎資料維護系統 - 安裝與配置指南
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
1. [系統需求](#系統需求)
|
||||
2. [環境配置](#環境配置)
|
||||
3. [資料庫設置](#資料庫設置)
|
||||
4. [Gitea 版本控制設置](#gitea-版本控制設置)
|
||||
5. [LLM API 配置](#llm-api-配置)
|
||||
6. [啟動系統](#啟動系統)
|
||||
7. [功能測試](#功能測試)
|
||||
8. [常見問題](#常見問題)
|
||||
|
||||
---
|
||||
|
||||
## 系統需求
|
||||
|
||||
### 軟體需求
|
||||
|
||||
- **Python**: 3.8 或更高版本
|
||||
- **MySQL**: 5.7 或更高版本
|
||||
- **Git**: 2.0 或更高版本 (用於版本控制)
|
||||
- **現代瀏覽器**: Chrome, Firefox, Edge (最新版本)
|
||||
|
||||
### 硬體需求
|
||||
|
||||
- **記憶體**: 最少 2GB RAM
|
||||
- **硬碟**: 最少 500MB 可用空間
|
||||
- **網路**: 穩定的網際網路連線 (用於 LLM API)
|
||||
|
||||
---
|
||||
|
||||
## 環境配置
|
||||
|
||||
### 1. 複製或下載項目
|
||||
|
||||
```bash
|
||||
cd d:/00001_Vibe_coding/1204剛為
|
||||
```
|
||||
|
||||
### 2. 創建虛擬環境 (建議)
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
|
||||
# Linux/Mac
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### 3. 安裝依賴套件
|
||||
|
||||
```bash
|
||||
pip install -r requirements_full.txt
|
||||
```
|
||||
|
||||
如果出現錯誤,可以嘗試逐個安裝:
|
||||
|
||||
```bash
|
||||
pip install flask flask-cors pymysql python-dotenv requests gitpython
|
||||
```
|
||||
|
||||
### 4. 配置環境變數
|
||||
|
||||
項目根目錄下已有 [.env](./.env) 文件,請根據實際情況修改:
|
||||
|
||||
```env
|
||||
# MySQL Database Configuration
|
||||
DB_HOST=mysql.theaken.com
|
||||
DB_PORT=33306
|
||||
DB_NAME=db_A102
|
||||
DB_USER=A102
|
||||
DB_PASSWORD=Bb123456
|
||||
|
||||
# Gitea Version Control Configuration
|
||||
GITEA_URL=https://gitea.theaken.com/
|
||||
GITEA_USER=donald
|
||||
GITEA_PASSWORD=!QAZ2wsx
|
||||
GITEA_TOKEN=9e0a888d1a25bde9cf2ad5dff2bb7ee6d68d6ff0
|
||||
|
||||
# LLM API Keys (需要自行申請並填寫)
|
||||
GEMINI_API_KEY=your_gemini_api_key_here
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
```
|
||||
|
||||
⚠️ **重要**: `.env` 文件包含敏感資訊,已加入 `.gitignore`,請勿上傳到版本控制系統。
|
||||
|
||||
---
|
||||
|
||||
## 資料庫設置
|
||||
|
||||
### 1. 測試資料庫連線
|
||||
|
||||
```bash
|
||||
python init_database.py
|
||||
```
|
||||
|
||||
此腳本會:
|
||||
- 測試資料庫連線
|
||||
- 創建 `hr_position_system` 資料庫架構
|
||||
- 建立所有必要的資料表
|
||||
- 插入參照資料
|
||||
|
||||
### 2. 驗證資料表
|
||||
|
||||
成功執行後,應該會看到以下資料表:
|
||||
|
||||
- `positions` - 崗位基礎資料表
|
||||
- `jobs` - 職務基礎資料表
|
||||
- `job_descriptions` - 崗位描述表
|
||||
- `reference_codes` - 參照資料代碼表
|
||||
- `audit_logs` - 審計日誌表
|
||||
|
||||
### 3. 手動執行 SQL (可選)
|
||||
|
||||
如果自動腳本失敗,可以手動執行:
|
||||
|
||||
```bash
|
||||
# 使用 MySQL 客戶端
|
||||
mysql -h mysql.theaken.com -P 33306 -u A102 -p db_A102 < database_schema.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gitea 版本控制設置
|
||||
|
||||
### 1. 測試 Gitea 連線
|
||||
|
||||
```bash
|
||||
python init_gitea.py
|
||||
```
|
||||
|
||||
此腳本會:
|
||||
- 測試 Gitea 伺服器連線
|
||||
- 創建新的 Git 倉庫 (如果不存在)
|
||||
- 配置本地 Git remote
|
||||
- 創建初始提交
|
||||
|
||||
### 2. 推送到 Gitea
|
||||
|
||||
```bash
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### 3. 訪問 Gitea 倉庫
|
||||
|
||||
在瀏覽器中訪問: [https://gitea.theaken.com/donald/hr-position-system](https://gitea.theaken.com/donald/hr-position-system)
|
||||
|
||||
---
|
||||
|
||||
## LLM API 配置
|
||||
|
||||
系統支援三種 LLM API:
|
||||
|
||||
### 1. Google Gemini
|
||||
|
||||
1. 訪問 [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
2. 創建 API Key
|
||||
3. 將 API Key 填入 `.env` 文件的 `GEMINI_API_KEY`
|
||||
|
||||
### 2. DeepSeek
|
||||
|
||||
1. 訪問 [DeepSeek 官網](https://www.deepseek.com/)
|
||||
2. 註冊並獲取 API Key
|
||||
3. 將 API Key 填入 `.env` 文件的 `DEEPSEEK_API_KEY`
|
||||
|
||||
### 3. OpenAI
|
||||
|
||||
1. 訪問 [OpenAI Platform](https://platform.openai.com/api-keys)
|
||||
2. 創建 API Key
|
||||
3. 將 API Key 填入 `.env` 文件的 `OPENAI_API_KEY`
|
||||
|
||||
### 4. 測試 LLM API
|
||||
|
||||
```bash
|
||||
python llm_config.py
|
||||
```
|
||||
|
||||
此腳本會測試所有已配置的 API 連線狀態。
|
||||
|
||||
---
|
||||
|
||||
## 啟動系統
|
||||
|
||||
### 方法 1: 使用更新版的 app (包含 LLM 功能)
|
||||
|
||||
```bash
|
||||
# 備份原有的 app.py
|
||||
mv app.py app_original.py
|
||||
|
||||
# 使用新版 app
|
||||
mv app_updated.py app.py
|
||||
|
||||
# 啟動系統
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 方法 2: 直接使用更新版
|
||||
|
||||
```bash
|
||||
python app_updated.py
|
||||
```
|
||||
|
||||
### 啟動成功
|
||||
|
||||
您應該會看到類似以下的輸出:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ HR 基礎資料維護系統 - Flask Backend ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ 伺服器啟動中... ║
|
||||
║ 訪問網址: http://localhost:5000 ║
|
||||
...
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
✓ LLM 功能已啟用
|
||||
已配置的 API: gemini, deepseek, openai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 功能測試
|
||||
|
||||
### 1. 訪問主頁面
|
||||
|
||||
在瀏覽器中打開: [http://localhost:5000](http://localhost:5000)
|
||||
|
||||
### 2. 測試 LLM API 連線
|
||||
|
||||
訪問 API 測試頁面: [http://localhost:5000/api-test](http://localhost:5000/api-test)
|
||||
|
||||
在此頁面可以:
|
||||
- 查看所有 LLM API 配置狀態
|
||||
- 單獨測試每個 API 連線
|
||||
- 一次測試所有 API
|
||||
|
||||
### 3. 測試 REST API
|
||||
|
||||
使用 Postman 或 curl 測試 API:
|
||||
|
||||
```bash
|
||||
# 獲取崗位列表
|
||||
curl http://localhost:5000/api/positions
|
||||
|
||||
# 獲取職務列表
|
||||
curl http://localhost:5000/api/jobs
|
||||
|
||||
# 測試 LLM API 配置
|
||||
curl http://localhost:5000/api/llm/config
|
||||
|
||||
# 測試 Gemini API
|
||||
curl http://localhost:5000/api/llm/test/gemini
|
||||
```
|
||||
|
||||
### 4. 測試崗位資料 CRUD
|
||||
|
||||
1. **新增崗位**: 點擊「新增崗位」按鈕
|
||||
2. **填寫表單**: 輸入崗位基本資料
|
||||
3. **AI 自動填充**: 點擊「✨ I'm feeling lucky」按鈕
|
||||
4. **保存資料**: 點擊「保存並退出」
|
||||
|
||||
### 5. 測試錯誤處理
|
||||
|
||||
系統已集成全局錯誤處理:
|
||||
|
||||
- 所有 API 錯誤會自動顯示錯誤對話框
|
||||
- 網路錯誤會有友好的提示訊息
|
||||
- 可以在瀏覽器控制台查看詳細錯誤信息
|
||||
|
||||
---
|
||||
|
||||
## 常見問題
|
||||
|
||||
### Q1: 資料庫連線失敗
|
||||
|
||||
**問題**: 執行 `init_database.py` 時出現連線錯誤
|
||||
|
||||
**解決方案**:
|
||||
1. 檢查 `.env` 文件中的資料庫配置是否正確
|
||||
2. 確認資料庫伺服器是否可訪問:
|
||||
```bash
|
||||
ping mysql.theaken.com
|
||||
```
|
||||
3. 檢查防火牆是否阻擋了 33306 端口
|
||||
4. 確認資料庫用戶名和密碼正確
|
||||
|
||||
### Q2: Gitea 推送失敗
|
||||
|
||||
**問題**: `git push` 時要求輸入密碼
|
||||
|
||||
**解決方案**:
|
||||
1. 使用 Token 進行認證:
|
||||
```bash
|
||||
git remote set-url origin https://donald:9e0a888d1a25bde9cf2ad5dff2bb7ee6d68d6ff0@gitea.theaken.com/donald/hr-position-system.git
|
||||
```
|
||||
2. 或配置 Git credential helper
|
||||
|
||||
### Q3: LLM API 測試失敗
|
||||
|
||||
**問題**: LLM API 連線測試顯示失敗
|
||||
|
||||
**解決方案**:
|
||||
1. 檢查 API Key 是否正確填寫在 `.env` 文件中
|
||||
2. 確認 API Key 是否有效 (未過期)
|
||||
3. 檢查網路連線是否正常
|
||||
4. 查看是否有 API 配額限制
|
||||
|
||||
### Q4: 端口被占用
|
||||
|
||||
**問題**: 啟動 Flask 時提示 `Address already in use`
|
||||
|
||||
**解決方案**:
|
||||
1. 更改端口:
|
||||
```python
|
||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||
```
|
||||
2. 或關閉占用 5000 端口的進程:
|
||||
```bash
|
||||
# Windows
|
||||
netstat -ano | findstr :5000
|
||||
taskkill /PID <PID> /F
|
||||
|
||||
# Linux/Mac
|
||||
lsof -i :5000
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### Q5: 前端頁面無法訪問
|
||||
|
||||
**問題**: 瀏覽器顯示 404 Not Found
|
||||
|
||||
**解決方案**:
|
||||
1. 確認 Flask 已正確啟動
|
||||
2. 檢查 `index.html` 是否存在於項目根目錄
|
||||
3. 清除瀏覽器緩存並重新載入
|
||||
|
||||
### Q6: LLM 功能未啟用
|
||||
|
||||
**問題**: 系統提示「LLM 功能未啟用」
|
||||
|
||||
**解決方案**:
|
||||
1. 確認 `llm_config.py` 文件存在
|
||||
2. 確認已安裝 `requests` 套件:
|
||||
```bash
|
||||
pip install requests
|
||||
```
|
||||
3. 使用 `app_updated.py` 而非原始的 `app.py`
|
||||
|
||||
---
|
||||
|
||||
## 系統架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 使用者瀏覽器 │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ index.html (主應用) │ │
|
||||
│ │ api_test.html (API測試頁) │ │
|
||||
│ │ error_handler.js (錯誤處理) │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Flask Backend (app.py) │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ 崗位資料 API │ │
|
||||
│ │ 職務資料 API │ │
|
||||
│ │ 參照資料 API │ │
|
||||
│ │ LLM API (llm_config.py) │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ MySQL │ │ LLM APIs │
|
||||
│ Database │ │ (Gemini, │
|
||||
│ │ │ DeepSeek,│
|
||||
│ │ │ OpenAI) │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案說明
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|------|------|
|
||||
| `.env` | 環境變數配置 |
|
||||
| `.gitignore` | Git 忽略檔案配置 |
|
||||
| `database_schema.sql` | MySQL 資料庫架構 |
|
||||
| `init_database.py` | 資料庫初始化腳本 |
|
||||
| `init_gitea.py` | Gitea 倉庫初始化腳本 |
|
||||
| `llm_config.py` | LLM API 配置模組 |
|
||||
| `app.py` | Flask 後端主程式 (原始版) |
|
||||
| `app_updated.py` | Flask 後端主程式 (包含 LLM) |
|
||||
| `error_handler.js` | 全局錯誤處理模組 |
|
||||
| `api_test.html` | LLM API 測試頁面 |
|
||||
| `index.html` | 主應用頁面 |
|
||||
| `requirements_full.txt` | Python 依賴套件列表 |
|
||||
| `SDD.md` | 軟體設計文件 |
|
||||
| `SETUP.md` | 本安裝指南 |
|
||||
|
||||
---
|
||||
|
||||
## 技術支援
|
||||
|
||||
如遇到問題,請:
|
||||
|
||||
1. 查看瀏覽器控制台的錯誤訊息
|
||||
2. 檢查 Flask 終端的錯誤日誌
|
||||
3. 參考本文檔的[常見問題](#常見問題)章節
|
||||
4. 查看系統設計文件 `SDD.md`
|
||||
|
||||
---
|
||||
|
||||
## 更新日誌
|
||||
|
||||
### Version 1.0 (2024-12-03)
|
||||
|
||||
- ✅ 初始版本發布
|
||||
- ✅ 資料庫架構設計與初始化
|
||||
- ✅ Gitea 版本控制整合
|
||||
- ✅ LLM API 整合 (Gemini, DeepSeek, OpenAI)
|
||||
- ✅ 全局錯誤處理機制
|
||||
- ✅ API 測試頁面
|
||||
- ✅ 環境配置自動化
|
||||
|
||||
---
|
||||
|
||||
**文件版本**: 1.0
|
||||
**最後更新**: 2024-12-03
|
||||
**維護者**: HR System Development Team
|
||||
520
api_test.html
Normal file
520
api_test.html
Normal file
@@ -0,0 +1,520 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LLM API 連線測試 - HR 系統</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1a5276;
|
||||
--primary-light: #2980b9;
|
||||
--success: #27ae60;
|
||||
--danger: #e74c3c;
|
||||
--warning: #f39c12;
|
||||
--border: #ddd;
|
||||
--bg-light: #f8f9fa;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.api-card {
|
||||
background: var(--bg-light);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.api-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.api-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.api-name {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #e8e8e8;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.status-testing {
|
||||
background: #fff3cd;
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.api-info {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.api-info-label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.api-info-value {
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.api-key-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.api-key-masked {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.api-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-light);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(26, 82, 118, 0.3);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #229954;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.result-message {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result-success {
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
border: 1px solid var(--success);
|
||||
}
|
||||
|
||||
.result-error {
|
||||
background: #f8d7da;
|
||||
color: var(--danger);
|
||||
border: 1px solid var(--danger);
|
||||
}
|
||||
|
||||
.test-all-section {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
background: var(--bg-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.test-all-btn {
|
||||
padding: 15px 40px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 3px solid rgba(255,255,255,.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.summary {
|
||||
background: white;
|
||||
border: 2px solid var(--primary);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.summary h2 {
|
||||
color: var(--primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔌 LLM API 連線測試</h1>
|
||||
<p>HR 基礎資料維護系統 - API 配置檢測</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Summary Section -->
|
||||
<div class="summary">
|
||||
<h2>📊 API 配置總覽</h2>
|
||||
<div class="summary-stats">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value" id="totalApis">3</div>
|
||||
<div class="stat-label">API 總數</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value" id="configuredApis" style="color: var(--success);">0</div>
|
||||
<div class="stat-label">已配置</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value" id="testedApis" style="color: var(--primary);">0</div>
|
||||
<div class="stat-label">已測試</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value" id="successApis" style="color: var(--success);">0</div>
|
||||
<div class="stat-label">測試成功</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test All Button -->
|
||||
<div class="test-all-section">
|
||||
<button class="btn btn-success test-all-btn" onclick="testAllAPIs()">
|
||||
🚀 測試所有已配置的 API
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Gemini API Card -->
|
||||
<div class="api-card" id="gemini-card">
|
||||
<div class="api-header">
|
||||
<div class="api-name">🔷 Google Gemini</div>
|
||||
<div class="status-badge status-pending" id="gemini-status">未測試</div>
|
||||
</div>
|
||||
<div class="api-info">
|
||||
<div class="api-info-label">API Key:</div>
|
||||
<div class="api-info-value">
|
||||
<div class="api-key-display" id="gemini-key">
|
||||
<span class="api-key-masked">••••••••••••••••</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-info-label">Endpoint:</div>
|
||||
<div class="api-info-value">https://generativelanguage.googleapis.com</div>
|
||||
<div class="api-info-label">狀態:</div>
|
||||
<div class="api-info-value" id="gemini-config-status">等待檢測...</div>
|
||||
</div>
|
||||
<div class="api-actions">
|
||||
<button class="btn btn-primary" onclick="testAPI('gemini')" id="gemini-test-btn">
|
||||
🧪 測試連線
|
||||
</button>
|
||||
</div>
|
||||
<div class="result-message" id="gemini-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- DeepSeek API Card -->
|
||||
<div class="api-card" id="deepseek-card">
|
||||
<div class="api-header">
|
||||
<div class="api-name">🔵 DeepSeek</div>
|
||||
<div class="status-badge status-pending" id="deepseek-status">未測試</div>
|
||||
</div>
|
||||
<div class="api-info">
|
||||
<div class="api-info-label">API Key:</div>
|
||||
<div class="api-info-value">
|
||||
<div class="api-key-display" id="deepseek-key">
|
||||
<span class="api-key-masked">••••••••••••••••</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-info-label">Endpoint:</div>
|
||||
<div class="api-info-value">https://api.deepseek.com/v1</div>
|
||||
<div class="api-info-label">狀態:</div>
|
||||
<div class="api-info-value" id="deepseek-config-status">等待檢測...</div>
|
||||
</div>
|
||||
<div class="api-actions">
|
||||
<button class="btn btn-primary" onclick="testAPI('deepseek')" id="deepseek-test-btn">
|
||||
🧪 測試連線
|
||||
</button>
|
||||
</div>
|
||||
<div class="result-message" id="deepseek-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI API Card -->
|
||||
<div class="api-card" id="openai-card">
|
||||
<div class="api-header">
|
||||
<div class="api-name">🟢 OpenAI</div>
|
||||
<div class="status-badge status-pending" id="openai-status">未測試</div>
|
||||
</div>
|
||||
<div class="api-info">
|
||||
<div class="api-info-label">API Key:</div>
|
||||
<div class="api-info-value">
|
||||
<div class="api-key-display" id="openai-key">
|
||||
<span class="api-key-masked">••••••••••••••••</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-info-label">Endpoint:</div>
|
||||
<div class="api-info-value">https://api.openai.com/v1</div>
|
||||
<div class="api-info-label">狀態:</div>
|
||||
<div class="api-info-value" id="openai-config-status">等待檢測...</div>
|
||||
</div>
|
||||
<div class="api-actions">
|
||||
<button class="btn btn-primary" onclick="testAPI('openai')" id="openai-test-btn">
|
||||
🧪 測試連線
|
||||
</button>
|
||||
</div>
|
||||
<div class="result-message" id="openai-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check API configuration on page load
|
||||
window.addEventListener('DOMContentLoaded', checkAPIConfiguration);
|
||||
|
||||
async function checkAPIConfiguration() {
|
||||
try {
|
||||
const response = await fetch('/api/llm/config');
|
||||
const data = await response.json();
|
||||
|
||||
let configured = 0;
|
||||
let tested = 0;
|
||||
|
||||
// Update UI for each API
|
||||
['gemini', 'deepseek', 'openai'].forEach(api => {
|
||||
const config = data[api];
|
||||
const isConfigured = config && config.enabled;
|
||||
|
||||
if (isConfigured) {
|
||||
configured++;
|
||||
document.getElementById(`${api}-config-status`).textContent = '✓ 已配置';
|
||||
document.getElementById(`${api}-config-status`).style.color = 'var(--success)';
|
||||
|
||||
// Show partial API key
|
||||
if (config.api_key) {
|
||||
const maskedKey = config.api_key.substring(0, 8) + '••••••••' + config.api_key.substring(config.api_key.length - 4);
|
||||
document.getElementById(`${api}-key`).innerHTML = `<span>${maskedKey}</span>`;
|
||||
}
|
||||
} else {
|
||||
document.getElementById(`${api}-config-status`).textContent = '✗ 未配置';
|
||||
document.getElementById(`${api}-config-status`).style.color = 'var(--danger)';
|
||||
document.getElementById(`${api}-test-btn`).disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update summary
|
||||
document.getElementById('configuredApis').textContent = configured;
|
||||
|
||||
} catch (error) {
|
||||
showError('無法載入 API 配置');
|
||||
}
|
||||
}
|
||||
|
||||
async function testAPI(apiName) {
|
||||
const statusBadge = document.getElementById(`${apiName}-status`);
|
||||
const resultDiv = document.getElementById(`${apiName}-result`);
|
||||
const testBtn = document.getElementById(`${apiName}-test-btn`);
|
||||
|
||||
// Update UI to testing state
|
||||
statusBadge.className = 'status-badge status-testing';
|
||||
statusBadge.innerHTML = '測試中<span class="loading-spinner"></span>';
|
||||
testBtn.disabled = true;
|
||||
resultDiv.className = 'result-message';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/llm/test/${apiName}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
statusBadge.className = 'status-badge status-success';
|
||||
statusBadge.textContent = '✓ 連線成功';
|
||||
resultDiv.className = 'result-message result-success show';
|
||||
resultDiv.textContent = `✓ ${data.message}`;
|
||||
updateSummary('tested', true);
|
||||
} else {
|
||||
statusBadge.className = 'status-badge status-error';
|
||||
statusBadge.textContent = '✗ 連線失敗';
|
||||
resultDiv.className = 'result-message result-error show';
|
||||
resultDiv.textContent = `✗ ${data.message}`;
|
||||
updateSummary('tested', false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
statusBadge.className = 'status-badge status-error';
|
||||
statusBadge.textContent = '✗ 錯誤';
|
||||
resultDiv.className = 'result-message result-error show';
|
||||
resultDiv.textContent = `✗ 發生錯誤: ${error.message}`;
|
||||
updateSummary('tested', false);
|
||||
} finally {
|
||||
testBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAllAPIs() {
|
||||
const apis = ['gemini', 'deepseek', 'openai'];
|
||||
const testBtn = document.querySelector('.test-all-btn');
|
||||
|
||||
testBtn.disabled = true;
|
||||
testBtn.innerHTML = '測試中<span class="loading-spinner"></span>';
|
||||
|
||||
for (const api of apis) {
|
||||
const isEnabled = !document.getElementById(`${api}-test-btn`).disabled;
|
||||
if (isEnabled) {
|
||||
await testAPI(api);
|
||||
// Small delay between tests
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
testBtn.disabled = false;
|
||||
testBtn.innerHTML = '🚀 測試所有已配置的 API';
|
||||
}
|
||||
|
||||
function updateSummary(type, success) {
|
||||
const testedEl = document.getElementById('testedApis');
|
||||
const successEl = document.getElementById('successApis');
|
||||
|
||||
if (type === 'tested') {
|
||||
const currentTested = parseInt(testedEl.textContent) || 0;
|
||||
testedEl.textContent = currentTested + 1;
|
||||
|
||||
if (success) {
|
||||
const currentSuccess = parseInt(successEl.textContent) || 0;
|
||||
successEl.textContent = currentSuccess + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert(message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
259
database_schema.sql
Normal file
259
database_schema.sql
Normal file
@@ -0,0 +1,259 @@
|
||||
-- HR Position System Database Schema
|
||||
-- Database: db_A102
|
||||
-- Schema: hr_position_system
|
||||
|
||||
-- Create schema if not exists
|
||||
CREATE SCHEMA IF NOT EXISTS hr_position_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE hr_position_system;
|
||||
|
||||
-- ============================================================
|
||||
-- Table: positions (崗位基礎資料)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS positions (
|
||||
id VARCHAR(20) PRIMARY KEY COMMENT '崗位編號',
|
||||
position_code VARCHAR(20) NOT NULL UNIQUE COMMENT '崗位編號',
|
||||
position_name VARCHAR(100) NOT NULL COMMENT '崗位名稱',
|
||||
position_category VARCHAR(2) COMMENT '崗位類別代碼',
|
||||
position_category_name VARCHAR(50) COMMENT '崗位類別名稱',
|
||||
position_nature VARCHAR(2) COMMENT '崗位性質代碼',
|
||||
position_nature_name VARCHAR(50) COMMENT '崗位性質名稱',
|
||||
headcount INT COMMENT '編制人數',
|
||||
position_level VARCHAR(2) COMMENT '崗位級別',
|
||||
effective_date DATE COMMENT '生效日期',
|
||||
position_desc TEXT COMMENT '崗位描述',
|
||||
position_remark TEXT COMMENT '崗位備注',
|
||||
|
||||
-- Recruitment Info
|
||||
min_education VARCHAR(3) COMMENT '最低學歷',
|
||||
required_gender VARCHAR(1) COMMENT '要求性別',
|
||||
salary_range VARCHAR(1) COMMENT '薪酬范圍',
|
||||
work_experience VARCHAR(2) COMMENT '工作經驗(年)',
|
||||
min_age INT COMMENT '最小年齡',
|
||||
max_age INT COMMENT '最大年齡',
|
||||
job_type VARCHAR(2) COMMENT '工作性質',
|
||||
recruit_position VARCHAR(3) COMMENT '招聘職位',
|
||||
job_title VARCHAR(100) COMMENT '職位名稱',
|
||||
job_desc TEXT COMMENT '職位描述',
|
||||
position_req TEXT COMMENT '崗位要求',
|
||||
title_req VARCHAR(4) COMMENT '職稱要求',
|
||||
major_req VARCHAR(200) COMMENT '專業要求',
|
||||
skill_req VARCHAR(200) COMMENT '技能要求',
|
||||
lang_req VARCHAR(100) COMMENT '語言要求',
|
||||
other_req VARCHAR(200) COMMENT '其他要求',
|
||||
superior_position VARCHAR(20) COMMENT '上級崗位編號',
|
||||
recruit_remark TEXT COMMENT '備注說明',
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||
created_by VARCHAR(50) COMMENT '建立人',
|
||||
updated_by VARCHAR(50) COMMENT '更新人',
|
||||
|
||||
INDEX idx_position_code (position_code),
|
||||
INDEX idx_position_name (position_name),
|
||||
INDEX idx_position_category (position_category),
|
||||
INDEX idx_position_level (position_level)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位基礎資料表';
|
||||
|
||||
-- ============================================================
|
||||
-- Table: jobs (職務基礎資料)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id VARCHAR(20) PRIMARY KEY COMMENT '職務編號',
|
||||
job_category_code VARCHAR(4) NOT NULL COMMENT '職務類別編號',
|
||||
job_category_name VARCHAR(50) COMMENT '職務類別名稱',
|
||||
job_code VARCHAR(20) NOT NULL UNIQUE COMMENT '職務編號',
|
||||
job_name VARCHAR(100) NOT NULL COMMENT '職務名稱',
|
||||
job_name_en VARCHAR(100) COMMENT '職務英文名稱',
|
||||
job_effective_date DATE COMMENT '生效日期',
|
||||
job_headcount INT COMMENT '編制人數',
|
||||
job_sort_order INT COMMENT '排列順序',
|
||||
job_remark TEXT COMMENT '備注說明',
|
||||
job_level VARCHAR(50) COMMENT '職務層級',
|
||||
has_attendance_bonus BOOLEAN DEFAULT FALSE COMMENT '是否有全勤',
|
||||
has_housing_allowance BOOLEAN DEFAULT FALSE COMMENT '是否住房補貼',
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||
created_by VARCHAR(50) COMMENT '建立人',
|
||||
updated_by VARCHAR(50) COMMENT '更新人',
|
||||
|
||||
INDEX idx_job_code (job_code),
|
||||
INDEX idx_job_name (job_name),
|
||||
INDEX idx_job_category (job_category_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='職務基礎資料表';
|
||||
|
||||
-- ============================================================
|
||||
-- Table: job_descriptions (崗位描述)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS job_descriptions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||
emp_no VARCHAR(20) COMMENT '工號',
|
||||
emp_name VARCHAR(50) COMMENT '姓名',
|
||||
position_code VARCHAR(20) COMMENT '崗位代碼',
|
||||
version_date DATE COMMENT '版本更新日期',
|
||||
|
||||
-- Position Info
|
||||
position_name VARCHAR(100) COMMENT '崗位名稱',
|
||||
department VARCHAR(100) COMMENT '所屬部門',
|
||||
position_effective_date DATE COMMENT '崗位生效日期',
|
||||
direct_supervisor VARCHAR(100) COMMENT '直接領導職務',
|
||||
position_grade_job VARCHAR(100) COMMENT '崗位職等&職務',
|
||||
report_to VARCHAR(100) COMMENT '匯報對象職務',
|
||||
direct_reports VARCHAR(200) COMMENT '直接下級',
|
||||
work_location VARCHAR(3) COMMENT '任職地點',
|
||||
emp_attribute VARCHAR(2) COMMENT '員工屬性',
|
||||
|
||||
-- Responsibilities
|
||||
position_purpose VARCHAR(500) COMMENT '崗位設置目的',
|
||||
main_responsibilities TEXT COMMENT '主要崗位職責',
|
||||
|
||||
-- Requirements
|
||||
education VARCHAR(200) COMMENT '教育程度',
|
||||
basic_skills TEXT COMMENT '基本技能',
|
||||
professional_knowledge TEXT COMMENT '專業知識',
|
||||
work_experience_req TEXT COMMENT '工作經驗',
|
||||
other_requirements TEXT COMMENT '其他要求',
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||
created_by VARCHAR(50) COMMENT '建立人',
|
||||
updated_by VARCHAR(50) COMMENT '更新人',
|
||||
|
||||
INDEX idx_emp_no (emp_no),
|
||||
INDEX idx_position_code (position_code),
|
||||
INDEX idx_version_date (version_date),
|
||||
FOREIGN KEY (position_code) REFERENCES positions(position_code) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='崗位描述表';
|
||||
|
||||
-- ============================================================
|
||||
-- Table: reference_codes (參照資料代碼表)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS reference_codes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||
code_type VARCHAR(50) NOT NULL COMMENT '代碼類型',
|
||||
code_value VARCHAR(10) NOT NULL COMMENT '代碼值',
|
||||
code_name VARCHAR(100) NOT NULL COMMENT '代碼名稱',
|
||||
code_name_en VARCHAR(100) COMMENT '英文名稱',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '是否啟用',
|
||||
remark VARCHAR(200) COMMENT '備注',
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
|
||||
|
||||
UNIQUE KEY uk_code_type_value (code_type, code_value),
|
||||
INDEX idx_code_type (code_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='參照資料代碼表';
|
||||
|
||||
-- ============================================================
|
||||
-- Table: audit_logs (審計日誌)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主鍵',
|
||||
action VARCHAR(20) NOT NULL COMMENT '操作類型(CREATE/UPDATE/DELETE)',
|
||||
entity_type VARCHAR(50) NOT NULL COMMENT '實體類型',
|
||||
entity_id VARCHAR(50) NOT NULL COMMENT '實體ID',
|
||||
old_data JSON COMMENT '變更前資料',
|
||||
new_data JSON COMMENT '變更後資料',
|
||||
user_id VARCHAR(50) COMMENT '操作用戶',
|
||||
ip_address VARCHAR(50) COMMENT 'IP地址',
|
||||
user_agent VARCHAR(200) COMMENT '用戶代理',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '操作時間',
|
||||
|
||||
INDEX idx_entity_type_id (entity_type, entity_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='審計日誌表';
|
||||
|
||||
-- ============================================================
|
||||
-- Insert Reference Data (參照資料初始化)
|
||||
-- ============================================================
|
||||
|
||||
-- 崗位類別 (Position Category)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('POSITION_CATEGORY', '01', '技術職', 1),
|
||||
('POSITION_CATEGORY', '02', '管理職', 2),
|
||||
('POSITION_CATEGORY', '03', '業務職', 3),
|
||||
('POSITION_CATEGORY', '04', '行政職', 4);
|
||||
|
||||
-- 崗位性質 (Position Nature)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
|
||||
('POSITION_NATURE', 'FT', '全職', 'Full-time', 1),
|
||||
('POSITION_NATURE', 'PT', '兼職', 'Part-time', 2),
|
||||
('POSITION_NATURE', 'CT', '約聘', 'Contract', 3),
|
||||
('POSITION_NATURE', 'IN', '實習', 'Intern', 4);
|
||||
|
||||
-- 崗位級別 (Position Level)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('POSITION_LEVEL', 'L1', '基層員工', 1),
|
||||
('POSITION_LEVEL', 'L2', '資深員工', 2),
|
||||
('POSITION_LEVEL', 'L3', '主管', 3),
|
||||
('POSITION_LEVEL', 'L4', '經理', 4),
|
||||
('POSITION_LEVEL', 'L5', '總監', 5),
|
||||
('POSITION_LEVEL', 'L6', '副總', 6),
|
||||
('POSITION_LEVEL', 'L7', '總經理', 7);
|
||||
|
||||
-- 職務類別 (Job Category)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('JOB_CATEGORY', 'MGR', '管理職', 1),
|
||||
('JOB_CATEGORY', 'TECH', '技術職', 2),
|
||||
('JOB_CATEGORY', 'SALE', '業務職', 3),
|
||||
('JOB_CATEGORY', 'ADMIN', '行政職', 4),
|
||||
('JOB_CATEGORY', 'RD', '研發職', 5),
|
||||
('JOB_CATEGORY', 'PROD', '生產職', 6);
|
||||
|
||||
-- 學歷 (Education)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, code_name_en, sort_order) VALUES
|
||||
('EDUCATION', 'HS', '高中', 'High School', 1),
|
||||
('EDUCATION', 'JC', '專科', 'Junior College', 2),
|
||||
('EDUCATION', 'BA', '大學', 'Bachelor', 3),
|
||||
('EDUCATION', 'MA', '碩士', 'Master', 4),
|
||||
('EDUCATION', 'PHD', '博士', 'PhD', 5);
|
||||
|
||||
-- 薪酬范圍 (Salary Range)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('SALARY_RANGE', 'A', 'A級', 1),
|
||||
('SALARY_RANGE', 'B', 'B級', 2),
|
||||
('SALARY_RANGE', 'C', 'C級', 3),
|
||||
('SALARY_RANGE', 'D', 'D級', 4),
|
||||
('SALARY_RANGE', 'E', 'E級', 5),
|
||||
('SALARY_RANGE', 'N', '面議', 6);
|
||||
|
||||
-- 任職地點 (Work Location)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('WORK_LOCATION', 'HQ', '總部', 1),
|
||||
('WORK_LOCATION', 'TPE', '台北辦公室', 2),
|
||||
('WORK_LOCATION', 'TYC', '桃園廠區', 3),
|
||||
('WORK_LOCATION', 'KHH', '高雄廠區', 4),
|
||||
('WORK_LOCATION', 'SH', '上海辦公室', 5),
|
||||
('WORK_LOCATION', 'SZ', '深圳辦公室', 6);
|
||||
|
||||
-- 員工屬性 (Employee Attribute)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('EMP_ATTRIBUTE', 'FT', '正式員工', 1),
|
||||
('EMP_ATTRIBUTE', 'CT', '約聘人員', 2),
|
||||
('EMP_ATTRIBUTE', 'PT', '兼職人員', 3),
|
||||
('EMP_ATTRIBUTE', 'IN', '實習生', 4),
|
||||
('EMP_ATTRIBUTE', 'DP', '派遣人員', 5);
|
||||
|
||||
-- 招聘職位 (Recruit Position)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('RECRUIT_POSITION', 'ENG', '工程師', 1),
|
||||
('RECRUIT_POSITION', 'MGR', '經理', 2),
|
||||
('RECRUIT_POSITION', 'AST', '助理', 3),
|
||||
('RECRUIT_POSITION', 'OP', '操作員', 4),
|
||||
('RECRUIT_POSITION', 'SAL', '業務', 5);
|
||||
|
||||
-- 職稱要求 (Title Requirement)
|
||||
INSERT INTO reference_codes (code_type, code_value, code_name, sort_order) VALUES
|
||||
('TITLE_REQ', 'NONE', '無要求', 1),
|
||||
('TITLE_REQ', 'CERT', '需證書', 2),
|
||||
('TITLE_REQ', 'LIC', '需執照', 3);
|
||||
|
||||
-- ============================================================
|
||||
-- End of Schema
|
||||
-- ============================================================
|
||||
559
error_handler.js
Normal file
559
error_handler.js
Normal file
@@ -0,0 +1,559 @@
|
||||
/**
|
||||
* Global Error Handler for HR Position System
|
||||
* Provides unified error handling and user-friendly error dialogs
|
||||
*/
|
||||
|
||||
class ErrorHandler {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create error modal container if not exists
|
||||
if (!document.getElementById('error-modal-container')) {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'error-modal-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Add error handler CSS
|
||||
this.injectStyles();
|
||||
|
||||
// Setup global error handlers
|
||||
this.setupGlobalHandlers();
|
||||
}
|
||||
|
||||
injectStyles() {
|
||||
if (document.getElementById('error-handler-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'error-handler-styles';
|
||||
style.textContent = `
|
||||
.error-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.error-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
animation: slideDown 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.error-modal-header.error {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
color: white;
|
||||
border-bottom-color: #c0392b;
|
||||
}
|
||||
|
||||
.error-modal-header.warning {
|
||||
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
|
||||
color: white;
|
||||
border-bottom-color: #e67e22;
|
||||
}
|
||||
|
||||
.error-modal-header.info {
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
color: white;
|
||||
border-bottom-color: #2980b9;
|
||||
}
|
||||
|
||||
.error-modal-header.success {
|
||||
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
|
||||
color: white;
|
||||
border-bottom-color: #229954;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
flex: 1;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-modal-body {
|
||||
padding: 25px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.error-modal-footer {
|
||||
padding: 15px 25px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
padding: 10px 25px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
}
|
||||
|
||||
.error-btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-btn-primary:hover {
|
||||
background: #2980b9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.error-btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.error-toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
z-index: 10001;
|
||||
animation: slideInRight 0.3s ease;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.error-toast.error {
|
||||
border-left: 4px solid #e74c3c;
|
||||
}
|
||||
|
||||
.error-toast.warning {
|
||||
border-left: 4px solid #f39c12;
|
||||
}
|
||||
|
||||
.error-toast.info {
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.error-toast.success {
|
||||
border-left: 4px solid #27ae60;
|
||||
}
|
||||
|
||||
.error-toast-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-toast-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.error-toast-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.error-toast-message {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.error-toast-close:hover {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setupGlobalHandlers() {
|
||||
// Handle uncaught errors
|
||||
window.addEventListener('error', (event) => {
|
||||
this.handleError({
|
||||
title: '執行錯誤',
|
||||
message: event.message,
|
||||
details: `檔案: ${event.filename}\n行號: ${event.lineno}:${event.colno}\n錯誤: ${event.error?.stack || event.message}`
|
||||
});
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
this.handleError({
|
||||
title: 'Promise 錯誤',
|
||||
message: '發生未處理的 Promise 錯誤',
|
||||
details: event.reason?.stack || event.reason
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error modal
|
||||
* @param {Object} options - Error options
|
||||
* @param {string} options.title - Error title
|
||||
* @param {string} options.message - Error message
|
||||
* @param {string} options.details - Error details (optional)
|
||||
* @param {string} options.type - Error type: error, warning, info, success (default: error)
|
||||
* @param {Function} options.onClose - Callback when modal closes
|
||||
*/
|
||||
showError(options) {
|
||||
const {
|
||||
title = '錯誤',
|
||||
message = '發生未知錯誤',
|
||||
details = null,
|
||||
type = 'error',
|
||||
onClose = null
|
||||
} = options;
|
||||
|
||||
const icons = {
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️',
|
||||
success: '✅'
|
||||
};
|
||||
|
||||
const container = document.getElementById('error-modal-container');
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'error-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="error-modal">
|
||||
<div class="error-modal-header ${type}">
|
||||
<div class="error-icon">${icons[type]}</div>
|
||||
<h3 class="error-title">${this.escapeHtml(title)}</h3>
|
||||
</div>
|
||||
<div class="error-modal-body">
|
||||
<div class="error-message">${this.escapeHtml(message)}</div>
|
||||
${details ? `<div class="error-details">${this.escapeHtml(details)}</div>` : ''}
|
||||
</div>
|
||||
<div class="error-modal-footer">
|
||||
<button class="error-btn error-btn-primary" onclick="window.errorHandler.closeModal(this)">
|
||||
確定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(modal);
|
||||
|
||||
// Store close callback
|
||||
modal._onClose = onClose;
|
||||
|
||||
// Close on overlay click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeModal(modal.querySelector('.error-btn'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* @param {Object} options - Toast options
|
||||
* @param {string} options.title - Toast title
|
||||
* @param {string} options.message - Toast message
|
||||
* @param {string} options.type - Toast type: error, warning, info, success (default: info)
|
||||
* @param {number} options.duration - Duration in ms (default: 3000)
|
||||
*/
|
||||
showToast(options) {
|
||||
const {
|
||||
title = '',
|
||||
message = '',
|
||||
type = 'info',
|
||||
duration = 3000
|
||||
} = options;
|
||||
|
||||
const icons = {
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️',
|
||||
success: '✅'
|
||||
};
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `error-toast ${type}`;
|
||||
toast.innerHTML = `
|
||||
<div class="error-toast-icon">${icons[type]}</div>
|
||||
<div class="error-toast-content">
|
||||
${title ? `<div class="error-toast-title">${this.escapeHtml(title)}</div>` : ''}
|
||||
<div class="error-toast-message">${this.escapeHtml(message)}</div>
|
||||
</div>
|
||||
<button class="error-toast-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Auto remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOutRight 0.3s ease';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
closeModal(button) {
|
||||
const modal = button.closest('.error-modal-overlay');
|
||||
if (modal) {
|
||||
const onClose = modal._onClose;
|
||||
|
||||
modal.style.animation = 'fadeOut 0.3s ease';
|
||||
setTimeout(() => {
|
||||
modal.remove();
|
||||
if (onClose && typeof onClose === 'function') {
|
||||
onClose();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API errors
|
||||
* @param {Error} error - Error object
|
||||
* @param {Object} options - Additional options
|
||||
*/
|
||||
async handleAPIError(error, options = {}) {
|
||||
const { showModal = true, showToast = false } = options;
|
||||
|
||||
let message = '發生 API 錯誤';
|
||||
let details = null;
|
||||
|
||||
if (error.response) {
|
||||
// Server responded with error
|
||||
const data = error.response.data;
|
||||
message = data.error || data.message || `HTTP ${error.response.status} 錯誤`;
|
||||
details = JSON.stringify(data, null, 2);
|
||||
} else if (error.request) {
|
||||
// No response received
|
||||
message = '無法連接到伺服器,請檢查網路連線';
|
||||
details = error.message;
|
||||
} else {
|
||||
// Request setup error
|
||||
message = error.message;
|
||||
details = error.stack;
|
||||
}
|
||||
|
||||
if (showModal) {
|
||||
this.showError({
|
||||
title: 'API 錯誤',
|
||||
message,
|
||||
details,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
this.showToast({
|
||||
title: 'API 錯誤',
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general errors
|
||||
* @param {Object} options - Error options
|
||||
*/
|
||||
handleError(options) {
|
||||
console.error('[ErrorHandler]', options);
|
||||
this.showError(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
* @param {string} text - Text to escape
|
||||
* @returns {string} - Escaped text
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm dialog
|
||||
* @param {Object} options - Confirm options
|
||||
* @param {string} options.title - Title
|
||||
* @param {string} options.message - Message
|
||||
* @param {string} options.confirmText - Confirm button text
|
||||
* @param {string} options.cancelText - Cancel button text
|
||||
* @param {Function} options.onConfirm - Confirm callback
|
||||
* @param {Function} options.onCancel - Cancel callback
|
||||
*/
|
||||
confirm(options) {
|
||||
const {
|
||||
title = '確認',
|
||||
message = '確定要執行此操作嗎?',
|
||||
confirmText = '確定',
|
||||
cancelText = '取消',
|
||||
onConfirm = null,
|
||||
onCancel = null
|
||||
} = options;
|
||||
|
||||
const container = document.getElementById('error-modal-container');
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'error-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="error-modal">
|
||||
<div class="error-modal-header info">
|
||||
<div class="error-icon">❓</div>
|
||||
<h3 class="error-title">${this.escapeHtml(title)}</h3>
|
||||
</div>
|
||||
<div class="error-modal-body">
|
||||
<div class="error-message">${this.escapeHtml(message)}</div>
|
||||
</div>
|
||||
<div class="error-modal-footer">
|
||||
<button class="error-btn error-btn-secondary" data-action="cancel">
|
||||
${this.escapeHtml(cancelText)}
|
||||
</button>
|
||||
<button class="error-btn error-btn-primary" data-action="confirm">
|
||||
${this.escapeHtml(confirmText)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(modal);
|
||||
|
||||
// Handle button clicks
|
||||
modal.querySelectorAll('[data-action]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.getAttribute('data-action');
|
||||
modal.remove();
|
||||
|
||||
if (action === 'confirm' && onConfirm) {
|
||||
onConfirm();
|
||||
} else if (action === 'cancel' && onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize global error handler
|
||||
window.errorHandler = new ErrorHandler();
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ErrorHandler;
|
||||
}
|
||||
2184
index.html
Normal file
2184
index.html
Normal file
File diff suppressed because it is too large
Load Diff
130
init_database.py
Normal file
130
init_database.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Database initialization script for HR Position System
|
||||
Connects to MySQL and executes the schema creation script
|
||||
"""
|
||||
|
||||
import os
|
||||
import pymysql
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def init_database():
|
||||
"""Initialize the database schema"""
|
||||
|
||||
# Database connection parameters
|
||||
db_config = {
|
||||
'host': os.getenv('DB_HOST'),
|
||||
'port': int(os.getenv('DB_PORT')),
|
||||
'user': os.getenv('DB_USER'),
|
||||
'password': os.getenv('DB_PASSWORD'),
|
||||
'database': os.getenv('DB_NAME'),
|
||||
'charset': 'utf8mb4',
|
||||
'cursorclass': pymysql.cursors.DictCursor
|
||||
}
|
||||
|
||||
print(f"Connecting to database: {db_config['host']}:{db_config['port']}")
|
||||
print(f"Database: {db_config['database']}")
|
||||
|
||||
try:
|
||||
# Connect to MySQL
|
||||
connection = pymysql.connect(**db_config)
|
||||
print("✓ Successfully connected to database")
|
||||
|
||||
# Read SQL schema file
|
||||
schema_file = os.path.join(os.path.dirname(__file__), 'database_schema.sql')
|
||||
with open(schema_file, 'r', encoding='utf-8') as f:
|
||||
sql_script = f.read()
|
||||
|
||||
# Split SQL script by semicolon and execute each statement
|
||||
cursor = connection.cursor()
|
||||
|
||||
# Split by semicolon and filter out empty statements
|
||||
statements = [stmt.strip() for stmt in sql_script.split(';') if stmt.strip()]
|
||||
|
||||
print(f"\nExecuting {len(statements)} SQL statements...")
|
||||
|
||||
executed = 0
|
||||
for i, statement in enumerate(statements, 1):
|
||||
try:
|
||||
# Skip comments and empty lines
|
||||
if not statement or statement.startswith('--'):
|
||||
continue
|
||||
|
||||
cursor.execute(statement)
|
||||
executed += 1
|
||||
|
||||
# Show progress every 10 statements
|
||||
if executed % 10 == 0:
|
||||
print(f" Executed {executed} statements...")
|
||||
|
||||
except Exception as e:
|
||||
print(f" Warning on statement {i}: {str(e)[:100]}")
|
||||
continue
|
||||
|
||||
# Commit changes
|
||||
connection.commit()
|
||||
print(f"\n✓ Successfully executed {executed} SQL statements")
|
||||
|
||||
# Verify tables were created
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'hr_position_system'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
print(f"\n✓ Created {len(tables)} tables:")
|
||||
for table in tables:
|
||||
print(f" - {table['table_name']}")
|
||||
|
||||
# Close connection
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
print("\n✓ Database initialization completed successfully!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ Error initializing database: {str(e)}")
|
||||
return False
|
||||
|
||||
def test_connection():
|
||||
"""Test database connection"""
|
||||
db_config = {
|
||||
'host': os.getenv('DB_HOST'),
|
||||
'port': int(os.getenv('DB_PORT')),
|
||||
'user': os.getenv('DB_USER'),
|
||||
'password': os.getenv('DB_PASSWORD'),
|
||||
'database': os.getenv('DB_NAME')
|
||||
}
|
||||
|
||||
try:
|
||||
connection = pymysql.connect(**db_config)
|
||||
print("✓ Database connection test successful")
|
||||
connection.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Database connection test failed: {str(e)}")
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("HR Position System - Database Initialization")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Test connection first
|
||||
print("Step 1: Testing database connection...")
|
||||
if not test_connection():
|
||||
print("\nPlease check your database configuration in .env file")
|
||||
exit(1)
|
||||
|
||||
print("\nStep 2: Initializing database schema...")
|
||||
if init_database():
|
||||
print("\nDatabase is ready to use!")
|
||||
else:
|
||||
print("\nDatabase initialization failed. Please check the errors above.")
|
||||
exit(1)
|
||||
308
llm_config.py
Normal file
308
llm_config.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
LLM API Configuration Module
|
||||
Supports Gemini, DeepSeek, and OpenAI APIs with connection testing
|
||||
"""
|
||||
|
||||
import os
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class LLMConfig:
|
||||
"""LLM API configuration and management"""
|
||||
|
||||
def __init__(self):
|
||||
self.apis = {
|
||||
'gemini': {
|
||||
'name': 'Google Gemini',
|
||||
'api_key': os.getenv('GEMINI_API_KEY', ''),
|
||||
'endpoint': 'https://generativelanguage.googleapis.com/v1/models',
|
||||
'enabled': bool(os.getenv('GEMINI_API_KEY'))
|
||||
},
|
||||
'deepseek': {
|
||||
'name': 'DeepSeek',
|
||||
'api_key': os.getenv('DEEPSEEK_API_KEY', ''),
|
||||
'endpoint': os.getenv('DEEPSEEK_API_URL', 'https://api.deepseek.com/v1'),
|
||||
'enabled': bool(os.getenv('DEEPSEEK_API_KEY'))
|
||||
},
|
||||
'openai': {
|
||||
'name': 'OpenAI',
|
||||
'api_key': os.getenv('OPENAI_API_KEY', ''),
|
||||
'endpoint': os.getenv('OPENAI_API_URL', 'https://api.openai.com/v1'),
|
||||
'enabled': bool(os.getenv('OPENAI_API_KEY'))
|
||||
}
|
||||
}
|
||||
|
||||
def get_enabled_apis(self) -> List[str]:
|
||||
"""Get list of enabled APIs"""
|
||||
return [key for key, config in self.apis.items() if config['enabled']]
|
||||
|
||||
def get_api_config(self, api_name: str) -> Dict:
|
||||
"""Get configuration for specific API"""
|
||||
return self.apis.get(api_name, {})
|
||||
|
||||
def test_gemini_connection(self) -> Tuple[bool, str]:
|
||||
"""Test Gemini API connection"""
|
||||
try:
|
||||
api_key = self.apis['gemini']['api_key']
|
||||
if not api_key:
|
||||
return False, "API Key 未設定"
|
||||
|
||||
# Test endpoint - list models
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
|
||||
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
models = data.get('models', [])
|
||||
model_count = len(models)
|
||||
return True, f"連線成功!找到 {model_count} 個可用模型"
|
||||
elif response.status_code == 400:
|
||||
return False, "API Key 格式錯誤"
|
||||
elif response.status_code == 403:
|
||||
return False, "API Key 無效或權限不足"
|
||||
else:
|
||||
return False, f"連線失敗 (HTTP {response.status_code})"
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "連線逾時"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "無法連接到伺服器"
|
||||
except Exception as e:
|
||||
return False, f"錯誤: {str(e)}"
|
||||
|
||||
def test_deepseek_connection(self) -> Tuple[bool, str]:
|
||||
"""Test DeepSeek API connection"""
|
||||
try:
|
||||
api_key = self.apis['deepseek']['api_key']
|
||||
endpoint = self.apis['deepseek']['endpoint']
|
||||
|
||||
if not api_key:
|
||||
return False, "API Key 未設定"
|
||||
|
||||
# Test endpoint - list models
|
||||
url = f"{endpoint}/models"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
models = data.get('data', [])
|
||||
if models:
|
||||
model_count = len(models)
|
||||
return True, f"連線成功!找到 {model_count} 個可用模型"
|
||||
else:
|
||||
return True, "連線成功!"
|
||||
elif response.status_code == 401:
|
||||
return False, "API Key 無效"
|
||||
elif response.status_code == 403:
|
||||
return False, "權限不足"
|
||||
else:
|
||||
return False, f"連線失敗 (HTTP {response.status_code})"
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "連線逾時"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "無法連接到伺服器"
|
||||
except Exception as e:
|
||||
return False, f"錯誤: {str(e)}"
|
||||
|
||||
def test_openai_connection(self) -> Tuple[bool, str]:
|
||||
"""Test OpenAI API connection"""
|
||||
try:
|
||||
api_key = self.apis['openai']['api_key']
|
||||
endpoint = self.apis['openai']['endpoint']
|
||||
|
||||
if not api_key:
|
||||
return False, "API Key 未設定"
|
||||
|
||||
# Test endpoint - list models
|
||||
url = f"{endpoint}/models"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
models = data.get('data', [])
|
||||
model_count = len(models)
|
||||
return True, f"連線成功!找到 {model_count} 個可用模型"
|
||||
elif response.status_code == 401:
|
||||
return False, "API Key 無效"
|
||||
elif response.status_code == 403:
|
||||
return False, "權限不足"
|
||||
else:
|
||||
return False, f"連線失敗 (HTTP {response.status_code})"
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "連線逾時"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "無法連接到伺服器"
|
||||
except Exception as e:
|
||||
return False, f"錯誤: {str(e)}"
|
||||
|
||||
def test_all_connections(self) -> Dict[str, Tuple[bool, str]]:
|
||||
"""Test all configured API connections"""
|
||||
results = {}
|
||||
|
||||
if self.apis['gemini']['enabled']:
|
||||
results['gemini'] = self.test_gemini_connection()
|
||||
|
||||
if self.apis['deepseek']['enabled']:
|
||||
results['deepseek'] = self.test_deepseek_connection()
|
||||
|
||||
if self.apis['openai']['enabled']:
|
||||
results['openai'] = self.test_openai_connection()
|
||||
|
||||
return results
|
||||
|
||||
def generate_text_gemini(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]:
|
||||
"""Generate text using Gemini API"""
|
||||
try:
|
||||
api_key = self.apis['gemini']['api_key']
|
||||
if not api_key:
|
||||
return False, "API Key 未設定"
|
||||
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||
|
||||
data = {
|
||||
"contents": [
|
||||
{
|
||||
"parts": [
|
||||
{"text": prompt}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": max_tokens,
|
||||
"temperature": 0.7
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
text = result['candidates'][0]['content']['parts'][0]['text']
|
||||
return True, text
|
||||
else:
|
||||
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"錯誤: {str(e)}"
|
||||
|
||||
def generate_text_deepseek(self, prompt: str, max_tokens: int = 2000) -> Tuple[bool, str]:
|
||||
"""Generate text using DeepSeek API"""
|
||||
try:
|
||||
api_key = self.apis['deepseek']['api_key']
|
||||
endpoint = self.apis['deepseek']['endpoint']
|
||||
|
||||
if not api_key:
|
||||
return False, "API Key 未設定"
|
||||
|
||||
url = f"{endpoint}/chat/completions"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
text = result['choices'][0]['message']['content']
|
||||
return True, text
|
||||
else:
|
||||
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"錯誤: {str(e)}"
|
||||
|
||||
def generate_text_openai(self, prompt: str, model: str = "gpt-3.5-turbo", max_tokens: int = 2000) -> Tuple[bool, str]:
|
||||
"""Generate text using OpenAI API"""
|
||||
try:
|
||||
api_key = self.apis['openai']['api_key']
|
||||
endpoint = self.apis['openai']['endpoint']
|
||||
|
||||
if not api_key:
|
||||
return False, "API Key 未設定"
|
||||
|
||||
url = f"{endpoint}/chat/completions"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
text = result['choices'][0]['message']['content']
|
||||
return True, text
|
||||
else:
|
||||
return False, f"生成失敗 (HTTP {response.status_code}): {response.text}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"錯誤: {str(e)}"
|
||||
|
||||
|
||||
def main():
|
||||
"""Test script"""
|
||||
print("=" * 60)
|
||||
print("LLM API Configuration Test")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
config = LLMConfig()
|
||||
|
||||
# Show enabled APIs
|
||||
enabled = config.get_enabled_apis()
|
||||
print(f"已啟用的 API: {', '.join(enabled) if enabled else '無'}")
|
||||
print()
|
||||
|
||||
# Test all connections
|
||||
print("測試 API 連線:")
|
||||
print("-" * 60)
|
||||
|
||||
results = config.test_all_connections()
|
||||
|
||||
for api_name, (success, message) in results.items():
|
||||
status = "✓" if success else "✗"
|
||||
api_display_name = config.apis[api_name]['name']
|
||||
print(f"{status} {api_display_name}: {message}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
23
requirements_full.txt
Normal file
23
requirements_full.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# Flask and Web Framework
|
||||
Flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
Werkzeug==3.0.1
|
||||
|
||||
# Database
|
||||
pymysql==1.1.0
|
||||
cryptography==41.0.7
|
||||
|
||||
# Environment Variables
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# HTTP Requests
|
||||
requests==2.31.0
|
||||
|
||||
# Version Control
|
||||
gitpython==3.1.40
|
||||
|
||||
# Development Tools
|
||||
pytest==7.4.3
|
||||
pytest-flask==1.3.0
|
||||
black==23.12.1
|
||||
flake8==7.0.0
|
||||
286
start_server.py
Normal file
286
start_server.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
啟動 Flask 服務器(解決 Windows 編碼問題)
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 設置 UTF-8 編碼
|
||||
if sys.platform == 'win32':
|
||||
import codecs
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
|
||||
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
|
||||
|
||||
# 導入並啟動 app
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
# Import LLM configuration
|
||||
try:
|
||||
from llm_config import LLMConfig
|
||||
llm_config = LLMConfig()
|
||||
LLM_ENABLED = True
|
||||
except ImportError:
|
||||
print("Warning: llm_config not found. LLM features will be disabled.")
|
||||
LLM_ENABLED = False
|
||||
|
||||
app = Flask(__name__, static_folder='.')
|
||||
CORS(app)
|
||||
|
||||
# 模擬資料庫
|
||||
positions_db = {}
|
||||
jobs_db = {}
|
||||
|
||||
# 預設崗位資料
|
||||
default_positions = {
|
||||
"MGR-001": {
|
||||
"id": "MGR-001",
|
||||
"basicInfo": {
|
||||
"positionCode": "MGR-001",
|
||||
"positionName": "管理職-資深班長",
|
||||
"positionCategory": "02",
|
||||
"positionCategoryName": "管理職",
|
||||
"positionNature": "FT",
|
||||
"positionNatureName": "全職",
|
||||
"headcount": "5",
|
||||
"positionLevel": "L3",
|
||||
"effectiveDate": "2001-01-01",
|
||||
"positionDesc": "負責生產線的日常管理與人員調度",
|
||||
"positionRemark": ""
|
||||
},
|
||||
"recruitInfo": {
|
||||
"minEducation": "JC",
|
||||
"requiredGender": "",
|
||||
"salaryRange": "C",
|
||||
"workExperience": "3",
|
||||
"minAge": "25",
|
||||
"maxAge": "45",
|
||||
"jobType": "FT",
|
||||
"recruitPosition": "MGR",
|
||||
"jobTitle": "資深班長",
|
||||
"jobDesc": "",
|
||||
"positionReq": "",
|
||||
"titleReq": "",
|
||||
"majorReq": "",
|
||||
"skillReq": "",
|
||||
"langReq": "",
|
||||
"otherReq": "",
|
||||
"superiorPosition": "",
|
||||
"recruitRemark": ""
|
||||
},
|
||||
"createdAt": "2024-01-01T00:00:00",
|
||||
"updatedAt": "2024-01-01T00:00:00"
|
||||
}
|
||||
}
|
||||
positions_db.update(default_positions)
|
||||
|
||||
# 預設職務資料
|
||||
default_jobs = {
|
||||
"VP-001": {
|
||||
"id": "VP-001",
|
||||
"jobCategoryCode": "MGR",
|
||||
"jobCategoryName": "管理職",
|
||||
"jobCode": "VP-001",
|
||||
"jobName": "副總",
|
||||
"jobNameEn": "Vice President",
|
||||
"jobEffectiveDate": "2001-01-01",
|
||||
"jobHeadcount": 2,
|
||||
"jobSortOrder": 10,
|
||||
"jobRemark": "",
|
||||
"jobLevel": "*保密*",
|
||||
"hasAttendanceBonus": False,
|
||||
"hasHousingAllowance": True,
|
||||
"createdAt": "2024-01-01T00:00:00",
|
||||
"updatedAt": "2024-01-01T00:00:00"
|
||||
}
|
||||
}
|
||||
jobs_db.update(default_jobs)
|
||||
|
||||
# ==================== 靜態頁面 ====================
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_from_directory('.', 'index.html')
|
||||
|
||||
@app.route('/api-test')
|
||||
def api_test_page():
|
||||
return send_from_directory('.', 'api_test.html')
|
||||
|
||||
# ==================== 崗位資料 API ====================
|
||||
|
||||
@app.route('/api/positions', methods=['GET'])
|
||||
def get_positions():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
size = request.args.get('size', 20, type=int)
|
||||
search = request.args.get('search', '', type=str)
|
||||
|
||||
filtered = list(positions_db.values())
|
||||
if search:
|
||||
filtered = [p for p in filtered
|
||||
if search.lower() in p['basicInfo'].get('positionCode', '').lower()
|
||||
or search.lower() in p['basicInfo'].get('positionName', '').lower()]
|
||||
|
||||
total = len(filtered)
|
||||
start = (page - 1) * size
|
||||
end = start + size
|
||||
paginated = filtered[start:end]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': paginated,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'size': size,
|
||||
'total': total,
|
||||
'totalPages': (total + size - 1) // size
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/positions/<position_id>', methods=['GET'])
|
||||
def get_position(position_id):
|
||||
if position_id not in positions_db:
|
||||
return jsonify({'success': False, 'error': '找不到該崗位資料'}), 404
|
||||
return jsonify({'success': True, 'data': positions_db[position_id]})
|
||||
|
||||
@app.route('/api/positions', methods=['POST'])
|
||||
def create_position():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': '請提供有效的 JSON 資料'}), 400
|
||||
|
||||
basic_info = data.get('basicInfo', {})
|
||||
if not basic_info.get('positionCode'):
|
||||
return jsonify({'success': False, 'error': '崗位編號為必填欄位'}), 400
|
||||
if not basic_info.get('positionName'):
|
||||
return jsonify({'success': False, 'error': '崗位名稱為必填欄位'}), 400
|
||||
|
||||
position_code = basic_info['positionCode']
|
||||
if position_code in positions_db:
|
||||
return jsonify({'success': False, 'error': f'崗位編號 {position_code} 已存在'}), 409
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
new_position = {
|
||||
'id': position_code,
|
||||
'basicInfo': basic_info,
|
||||
'recruitInfo': data.get('recruitInfo', {}),
|
||||
'createdAt': now,
|
||||
'updatedAt': now
|
||||
}
|
||||
positions_db[position_code] = new_position
|
||||
return jsonify({'success': True, 'message': '崗位資料新增成功', 'data': new_position}), 201
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'新增失敗: {str(e)}'}), 500
|
||||
|
||||
# ==================== LLM API ====================
|
||||
|
||||
@app.route('/api/llm/config', methods=['GET'])
|
||||
def get_llm_config():
|
||||
if not LLM_ENABLED:
|
||||
return jsonify({'success': False, 'error': 'LLM 功能未啟用'}), 503
|
||||
|
||||
try:
|
||||
config_data = {}
|
||||
for api_name, api_config in llm_config.apis.items():
|
||||
config_data[api_name] = {
|
||||
'name': api_config['name'],
|
||||
'enabled': api_config['enabled'],
|
||||
'endpoint': api_config['endpoint'],
|
||||
'api_key': api_config['api_key'][:8] + '...' if api_config['api_key'] else ''
|
||||
}
|
||||
return jsonify(config_data)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'獲取配置失敗: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/llm/test/<api_name>', methods=['GET'])
|
||||
def test_llm_api(api_name):
|
||||
if not LLM_ENABLED:
|
||||
return jsonify({'success': False, 'message': 'LLM 功能未啟用'}), 503
|
||||
|
||||
try:
|
||||
if api_name not in llm_config.apis:
|
||||
return jsonify({'success': False, 'message': f'不支援的 API: {api_name}'}), 400
|
||||
|
||||
if api_name == 'gemini':
|
||||
success, message = llm_config.test_gemini_connection()
|
||||
elif api_name == 'deepseek':
|
||||
success, message = llm_config.test_deepseek_connection()
|
||||
elif api_name == 'openai':
|
||||
success, message = llm_config.test_openai_connection()
|
||||
else:
|
||||
return jsonify({'success': False, 'message': f'未實作的 API: {api_name}'}), 400
|
||||
|
||||
return jsonify({'success': success, 'message': message})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'測試失敗: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/llm/generate', methods=['POST'])
|
||||
def generate_llm_text():
|
||||
if not LLM_ENABLED:
|
||||
return jsonify({'success': False, 'error': 'LLM 功能未啟用'}), 503
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': '請提供有效的 JSON 資料'}), 400
|
||||
|
||||
api_name = data.get('api', 'gemini')
|
||||
prompt = data.get('prompt', '')
|
||||
max_tokens = data.get('max_tokens', 2000)
|
||||
|
||||
if not prompt:
|
||||
return jsonify({'success': False, 'error': '請提供提示詞'}), 400
|
||||
|
||||
if api_name == 'gemini':
|
||||
success, result = llm_config.generate_text_gemini(prompt, max_tokens)
|
||||
elif api_name == 'deepseek':
|
||||
success, result = llm_config.generate_text_deepseek(prompt, max_tokens)
|
||||
elif api_name == 'openai':
|
||||
model = data.get('model', 'gpt-3.5-turbo')
|
||||
success, result = llm_config.generate_text_openai(prompt, model, max_tokens)
|
||||
else:
|
||||
return jsonify({'success': False, 'error': f'不支援的 API: {api_name}'}), 400
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True, 'text': result})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': result}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'生成失敗: {str(e)}'}), 500
|
||||
|
||||
# ==================== 錯誤處理 ====================
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return jsonify({'success': False, 'error': '找不到請求的資源'}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return jsonify({'success': False, 'error': '伺服器內部錯誤'}), 500
|
||||
|
||||
# ==================== 主程式 ====================
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("HR Position System - Flask Backend")
|
||||
print("=" * 60)
|
||||
print("\nServer starting...")
|
||||
print("URL: http://localhost:5000")
|
||||
print()
|
||||
|
||||
if LLM_ENABLED:
|
||||
print("[OK] LLM features enabled")
|
||||
enabled_apis = llm_config.get_enabled_apis()
|
||||
if enabled_apis:
|
||||
print(f"Configured APIs: {', '.join(enabled_apis)}")
|
||||
else:
|
||||
print("Warning: No LLM API Keys configured")
|
||||
else:
|
||||
print("[!] LLM features disabled (llm_config.py not found)")
|
||||
|
||||
print("\nPress Ctrl+C to stop")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
Reference in New Issue
Block a user