主要功能更新: - 崗位描述保存功能:保存後資料寫入資料庫 - 崗位清單自動刷新:切換模組時自動載入最新資料 - 崗位清單檢視功能:點擊「檢視」按鈕載入對應描述 - 管理者頁面擴充:新增崗位資料管理與匯出功能 - CSV 批次匯入:支援崗位與職務資料批次匯入 後端 API 新增: - Position Description CRUD APIs - Position List Query & Export APIs - CSV Template Download & Import APIs 文件更新: - SDD.md 更新至版本 2.1 - README.md 更新功能說明與版本歷史 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1505 lines
47 KiB
Python
1505 lines
47 KiB
Python
"""
|
|
HR 崗位基礎資料維護系統 - Flask Backend API
|
|
=============================================
|
|
提供崗位資料的 CRUD 操作 API 和 LLM API 整合
|
|
"""
|
|
|
|
from flask import Flask, request, jsonify, send_from_directory, make_response
|
|
from flask_cors import CORS
|
|
from datetime import datetime
|
|
import json
|
|
import os
|
|
import uuid
|
|
import csv
|
|
import io
|
|
import sys
|
|
|
|
# 設定 UTF-8 輸出
|
|
if sys.stdout.encoding != 'utf-8':
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
|
|
# 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)
|
|
|
|
# 模擬資料庫 (實際應用中應使用 MySQL/PostgreSQL)
|
|
positions_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)
|
|
|
|
# 職務資料庫
|
|
jobs_db = {}
|
|
|
|
# 預設職務資料
|
|
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)
|
|
|
|
# 崗位描述資料庫
|
|
position_descriptions_db = {}
|
|
|
|
# 預設崗位描述資料
|
|
default_descriptions = {
|
|
"MGR-001": {
|
|
"id": "MGR-001",
|
|
"positionCode": "MGR-001",
|
|
"positionName": "管理職-資深班長",
|
|
"effectiveDate": "2024-01-01",
|
|
"jobDuties": "1. 生產線管理\n2. 人員調度\n3. 品質監控",
|
|
"requiredSkills": "領導能力、溝通協調、問題解決",
|
|
"workEnvironment": "生產現場",
|
|
"careerPath": "班長 → 組長 → 課長",
|
|
"createdAt": "2024-01-01T00:00:00",
|
|
"updatedAt": "2024-01-01T00:00:00"
|
|
}
|
|
}
|
|
|
|
position_descriptions_db.update(default_descriptions)
|
|
|
|
|
|
# ==================== 靜態頁面 ====================
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""返回主頁面"""
|
|
return send_from_directory('.', 'index.html')
|
|
|
|
|
|
@app.route('/api-test')
|
|
def api_test_page():
|
|
"""返回 API 測試頁面"""
|
|
return send_from_directory('.', 'api_test.html')
|
|
|
|
|
|
# ==================== 崗位資料 API ====================
|
|
|
|
# CSV 路由必須在 <position_id> 路由之前定義
|
|
@app.route('/api/positions/csv-template', methods=['GET'])
|
|
def download_position_csv_template():
|
|
"""下載崗位資料 CSV 範本"""
|
|
# CSV 欄位定義
|
|
headers = [
|
|
'崗位編號*', '崗位名稱*', '崗位類別代碼*', '崗位類別名稱',
|
|
'崗位性質代碼', '崗位性質名稱', '人數', '崗位等級',
|
|
'生效日期', '崗位說明', '崗位備註',
|
|
'最低學歷', '性別要求', '薪資範圍', '工作經驗年數',
|
|
'最低年齡', '最高年齡', '職缺性質', '招募崗位',
|
|
'職稱', '工作說明', '崗位要求', '職稱要求',
|
|
'專業要求', '技能要求', '語言要求', '其他要求',
|
|
'上級崗位', '招募備註'
|
|
]
|
|
|
|
# 範例資料
|
|
example_row = [
|
|
'MGR-001', '管理職-資深班長', '02', '管理職',
|
|
'FT', '全職', '5', 'L3',
|
|
'2024-01-01', '負責生產線的日常管理與人員調度', '',
|
|
'JC', '', 'C', '3',
|
|
'25', '45', 'FT', 'MGR',
|
|
'資深班長', '', '', '',
|
|
'', '', '', '',
|
|
'', ''
|
|
]
|
|
|
|
# 建立 CSV 內容
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(headers)
|
|
writer.writerow(example_row)
|
|
|
|
# 建立回應
|
|
response = make_response(output.getvalue())
|
|
response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig'
|
|
response.headers['Content-Disposition'] = 'attachment; filename=position_template.csv'
|
|
|
|
return response
|
|
|
|
|
|
@app.route('/api/positions/import-csv', methods=['POST'])
|
|
def import_positions_csv():
|
|
"""批次匯入崗位資料 CSV"""
|
|
try:
|
|
if 'file' not in request.files:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '未提供 CSV 檔案'
|
|
}), 400
|
|
|
|
file = request.files['file']
|
|
|
|
if file.filename == '':
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '未選擇檔案'
|
|
}), 400
|
|
|
|
if not file.filename.endswith('.csv'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '檔案格式錯誤,請上傳 CSV 檔案'
|
|
}), 400
|
|
|
|
# 讀取 CSV 內容
|
|
stream = io.StringIO(file.stream.read().decode('utf-8-sig'))
|
|
csv_reader = csv.DictReader(stream)
|
|
|
|
success_count = 0
|
|
error_count = 0
|
|
errors = []
|
|
|
|
for row_num, row in enumerate(csv_reader, start=2):
|
|
try:
|
|
# 驗證必填欄位
|
|
position_code = row.get('崗位編號*', '').strip()
|
|
position_name = row.get('崗位名稱*', '').strip()
|
|
position_category = row.get('崗位類別代碼*', '').strip()
|
|
|
|
if not position_code:
|
|
errors.append(f"第 {row_num} 列: 崗位編號為必填")
|
|
error_count += 1
|
|
continue
|
|
|
|
if not position_name:
|
|
errors.append(f"第 {row_num} 列: 崗位名稱為必填")
|
|
error_count += 1
|
|
continue
|
|
|
|
if not position_category:
|
|
errors.append(f"第 {row_num} 列: 崗位類別代碼為必填")
|
|
error_count += 1
|
|
continue
|
|
|
|
# 檢查是否已存在
|
|
if position_code in positions_db:
|
|
errors.append(f"第 {row_num} 列: 崗位編號 {position_code} 已存在")
|
|
error_count += 1
|
|
continue
|
|
|
|
# 建立崗位資料
|
|
now = datetime.now().isoformat()
|
|
new_position = {
|
|
'id': position_code,
|
|
'basicInfo': {
|
|
'positionCode': position_code,
|
|
'positionName': position_name,
|
|
'positionCategory': position_category,
|
|
'positionCategoryName': row.get('崗位類別名稱', '').strip(),
|
|
'positionNature': row.get('崗位性質代碼', 'FT').strip(),
|
|
'positionNatureName': row.get('崗位性質名稱', '全職').strip(),
|
|
'headcount': row.get('人數', '1').strip(),
|
|
'positionLevel': row.get('崗位等級', 'L3').strip(),
|
|
'effectiveDate': row.get('生效日期', '2024-01-01').strip(),
|
|
'positionDesc': row.get('崗位說明', '').strip(),
|
|
'positionRemark': row.get('崗位備註', '').strip()
|
|
},
|
|
'recruitInfo': {
|
|
'minEducation': row.get('最低學歷', 'BA').strip(),
|
|
'requiredGender': row.get('性別要求', '').strip(),
|
|
'salaryRange': row.get('薪資範圍', 'C').strip(),
|
|
'workExperience': row.get('工作經驗年數', '0').strip(),
|
|
'minAge': row.get('最低年齡', '22').strip(),
|
|
'maxAge': row.get('最高年齡', '50').strip(),
|
|
'jobType': row.get('職缺性質', 'FT').strip(),
|
|
'recruitPosition': row.get('招募崗位', '').strip(),
|
|
'jobTitle': row.get('職稱', '').strip(),
|
|
'jobDesc': row.get('工作說明', '').strip(),
|
|
'positionReq': row.get('崗位要求', '').strip(),
|
|
'titleReq': row.get('職稱要求', '').strip(),
|
|
'majorReq': row.get('專業要求', '').strip(),
|
|
'skillReq': row.get('技能要求', '').strip(),
|
|
'langReq': row.get('語言要求', '').strip(),
|
|
'otherReq': row.get('其他要求', '').strip(),
|
|
'superiorPosition': row.get('上級崗位', '').strip(),
|
|
'recruitRemark': row.get('招募備註', '').strip()
|
|
},
|
|
'createdAt': now,
|
|
'updatedAt': now
|
|
}
|
|
|
|
positions_db[position_code] = new_position
|
|
success_count += 1
|
|
|
|
except Exception as e:
|
|
errors.append(f"第 {row_num} 列: {str(e)}")
|
|
error_count += 1
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'匯入完成: 成功 {success_count} 筆, 失敗 {error_count} 筆',
|
|
'successCount': success_count,
|
|
'errorCount': error_count,
|
|
'errors': errors[:10] # 只返回前 10 個錯誤
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'匯入失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/positions', methods=['GET'])
|
|
def get_positions():
|
|
"""
|
|
獲取所有崗位資料
|
|
Query params:
|
|
- page: 頁碼 (default: 1)
|
|
- size: 每頁筆數 (default: 20)
|
|
- search: 搜尋關鍵字
|
|
"""
|
|
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():
|
|
"""
|
|
新增崗位資料
|
|
Request body: {
|
|
basicInfo: {...},
|
|
recruitInfo: {...}
|
|
}
|
|
"""
|
|
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
|
|
|
|
|
|
@app.route('/api/positions/<position_id>', methods=['PUT'])
|
|
def update_position(position_id):
|
|
"""
|
|
更新崗位資料
|
|
Request body: {
|
|
basicInfo: {...},
|
|
recruitInfo: {...}
|
|
}
|
|
"""
|
|
try:
|
|
if position_id not in positions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該崗位資料'
|
|
}), 404
|
|
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供有效的 JSON 資料'
|
|
}), 400
|
|
|
|
# 更新資料
|
|
existing = positions_db[position_id]
|
|
|
|
if 'basicInfo' in data:
|
|
existing['basicInfo'].update(data['basicInfo'])
|
|
|
|
if 'recruitInfo' in data:
|
|
existing['recruitInfo'].update(data['recruitInfo'])
|
|
|
|
existing['updatedAt'] = datetime.now().isoformat()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '崗位資料更新成功',
|
|
'data': existing
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'更新失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/positions/<position_id>', methods=['DELETE'])
|
|
def delete_position(position_id):
|
|
"""刪除崗位資料"""
|
|
try:
|
|
if position_id not in positions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該崗位資料'
|
|
}), 404
|
|
|
|
deleted = positions_db.pop(position_id)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '崗位資料已刪除',
|
|
'data': deleted
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'刪除失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/positions/<position_id>/change-code', methods=['POST'])
|
|
def change_position_code(position_id):
|
|
"""
|
|
更改崗位編號
|
|
Request body: { newCode: "新編號" }
|
|
"""
|
|
try:
|
|
if position_id not in positions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該崗位資料'
|
|
}), 404
|
|
|
|
data = request.get_json()
|
|
new_code = data.get('newCode')
|
|
|
|
if not new_code:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供新的崗位編號'
|
|
}), 400
|
|
|
|
if new_code in positions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'崗位編號 {new_code} 已存在'
|
|
}), 409
|
|
|
|
# 更新編號
|
|
position = positions_db.pop(position_id)
|
|
position['id'] = new_code
|
|
position['basicInfo']['positionCode'] = new_code
|
|
position['updatedAt'] = datetime.now().isoformat()
|
|
|
|
positions_db[new_code] = position
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'崗位編號已從 {position_id} 更改為 {new_code}',
|
|
'data': position
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'更改失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
# ==================== 職務基礎資料 API ====================
|
|
|
|
# CSV 路由必須在 <job_id> 路由之前定義
|
|
@app.route('/api/jobs/csv-template', methods=['GET'])
|
|
def download_job_csv_template():
|
|
"""下載職務資料 CSV 範本"""
|
|
# CSV 欄位定義
|
|
headers = [
|
|
'職務編號*', '職務類別代碼*', '職務類別名稱', '職務名稱*',
|
|
'職務英文名稱', '生效日期', '人數', '排序',
|
|
'備註', '職級', '全勤獎金', '住宿津貼'
|
|
]
|
|
|
|
# 範例資料
|
|
example_row = [
|
|
'VP-001', 'MGR', '管理職', '副總',
|
|
'Vice President', '2024-01-01', '2', '10',
|
|
'', '*保密*', '否', '是'
|
|
]
|
|
|
|
# 建立 CSV 內容
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(headers)
|
|
writer.writerow(example_row)
|
|
|
|
# 建立回應
|
|
response = make_response(output.getvalue())
|
|
response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig'
|
|
response.headers['Content-Disposition'] = 'attachment; filename=job_template.csv'
|
|
|
|
return response
|
|
|
|
|
|
@app.route('/api/jobs/import-csv', methods=['POST'])
|
|
def import_jobs_csv():
|
|
"""批次匯入職務資料 CSV"""
|
|
try:
|
|
if 'file' not in request.files:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '未提供 CSV 檔案'
|
|
}), 400
|
|
|
|
file = request.files['file']
|
|
|
|
if file.filename == '':
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '未選擇檔案'
|
|
}), 400
|
|
|
|
if not file.filename.endswith('.csv'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '檔案格式錯誤,請上傳 CSV 檔案'
|
|
}), 400
|
|
|
|
# 讀取 CSV 內容
|
|
stream = io.StringIO(file.stream.read().decode('utf-8-sig'))
|
|
csv_reader = csv.DictReader(stream)
|
|
|
|
success_count = 0
|
|
error_count = 0
|
|
errors = []
|
|
|
|
for row_num, row in enumerate(csv_reader, start=2):
|
|
try:
|
|
# 驗證必填欄位
|
|
job_code = row.get('職務編號*', '').strip()
|
|
job_name = row.get('職務名稱*', '').strip()
|
|
job_category_code = row.get('職務類別代碼*', '').strip()
|
|
|
|
if not job_code:
|
|
errors.append(f"第 {row_num} 列: 職務編號為必填")
|
|
error_count += 1
|
|
continue
|
|
|
|
if not job_name:
|
|
errors.append(f"第 {row_num} 列: 職務名稱為必填")
|
|
error_count += 1
|
|
continue
|
|
|
|
if not job_category_code:
|
|
errors.append(f"第 {row_num} 列: 職務類別代碼為必填")
|
|
error_count += 1
|
|
continue
|
|
|
|
# 檢查是否已存在
|
|
if job_code in jobs_db:
|
|
errors.append(f"第 {row_num} 列: 職務編號 {job_code} 已存在")
|
|
error_count += 1
|
|
continue
|
|
|
|
# 建立職務資料
|
|
now = datetime.now().isoformat()
|
|
new_job = {
|
|
'id': job_code,
|
|
'jobCategoryCode': job_category_code,
|
|
'jobCategoryName': row.get('職務類別名稱', '').strip(),
|
|
'jobCode': job_code,
|
|
'jobName': job_name,
|
|
'jobNameEn': row.get('職務英文名稱', '').strip(),
|
|
'jobEffectiveDate': row.get('生效日期', '2024-01-01').strip(),
|
|
'jobHeadcount': int(row.get('人數', '1').strip() or '1'),
|
|
'jobSortOrder': int(row.get('排序', '0').strip() or '0'),
|
|
'jobRemark': row.get('備註', '').strip(),
|
|
'jobLevel': row.get('職級', '').strip(),
|
|
'hasAttendanceBonus': row.get('全勤獎金', '否').strip() in ['是', 'True', 'true', '1'],
|
|
'hasHousingAllowance': row.get('住宿津貼', '否').strip() in ['是', 'True', 'true', '1'],
|
|
'createdAt': now,
|
|
'updatedAt': now
|
|
}
|
|
|
|
jobs_db[job_code] = new_job
|
|
success_count += 1
|
|
|
|
except Exception as e:
|
|
errors.append(f"第 {row_num} 列: {str(e)}")
|
|
error_count += 1
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'匯入完成: 成功 {success_count} 筆, 失敗 {error_count} 筆',
|
|
'successCount': success_count,
|
|
'errorCount': error_count,
|
|
'errors': errors[:10] # 只返回前 10 個錯誤
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'匯入失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/jobs', methods=['GET'])
|
|
def get_jobs():
|
|
"""
|
|
獲取所有職務資料
|
|
Query params:
|
|
- page: 頁碼 (default: 1)
|
|
- size: 每頁筆數 (default: 20)
|
|
- search: 搜尋關鍵字
|
|
- category: 職務類別篩選
|
|
"""
|
|
page = request.args.get('page', 1, type=int)
|
|
size = request.args.get('size', 20, type=int)
|
|
search = request.args.get('search', '', type=str)
|
|
category = request.args.get('category', '', type=str)
|
|
|
|
# 過濾搜尋
|
|
filtered = list(jobs_db.values())
|
|
if search:
|
|
filtered = [
|
|
j for j in filtered
|
|
if search.lower() in j.get('jobCode', '').lower()
|
|
or search.lower() in j.get('jobName', '').lower()
|
|
]
|
|
if category:
|
|
filtered = [j for j in filtered if j.get('jobCategoryCode') == category]
|
|
|
|
# 排序
|
|
filtered.sort(key=lambda x: x.get('jobSortOrder', 0))
|
|
|
|
# 分頁
|
|
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/jobs/<job_id>', methods=['GET'])
|
|
def get_job(job_id):
|
|
"""獲取單一職務資料"""
|
|
if job_id not in jobs_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該職務資料'
|
|
}), 404
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'data': jobs_db[job_id]
|
|
})
|
|
|
|
|
|
@app.route('/api/jobs', methods=['POST'])
|
|
def create_job():
|
|
"""
|
|
新增職務資料
|
|
Request body: {
|
|
jobCategoryCode: "MGR",
|
|
jobCategoryName: "管理職",
|
|
jobCode: "VP-001",
|
|
jobName: "副總",
|
|
...
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供有效的 JSON 資料'
|
|
}), 400
|
|
|
|
# 驗證必填欄位
|
|
if not data.get('jobCode'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '職務編號為必填欄位'
|
|
}), 400
|
|
|
|
if not data.get('jobName'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '職務名稱為必填欄位'
|
|
}), 400
|
|
|
|
if not data.get('jobCategoryCode'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '職務類別為必填欄位'
|
|
}), 400
|
|
|
|
job_code = data['jobCode']
|
|
|
|
# 檢查是否已存在
|
|
if job_code in jobs_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'職務編號 {job_code} 已存在'
|
|
}), 409
|
|
|
|
# 建立新記錄
|
|
now = datetime.now().isoformat()
|
|
new_job = {
|
|
'id': job_code,
|
|
**data,
|
|
'createdAt': now,
|
|
'updatedAt': now
|
|
}
|
|
|
|
jobs_db[job_code] = new_job
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '職務資料新增成功',
|
|
'data': new_job
|
|
}), 201
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'新增失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/jobs/<job_id>', methods=['PUT'])
|
|
def update_job(job_id):
|
|
"""更新職務資料"""
|
|
try:
|
|
if job_id not in jobs_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該職務資料'
|
|
}), 404
|
|
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供有效的 JSON 資料'
|
|
}), 400
|
|
|
|
# 更新資料
|
|
existing = jobs_db[job_id]
|
|
existing.update(data)
|
|
existing['updatedAt'] = datetime.now().isoformat()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '職務資料更新成功',
|
|
'data': existing
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'更新失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/jobs/<job_id>', methods=['DELETE'])
|
|
def delete_job(job_id):
|
|
"""刪除職務資料"""
|
|
try:
|
|
if job_id not in jobs_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該職務資料'
|
|
}), 404
|
|
|
|
deleted = jobs_db.pop(job_id)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '職務資料已刪除',
|
|
'data': deleted
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'刪除失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/jobs/<job_id>/change-code', methods=['POST'])
|
|
def change_job_code(job_id):
|
|
"""
|
|
更改職務編號
|
|
Request body: { newCode: "新編號" }
|
|
"""
|
|
try:
|
|
if job_id not in jobs_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該職務資料'
|
|
}), 404
|
|
|
|
data = request.get_json()
|
|
new_code = data.get('newCode')
|
|
|
|
if not new_code:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供新的職務編號'
|
|
}), 400
|
|
|
|
if new_code in jobs_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'職務編號 {new_code} 已存在'
|
|
}), 409
|
|
|
|
# 更新編號
|
|
job = jobs_db.pop(job_id)
|
|
job['id'] = new_code
|
|
job['jobCode'] = new_code
|
|
job['updatedAt'] = datetime.now().isoformat()
|
|
|
|
jobs_db[new_code] = job
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'職務編號已從 {job_id} 更改為 {new_code}',
|
|
'data': job
|
|
})
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'更改失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
# ==================== 參照資料 API ====================
|
|
|
|
@app.route('/api/reference/categories', methods=['GET'])
|
|
def get_categories():
|
|
"""獲取崗位類別選項"""
|
|
return jsonify({
|
|
'success': True,
|
|
'data': [
|
|
{'code': '01', 'name': '技術職'},
|
|
{'code': '02', 'name': '管理職'},
|
|
{'code': '03', 'name': '業務職'},
|
|
{'code': '04', 'name': '行政職'},
|
|
{'code': '05', 'name': '研發職'},
|
|
{'code': '06', 'name': '生產職'}
|
|
]
|
|
})
|
|
|
|
|
|
@app.route('/api/reference/job-categories', methods=['GET'])
|
|
def get_job_categories():
|
|
"""獲取職務類別選項"""
|
|
return jsonify({
|
|
'success': True,
|
|
'data': [
|
|
{'code': 'MGR', 'name': '管理職'},
|
|
{'code': 'TECH', 'name': '技術職'},
|
|
{'code': 'SALE', 'name': '業務職'},
|
|
{'code': 'ADMIN', 'name': '行政職'},
|
|
{'code': 'RD', 'name': '研發職'},
|
|
{'code': 'PROD', 'name': '生產職'}
|
|
]
|
|
})
|
|
|
|
|
|
@app.route('/api/reference/natures', methods=['GET'])
|
|
def get_natures():
|
|
"""獲取崗位性質選項"""
|
|
return jsonify({
|
|
'success': True,
|
|
'data': [
|
|
{'code': 'FT', 'name': '全職'},
|
|
{'code': 'PT', 'name': '兼職'},
|
|
{'code': 'CT', 'name': '約聘'},
|
|
{'code': 'IN', 'name': '實習'},
|
|
{'code': 'DP', 'name': '派遣'}
|
|
]
|
|
})
|
|
|
|
|
|
@app.route('/api/reference/education', methods=['GET'])
|
|
def get_education():
|
|
"""獲取學歷選項"""
|
|
return jsonify({
|
|
'success': True,
|
|
'data': [
|
|
{'code': 'HS', 'name': '高中/職'},
|
|
{'code': 'JC', 'name': '專科'},
|
|
{'code': 'BA', 'name': '大學'},
|
|
{'code': 'MA', 'name': '碩士'},
|
|
{'code': 'PHD', 'name': '博士'}
|
|
]
|
|
})
|
|
|
|
|
|
@app.route('/api/reference/majors', methods=['GET'])
|
|
def get_majors():
|
|
"""獲取專業選項"""
|
|
return jsonify({
|
|
'success': True,
|
|
'data': [
|
|
{'code': 'CS', 'name': '資訊工程'},
|
|
{'code': 'EE', 'name': '電機電子'},
|
|
{'code': 'ME', 'name': '機械工程'},
|
|
{'code': 'BA', 'name': '企業管理'},
|
|
{'code': 'ACC', 'name': '會計財務'},
|
|
{'code': 'HR', 'name': '人力資源'},
|
|
{'code': 'MKT', 'name': '行銷企劃'},
|
|
{'code': 'LAW', 'name': '法律'},
|
|
{'code': 'CHE', 'name': '化學工程'},
|
|
{'code': 'IE', 'name': '工業工程'}
|
|
]
|
|
})
|
|
|
|
|
|
# ==================== 崗位描述 API ====================
|
|
|
|
@app.route('/api/position-descriptions', methods=['GET'])
|
|
def get_position_descriptions():
|
|
"""獲取所有崗位描述"""
|
|
return jsonify({
|
|
'success': True,
|
|
'data': list(position_descriptions_db.values())
|
|
})
|
|
|
|
|
|
@app.route('/api/position-descriptions/<position_code>', methods=['GET'])
|
|
def get_position_description(position_code):
|
|
"""獲取單一崗位描述"""
|
|
if position_code not in position_descriptions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該崗位描述'
|
|
}), 404
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'data': position_descriptions_db[position_code]
|
|
})
|
|
|
|
|
|
@app.route('/api/position-descriptions', methods=['POST'])
|
|
def create_position_description():
|
|
"""新增或更新崗位描述"""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供有效的 JSON 資料'
|
|
}), 400
|
|
|
|
position_code = data.get('positionCode')
|
|
if not position_code:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '崗位編號為必填欄位'
|
|
}), 400
|
|
|
|
# 檢查崗位是否存在
|
|
if position_code not in positions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'崗位編號 {position_code} 不存在,請先建立崗位基礎資料'
|
|
}), 404
|
|
|
|
now = datetime.now().isoformat()
|
|
|
|
# 如果已存在則更新,否則新增
|
|
if position_code in position_descriptions_db:
|
|
position_descriptions_db[position_code].update({
|
|
**data,
|
|
'updatedAt': now
|
|
})
|
|
message = '崗位描述更新成功'
|
|
else:
|
|
position_descriptions_db[position_code] = {
|
|
'id': position_code,
|
|
**data,
|
|
'createdAt': now,
|
|
'updatedAt': now
|
|
}
|
|
message = '崗位描述新增成功'
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': message,
|
|
'data': position_descriptions_db[position_code]
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'操作失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/position-descriptions/<position_code>', methods=['PUT'])
|
|
def update_position_description(position_code):
|
|
"""更新崗位描述"""
|
|
try:
|
|
if position_code not in position_descriptions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該崗位描述'
|
|
}), 404
|
|
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '請提供有效的 JSON 資料'
|
|
}), 400
|
|
|
|
position_descriptions_db[position_code].update({
|
|
**data,
|
|
'updatedAt': datetime.now().isoformat()
|
|
})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '崗位描述更新成功',
|
|
'data': position_descriptions_db[position_code]
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'更新失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/position-descriptions/<position_code>', methods=['DELETE'])
|
|
def delete_position_description(position_code):
|
|
"""刪除崗位描述"""
|
|
try:
|
|
if position_code not in position_descriptions_db:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': '找不到該崗位描述'
|
|
}), 404
|
|
|
|
deleted = position_descriptions_db.pop(position_code)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': '崗位描述已刪除',
|
|
'data': deleted
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'刪除失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
# ==================== 崗位清單 API ====================
|
|
|
|
@app.route('/api/position-list', methods=['GET'])
|
|
def get_position_list():
|
|
"""
|
|
獲取崗位清單 (結合崗位基礎資料和崗位描述)
|
|
Query params:
|
|
- page: 頁碼 (default: 1)
|
|
- size: 每頁筆數 (default: 20)
|
|
- search: 搜尋關鍵字
|
|
"""
|
|
page = request.args.get('page', 1, type=int)
|
|
size = request.args.get('size', 20, type=int)
|
|
search = request.args.get('search', '', type=str)
|
|
|
|
# 組合崗位資料和描述
|
|
combined_list = []
|
|
for position_code, position in positions_db.items():
|
|
description = position_descriptions_db.get(position_code, {})
|
|
|
|
combined = {
|
|
'positionCode': position_code,
|
|
'positionName': position['basicInfo'].get('positionName', ''),
|
|
'positionCategory': position['basicInfo'].get('positionCategoryName', ''),
|
|
'positionNature': position['basicInfo'].get('positionNatureName', ''),
|
|
'headcount': position['basicInfo'].get('headcount', ''),
|
|
'positionLevel': position['basicInfo'].get('positionLevel', ''),
|
|
'effectiveDate': position['basicInfo'].get('effectiveDate', ''),
|
|
'minEducation': position['recruitInfo'].get('minEducation', ''),
|
|
'salaryRange': position['recruitInfo'].get('salaryRange', ''),
|
|
'hasDescription': position_code in position_descriptions_db,
|
|
'jobDuties': description.get('jobDuties', ''),
|
|
'requiredSkills': description.get('requiredSkills', ''),
|
|
'workEnvironment': description.get('workEnvironment', ''),
|
|
'createdAt': position.get('createdAt', ''),
|
|
'updatedAt': position.get('updatedAt', '')
|
|
}
|
|
combined_list.append(combined)
|
|
|
|
# 過濾搜尋
|
|
if search:
|
|
combined_list = [
|
|
item for item in combined_list
|
|
if search.lower() in item['positionCode'].lower()
|
|
or search.lower() in item['positionName'].lower()
|
|
]
|
|
|
|
# 分頁
|
|
total = len(combined_list)
|
|
start = (page - 1) * size
|
|
end = start + size
|
|
paginated = combined_list[start:end]
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'data': paginated,
|
|
'pagination': {
|
|
'page': page,
|
|
'size': size,
|
|
'total': total,
|
|
'totalPages': (total + size - 1) // size
|
|
}
|
|
})
|
|
|
|
|
|
@app.route('/api/position-list/export', methods=['GET'])
|
|
def export_position_list():
|
|
"""匯出完整崗位清單為 CSV"""
|
|
try:
|
|
# CSV 欄位定義
|
|
headers = [
|
|
'崗位編號', '崗位名稱', '崗位類別', '崗位性質', '人數', '崗位等級',
|
|
'生效日期', '最低學歷', '薪資範圍', '工作職責', '所需技能', '工作環境',
|
|
'建立時間', '更新時間'
|
|
]
|
|
|
|
# 組合所有崗位資料
|
|
rows = []
|
|
for position_code, position in positions_db.items():
|
|
description = position_descriptions_db.get(position_code, {})
|
|
|
|
row = [
|
|
position_code,
|
|
position['basicInfo'].get('positionName', ''),
|
|
position['basicInfo'].get('positionCategoryName', ''),
|
|
position['basicInfo'].get('positionNatureName', ''),
|
|
position['basicInfo'].get('headcount', ''),
|
|
position['basicInfo'].get('positionLevel', ''),
|
|
position['basicInfo'].get('effectiveDate', ''),
|
|
position['recruitInfo'].get('minEducation', ''),
|
|
position['recruitInfo'].get('salaryRange', ''),
|
|
description.get('jobDuties', '').replace('\n', ' | '),
|
|
description.get('requiredSkills', ''),
|
|
description.get('workEnvironment', ''),
|
|
position.get('createdAt', ''),
|
|
position.get('updatedAt', '')
|
|
]
|
|
rows.append(row)
|
|
|
|
# 建立 CSV 內容
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(headers)
|
|
writer.writerows(rows)
|
|
|
|
# 建立回應
|
|
response = make_response(output.getvalue())
|
|
response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig'
|
|
response.headers['Content-Disposition'] = f'attachment; filename=position_list_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
|
|
|
|
return response
|
|
|
|
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():
|
|
"""獲取 LLM API 配置狀態"""
|
|
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):
|
|
"""測試單個 LLM API 連線"""
|
|
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/test-all', methods=['GET'])
|
|
def test_all_llm_apis():
|
|
"""測試所有已配置的 LLM API"""
|
|
if not LLM_ENABLED:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'LLM 功能未啟用'
|
|
}), 503
|
|
|
|
try:
|
|
results = llm_config.test_all_connections()
|
|
|
|
response = {}
|
|
for api_name, (success, message) in results.items():
|
|
response[api_name] = {
|
|
'success': success,
|
|
'message': message
|
|
}
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'results': response
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': f'測試失敗: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/llm/generate', methods=['POST'])
|
|
def generate_llm_text():
|
|
"""
|
|
使用 LLM API 生成文字
|
|
Request body: {
|
|
"api": "gemini" | "deepseek" | "openai",
|
|
"prompt": "提示詞",
|
|
"max_tokens": 2000
|
|
}
|
|
"""
|
|
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("""
|
|
╔══════════════════════════════════════════════════════════════╗
|
|
║ HR 基礎資料維護系統 - Flask Backend ║
|
|
╠══════════════════════════════════════════════════════════════╣
|
|
║ 伺服器啟動中... ║
|
|
║ 訪問網址: http://localhost:5000 ║
|
|
║ ║
|
|
║ 主要頁面: ║
|
|
║ / - 主頁面 ║
|
|
║ /api-test - LLM API 測試頁面 ║
|
|
║ ║
|
|
║ 崗位資料 API: ║
|
|
║ GET /api/positions - 獲取所有崗位 ║
|
|
║ GET /api/positions/<id> - 獲取單一崗位 ║
|
|
║ POST /api/positions - 新增崗位 ║
|
|
║ PUT /api/positions/<id> - 更新崗位 ║
|
|
║ DELETE /api/positions/<id> - 刪除崗位 ║
|
|
║ ║
|
|
║ 職務資料 API: ║
|
|
║ GET /api/jobs - 獲取所有職務 ║
|
|
║ GET /api/jobs/<id> - 獲取單一職務 ║
|
|
║ POST /api/jobs - 新增職務 ║
|
|
║ PUT /api/jobs/<id> - 更新職務 ║
|
|
║ DELETE /api/jobs/<id> - 刪除職務 ║
|
|
║ ║
|
|
║ LLM API: ║
|
|
║ GET /api/llm/config - 獲取 LLM 配置 ║
|
|
║ GET /api/llm/test/<api> - 測試單個 API ║
|
|
║ GET /api/llm/test-all - 測試所有 API ║
|
|
║ POST /api/llm/generate - 生成文字 ║
|
|
║ ║
|
|
║ 按 Ctrl+C 停止伺服器 ║
|
|
╚══════════════════════════════════════════════════════════════╝
|
|
""")
|
|
|
|
if LLM_ENABLED:
|
|
print("[OK] LLM 功能已啟用")
|
|
enabled_apis = llm_config.get_enabled_apis()
|
|
if enabled_apis:
|
|
print(f" 已配置的 API: {', '.join(enabled_apis)}")
|
|
else:
|
|
print(" 警告: 尚未配置任何 LLM API Key")
|
|
else:
|
|
print("[!] LLM 功能未啟用 (llm_config.py 未找到)")
|
|
|
|
print()
|
|
app.run(host='0.0.0.0', port=5000, debug=True)
|