Files
hr-position-system/app.py
DonaldFang 方士碩 b2584772c4 feat: 新增崗位描述與清單整合功能 v2.1
主要功能更新:
- 崗位描述保存功能:保存後資料寫入資料庫
- 崗位清單自動刷新:切換模組時自動載入最新資料
- 崗位清單檢視功能:點擊「檢視」按鈕載入對應描述
- 管理者頁面擴充:新增崗位資料管理與匯出功能
- 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>
2025-12-04 12:46:36 +08:00

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)