""" 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 路由必須在 路由之前定義 @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/', 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/', 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/', 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//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 路由必須在 路由之前定義 @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/', 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/', 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/', 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//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/', 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/', 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/', 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/', 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/ - 獲取單一崗位 ║ ║ POST /api/positions - 新增崗位 ║ ║ PUT /api/positions/ - 更新崗位 ║ ║ DELETE /api/positions/ - 刪除崗位 ║ ║ ║ ║ 職務資料 API: ║ ║ GET /api/jobs - 獲取所有職務 ║ ║ GET /api/jobs/ - 獲取單一職務 ║ ║ POST /api/jobs - 新增職務 ║ ║ PUT /api/jobs/ - 更新職務 ║ ║ DELETE /api/jobs/ - 刪除職務 ║ ║ ║ ║ LLM API: ║ ║ GET /api/llm/config - 獲取 LLM 配置 ║ ║ GET /api/llm/test/ - 測試單個 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)