Update auth flows and admin tools
This commit is contained in:
parent
d97c7ca086
commit
26a7b531f3
|
|
@ -16,7 +16,11 @@ from flask import (
|
|||
url_for,
|
||||
)
|
||||
|
||||
from lawrisk.services.auth_service import get_user_by_username, verify_password
|
||||
from lawrisk.services.auth_service import (
|
||||
get_user_by_username,
|
||||
update_user_account,
|
||||
verify_password,
|
||||
)
|
||||
|
||||
|
||||
auth_bp = Blueprint("auth", __name__, url_prefix="/fs-ai-asistant/api/workflow/lawrisk")
|
||||
|
|
@ -233,3 +237,103 @@ def current_user_endpoint() -> Response:
|
|||
if not user:
|
||||
return jsonify({"authenticated": False}), 401
|
||||
return jsonify({"authenticated": True, "user": user})
|
||||
|
||||
|
||||
def _validate_password_complexity(password: str) -> Optional[str]:
|
||||
"""验证密码复杂度:最小8位,必须包含字母和数字"""
|
||||
if len(password) < 8:
|
||||
return "密码长度至少为 8 位"
|
||||
if not (any(c.isalpha() for c in password) and any(c.isdigit() for c in password)):
|
||||
return "密码必须同时包含字母和数字"
|
||||
return None
|
||||
|
||||
|
||||
@auth_bp.post("/v2/auth/change-password")
|
||||
@login_required
|
||||
def change_password_endpoint() -> Response:
|
||||
"""Allow authenticated users to change their own password."""
|
||||
try:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
current_password = (payload.get("current_password", "") if payload else "").strip()
|
||||
new_password = (payload.get("new_password", "") if payload else "").strip()
|
||||
|
||||
# Validate required fields
|
||||
if not current_password:
|
||||
return jsonify({"error": "请输入当前密码"}), 400
|
||||
if not new_password:
|
||||
return jsonify({"error": "请输入新密码"}), 400
|
||||
|
||||
# Get current user from session
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({"error": "用户未登录"}), 401
|
||||
|
||||
print(f"DEBUG: Session user = {user}")
|
||||
|
||||
# Get complete user data including password hash
|
||||
username = user.get("username")
|
||||
if not username:
|
||||
return jsonify({"error": "无效的用户信息"}), 400
|
||||
|
||||
# 使用 get_user_by_username 获取完整用户数据(包括密码哈希)
|
||||
user_data = get_user_by_username(username)
|
||||
if not user_data:
|
||||
return jsonify({"error": "用户不存在"}), 404
|
||||
|
||||
# Debug logging
|
||||
print(f"DEBUG: username = {username}")
|
||||
print(f"DEBUG: password_hash exists = {'password_hash' in user_data}")
|
||||
print(f"DEBUG: password_hash length = {len(user_data.get('password_hash', ''))}")
|
||||
print(f"DEBUG: current_password provided = {bool(current_password)}")
|
||||
|
||||
# Verify current password
|
||||
password_hash = user_data.get("password_hash", "")
|
||||
if not password_hash:
|
||||
print("ERROR: password_hash is empty!")
|
||||
return jsonify({"error": "系统错误:无法获取密码哈希"}), 500
|
||||
|
||||
if not verify_password(current_password, password_hash):
|
||||
print("ERROR: Password verification failed!")
|
||||
print(f"DEBUG: Hash preview: {password_hash[:20]}..." if len(password_hash) > 20 else f"DEBUG: Hash: {password_hash}")
|
||||
return jsonify({"error": "当前密码错误"}), 401
|
||||
|
||||
print("DEBUG: Password verified successfully!")
|
||||
|
||||
# Get user_id for update operation
|
||||
user_id = user_data.get("id")
|
||||
if not user_id:
|
||||
return jsonify({"error": "无法获取用户ID"}), 400
|
||||
|
||||
# Validate new password complexity
|
||||
complexity_error = _validate_password_complexity(new_password)
|
||||
if complexity_error:
|
||||
return jsonify({"error": complexity_error}), 400
|
||||
|
||||
# Check if new password is same as current
|
||||
if verify_password(new_password, user_data.get("password_hash", "")):
|
||||
return jsonify({"error": "新密码不能与当前密码相同"}), 400
|
||||
|
||||
# Update password
|
||||
print(f"DEBUG: Attempting to update password for user_id = {user_id}")
|
||||
update_result = update_user_account(
|
||||
user_id=user_id,
|
||||
password=new_password,
|
||||
)
|
||||
print(f"DEBUG: Update result = {update_result}")
|
||||
|
||||
if not update_result:
|
||||
print("ERROR: Password update returned None/False")
|
||||
return jsonify({"error": "密码修改失败,请稍后重试"}), 500
|
||||
|
||||
# Note: update_user_account already logs the operation internally
|
||||
print("DEBUG: Password change completed successfully")
|
||||
return jsonify({"message": "密码修改成功"}), 200
|
||||
|
||||
except Exception as e:
|
||||
# Log full error details
|
||||
import traceback
|
||||
print(f"ERROR: Exception in change_password_endpoint")
|
||||
print(f"ERROR: Type: {type(e).__name__}")
|
||||
print(f"ERROR: Message: {str(e)}")
|
||||
print(f"ERROR: Traceback:\n{traceback.format_exc()}")
|
||||
return jsonify({"error": "密码修改失败,请稍后重试"}), 500
|
||||
|
|
|
|||
|
|
@ -363,6 +363,7 @@ def get_user_by_id(user_id: str) -> Optional[Dict[str, Any]]:
|
|||
au.role,
|
||||
au.grade,
|
||||
au.is_active,
|
||||
au.password_hash,
|
||||
au.service_department_id,
|
||||
au.department_role,
|
||||
au.created_at,
|
||||
|
|
|
|||
|
|
@ -1089,6 +1089,30 @@
|
|||
background: #5568d3;
|
||||
}
|
||||
|
||||
.btn-change-password {
|
||||
background: white;
|
||||
color: #374151;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-change-password:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
background: #f9fafb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.btn-change-password:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
|
|
@ -1215,6 +1239,32 @@
|
|||
animation: modalFadeIn 0.3s;
|
||||
}
|
||||
|
||||
.password-strength-indicator {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.password-strength-indicator.weak { color: #ef4444; }
|
||||
.password-strength-indicator.medium { color: #f59e0b; }
|
||||
.password-strength-indicator.strong { color: #10b981; }
|
||||
|
||||
.password-error {
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-error.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input.error {
|
||||
border-color: #ef4444 !important;
|
||||
background-color: #fef2f2;
|
||||
}
|
||||
|
||||
.import-modal-content {
|
||||
width: 1200px;
|
||||
max-width: 98vw;
|
||||
|
|
@ -2453,6 +2503,7 @@
|
|||
<div class="user-alert" id="userStatus"></div>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<button type="button" class="btn-change-password" id="changePasswordBtn">修改密码</button>
|
||||
<button type="button" class="btn-logout" id="logoutBtn">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -3005,6 +3056,267 @@
|
|||
});
|
||||
}
|
||||
|
||||
// 密码修改相关
|
||||
function openPasswordModal() {
|
||||
const passwordModal = document.getElementById('passwordModal');
|
||||
if (!passwordModal) return;
|
||||
|
||||
passwordModal.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.getElementById('passwordChangeForm').reset();
|
||||
clearPasswordErrors();
|
||||
updatePasswordStrength('');
|
||||
|
||||
// 绑定模态框内的事件
|
||||
bindPasswordModalEvents();
|
||||
|
||||
setTimeout(() => document.getElementById('currentPassword').focus(), 100);
|
||||
}
|
||||
|
||||
function closePasswordModal() {
|
||||
const passwordModal = document.getElementById('passwordModal');
|
||||
if (!passwordModal) return;
|
||||
|
||||
passwordModal.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
document.getElementById('passwordChangeForm').reset();
|
||||
clearPasswordErrors();
|
||||
updatePasswordStrength('');
|
||||
}
|
||||
|
||||
function clearPasswordErrors() {
|
||||
document.querySelectorAll('.password-error').forEach(el => {
|
||||
el.textContent = '';
|
||||
el.classList.remove('show');
|
||||
});
|
||||
|
||||
// 清除输入框的错误样式
|
||||
document.querySelectorAll('#passwordChangeForm input').forEach(input => {
|
||||
input.classList.remove('error');
|
||||
});
|
||||
}
|
||||
|
||||
function updatePasswordStrength(password) {
|
||||
const indicator = document.getElementById('passwordStrength');
|
||||
if (!password) {
|
||||
indicator.textContent = '';
|
||||
indicator.className = 'password-strength-indicator';
|
||||
return;
|
||||
}
|
||||
let strength = 0;
|
||||
if (password.length >= 8) strength++;
|
||||
if (password.length >= 12) strength++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
||||
if (/\d/.test(password)) strength++;
|
||||
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) strength++;
|
||||
|
||||
if (strength <= 2) {
|
||||
indicator.textContent = '密码强度:弱';
|
||||
indicator.className = 'password-strength-indicator weak';
|
||||
} else if (strength <= 3) {
|
||||
indicator.textContent = '密码强度:中';
|
||||
indicator.className = 'password-strength-indicator medium';
|
||||
} else {
|
||||
indicator.textContent = '密码强度:强';
|
||||
indicator.className = 'password-strength-indicator strong';
|
||||
}
|
||||
}
|
||||
|
||||
function validatePasswordForm() {
|
||||
console.log('=== validatePasswordForm called ===');
|
||||
let isValid = true;
|
||||
const errors = []; // 收集所有错误消息
|
||||
|
||||
clearPasswordErrors();
|
||||
|
||||
const currentPassword = document.getElementById('currentPassword').value.trim();
|
||||
const newPassword = document.getElementById('newPassword').value.trim();
|
||||
const confirmPassword = document.getElementById('confirmPassword').value.trim();
|
||||
|
||||
console.log('currentPassword:', currentPassword ? '***' : '(empty)');
|
||||
console.log('newPassword:', newPassword ? '***' : '(empty)');
|
||||
console.log('confirmPassword:', confirmPassword ? '***' : '(empty)');
|
||||
|
||||
if (!currentPassword) {
|
||||
console.log('Validation failed: current password is empty');
|
||||
showError('currentPasswordError', '请输入当前密码');
|
||||
errors.push('请输入当前密码');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
console.log('Validation failed: new password is empty');
|
||||
showError('newPasswordError', '请输入新密码');
|
||||
errors.push('请输入新密码');
|
||||
isValid = false;
|
||||
} else if (newPassword.length < 8) {
|
||||
console.log('Validation failed: new password too short');
|
||||
showError('newPasswordError', '新密码长度至少为 8 位');
|
||||
errors.push('新密码长度至少为 8 位');
|
||||
isValid = false;
|
||||
} else if (!/(?=.*[a-zA-Z])(?=.*\d)/.test(newPassword)) {
|
||||
console.log('Validation failed: new password complexity');
|
||||
showError('newPasswordError', '新密码必须同时包含字母和数字');
|
||||
errors.push('新密码必须同时包含字母和数字');
|
||||
isValid = false;
|
||||
} else if (newPassword === currentPassword) {
|
||||
console.log('Validation failed: passwords are the same');
|
||||
showError('newPasswordError', '新密码不能与当前密码相同');
|
||||
errors.push('新密码不能与当前密码相同');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!confirmPassword) {
|
||||
console.log('Validation failed: confirm password is empty');
|
||||
showError('confirmPasswordError', '请确认新密码');
|
||||
errors.push('请确认新密码');
|
||||
isValid = false;
|
||||
} else if (newPassword !== confirmPassword) {
|
||||
console.log('Validation failed: passwords do not match');
|
||||
showError('confirmPasswordError', '两次输入的密码不一致');
|
||||
errors.push('两次输入的密码不一致');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
console.log('Validation result:', isValid);
|
||||
console.log('Errors collected:', errors);
|
||||
|
||||
// 如果有错误,在控制台和 alert 中显示
|
||||
if (!isValid && errors.length > 0) {
|
||||
const errorMessage = errors.join('\n• ');
|
||||
console.log('Showing error alert:', errorMessage);
|
||||
// 延迟一点显示 alert,确保 DOM 更新
|
||||
setTimeout(() => {
|
||||
alert('❌ 表单验证失败:\n\n• ' + errorMessage);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
console.log('=== validatePasswordForm end ===');
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function showError(elementId, message) {
|
||||
console.log(`showError called: ${elementId} = "${message}"`);
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) {
|
||||
console.error(`Element not found: ${elementId}`);
|
||||
alert(`错误:${message}`); // 降级到 alert
|
||||
return;
|
||||
}
|
||||
el.textContent = message;
|
||||
el.classList.add('show');
|
||||
|
||||
// 给对应的输入框添加错误样式
|
||||
const inputId = elementId.replace('Error', '');
|
||||
const inputEl = document.getElementById(inputId);
|
||||
if (inputEl) {
|
||||
inputEl.classList.add('error');
|
||||
}
|
||||
|
||||
console.log(`Error shown successfully for ${elementId}`);
|
||||
}
|
||||
|
||||
async function handleSubmitPasswordChange() {
|
||||
console.log('handleSubmitPasswordChange called');
|
||||
|
||||
if (!validatePasswordForm()) {
|
||||
console.log('Form validation failed');
|
||||
// validatePasswordForm 已经显示了详细的错误消息
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Form validation passed');
|
||||
|
||||
const currentPassword = document.getElementById('currentPassword').value.trim();
|
||||
const newPassword = document.getElementById('newPassword').value.trim();
|
||||
const submitPasswordChange = document.getElementById('submitPasswordChange');
|
||||
|
||||
if (!submitPasswordChange) {
|
||||
console.error('Submit button not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting password change request...');
|
||||
|
||||
submitPasswordChange.disabled = true;
|
||||
submitPasswordChange.textContent = '提交中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/fs-ai-asistant/api/workflow/lawrisk/v2/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
const data = await response.json();
|
||||
console.log('Response data:', data);
|
||||
|
||||
if (response.ok) {
|
||||
alert('✅ 密码修改成功!');
|
||||
closePasswordModal();
|
||||
} else {
|
||||
alert(`❌ 密码修改失败:${data.error || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Password change error:', error);
|
||||
alert('❌ 密码修改失败,请稍后重试');
|
||||
} finally {
|
||||
submitPasswordChange.disabled = false;
|
||||
submitPasswordChange.textContent = '确认修改';
|
||||
}
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
const changePasswordBtn = document.getElementById('changePasswordBtn');
|
||||
|
||||
console.log('changePasswordBtn:', changePasswordBtn);
|
||||
|
||||
if (changePasswordBtn) {
|
||||
changePasswordBtn.addEventListener('click', function(e) {
|
||||
console.log('changePasswordBtn clicked');
|
||||
openPasswordModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 延迟绑定模态框内的事件(因为模态框在页面底部)
|
||||
function bindPasswordModalEvents() {
|
||||
const submitPasswordChange = document.getElementById('submitPasswordChange');
|
||||
console.log('bindPasswordModalEvents - submitPasswordChange:', submitPasswordChange);
|
||||
|
||||
if (submitPasswordChange && !submitPasswordChange.hasAttribute('data-bound')) {
|
||||
submitPasswordChange.addEventListener('click', function(e) {
|
||||
console.log('submitPasswordChange clicked');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSubmitPasswordChange();
|
||||
});
|
||||
submitPasswordChange.setAttribute('data-bound', 'true');
|
||||
console.log('submitPasswordChange event bound');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('newPassword')?.addEventListener('input', (e) => {
|
||||
updatePasswordStrength(e.target.value);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const passwordModal = document.getElementById('passwordModal');
|
||||
if (passwordModal && e.target === passwordModal) {
|
||||
closePasswordModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const passwordModal = document.getElementById('passwordModal');
|
||||
if (e.key === 'Escape' && passwordModal && passwordModal.classList.contains('show')) {
|
||||
closePasswordModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 导航状态管理
|
||||
let currentStep = 1; // 1=区划, 2=事项, 3=详情
|
||||
let historyStack = []; // 历史记录栈
|
||||
|
|
@ -8118,6 +8430,40 @@
|
|||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 密码修改模态框 -->
|
||||
<div class="modal" id="passwordModal">
|
||||
<div class="modal-content" style="max-width: 450px;">
|
||||
<div class="modal-header" style="text-align: left; padding: 20px; border-bottom: 1px solid #e5e7eb; margin: 0;">
|
||||
<h3 style="color: #1f2937; font-size: 18px; margin: 0;">修改密码</h3>
|
||||
<button type="button" onclick="closePasswordModal()" style="position: absolute; top: 20px; right: 20px; background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280;">✕</button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 30px;">
|
||||
<form id="passwordChangeForm">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; color: #374151; font-weight: 500; font-size: 14px;">当前密码 *</label>
|
||||
<input type="password" id="currentPassword" name="currentPassword" required style="width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;">
|
||||
<div id="currentPasswordError" class="password-error"></div>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; color: #374151; font-weight: 500; font-size: 14px;">新密码 *</label>
|
||||
<input type="password" id="newPassword" name="newPassword" required style="width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;">
|
||||
<div id="passwordStrength" class="password-strength-indicator"></div>
|
||||
<div id="newPasswordError" class="password-error"></div>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; margin-bottom: 8px; color: #374151; font-weight: 500; font-size: 14px;">确认新密码 *</label>
|
||||
<input type="password" id="confirmPassword" name="confirmPassword" required style="width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;">
|
||||
<div id="confirmPasswordError" class="password-error"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding: 20px; border-top: 1px solid #e5e7eb; display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button type="button" class="btn btn-secondary" onclick="closePasswordModal()">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="submitPasswordChange">确认修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Excel服务部门层级信息补充脚本
|
||||
|
||||
功能:
|
||||
1. 读取Excel文件,为每条记录添加"绑定服务部门"和"服务部门所属部门"两列
|
||||
2. 根据层级规则和区名关键词自动判断上级部门
|
||||
3. 生成统计报告并保存补充后的Excel文件
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
|
||||
# 区名关键词映射表
|
||||
REGION_KEYWORDS = {
|
||||
'禅城区': '禅城区服务部门',
|
||||
'南海区': '南海区服务部门',
|
||||
'顺德区': '顺德区服务部门',
|
||||
'三水区': '三水区服务部门',
|
||||
'高明区': '高明区服务部门'
|
||||
}
|
||||
|
||||
MUNICIPAL_DEPT = '市级服务部门'
|
||||
|
||||
|
||||
def extract_region_service_dept(unit_name):
|
||||
"""
|
||||
从单位名称中提取区级服务部门
|
||||
|
||||
Args:
|
||||
unit_name: 单位名称字符串
|
||||
|
||||
Returns:
|
||||
对应的区级服务部门名称,如果未找到则返回None
|
||||
"""
|
||||
if pd.isna(unit_name):
|
||||
return None
|
||||
|
||||
unit_name_str = str(unit_name)
|
||||
|
||||
# 按优先级顺序匹配区名关键词
|
||||
for keyword, service_dept in REGION_KEYWORDS.items():
|
||||
if keyword in unit_name_str:
|
||||
return service_dept
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def determine_parent_department(unit_name, level_value):
|
||||
"""
|
||||
确定服务部门所属部门
|
||||
|
||||
规则:
|
||||
1. 如果层级值包含"市级" → 市级服务部门
|
||||
2. 如果单位名称包含区名关键词 → 对应区级服务部门
|
||||
3. 否则默认为市级服务部门(兜底策略)
|
||||
|
||||
Args:
|
||||
unit_name: 单位名称
|
||||
level_value: 层级/风险提示/备注列的值
|
||||
|
||||
Returns:
|
||||
服务部门所属部门名称
|
||||
"""
|
||||
# 规则1: 检查层级值是否包含"市级"
|
||||
if pd.notna(level_value) and '市级' in str(level_value):
|
||||
return MUNICIPAL_DEPT
|
||||
|
||||
# 规则2: 从单位名称中提取区级服务部门
|
||||
region_dept = extract_region_service_dept(unit_name)
|
||||
if region_dept:
|
||||
return region_dept
|
||||
|
||||
# 规则3: 兜底策略 - 默认为市级服务部门
|
||||
return MUNICIPAL_DEPT
|
||||
|
||||
|
||||
def supplement_excel_file(input_path, output_path):
|
||||
"""
|
||||
补充Excel文件的部门层级信息
|
||||
|
||||
Args:
|
||||
input_path: 输入Excel文件路径
|
||||
output_path: 输出Excel文件路径
|
||||
"""
|
||||
print(f"[INFO] Reading Excel file: {input_path}")
|
||||
|
||||
# 读取Excel文件
|
||||
try:
|
||||
df = pd.read_excel(input_path)
|
||||
print(f"[OK] Successfully read {len(df)} records")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to read Excel file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 显示原始列名
|
||||
print(f"\n[INFO] Original columns ({len(df.columns)} columns):")
|
||||
for i, col in enumerate(df.columns):
|
||||
print(f" {i+1}. {col}")
|
||||
|
||||
# 添加"绑定服务部门"列(直接复制"单位名称"列)
|
||||
# 假设第二列是"单位名称"
|
||||
unit_name_col = df.columns[1] # 第二列
|
||||
df['绑定服务部门'] = df[unit_name_col]
|
||||
|
||||
# 添加"服务部门所属部门"列(根据规则计算)
|
||||
level_col = df.columns[4] # "层级/风险提示/备注"列(第5列)
|
||||
|
||||
df['服务部门所属部门'] = df.apply(
|
||||
lambda row: determine_parent_department(row[unit_name_col], row[level_col]),
|
||||
axis=1
|
||||
)
|
||||
|
||||
print(f"\n[OK] Added 2 columns:")
|
||||
print(f" - Binding Service Department (copied from '{unit_name_col}')")
|
||||
print(f" - Parent Service Department (calculated based on hierarchy rules)")
|
||||
|
||||
# 生成统计报告
|
||||
print(f"\n[STATS] Parent Service Department Distribution:")
|
||||
parent_dept_counts = Counter(df['服务部门所属部门'])
|
||||
|
||||
# 按标准顺序显示
|
||||
standard_order = [MUNICIPAL_DEPT] + list(REGION_KEYWORDS.values())
|
||||
for dept in standard_order:
|
||||
count = parent_dept_counts.get(dept, 0)
|
||||
percentage = (count / len(df) * 100) if len(df) > 0 else 0
|
||||
print(f" {dept}: {count} 条 ({percentage:.1f}%)")
|
||||
|
||||
# 检查无法匹配的记录
|
||||
unmapped_records = df[
|
||||
(~df[level_col].astype(str).str.contains('市级', na=False)) &
|
||||
(~df[unit_name_col].astype(str).str.contains('|'.join(REGION_KEYWORDS.keys()), na=False))
|
||||
]
|
||||
|
||||
if len(unmapped_records) > 0:
|
||||
print(f"\n[WARNING] Found {len(unmapped_records)} records that could not be matched by region keywords, assigned to Municipal Service Department by default:")
|
||||
print(" Sample records:")
|
||||
for idx, row in unmapped_records.head(5).iterrows():
|
||||
print(f" - {row[unit_name_col]} (层级: {row[level_col]})")
|
||||
if len(unmapped_records) > 5:
|
||||
print(f" ... 还有 {len(unmapped_records) - 5} 条")
|
||||
|
||||
# 保存补充后的Excel文件
|
||||
print(f"\n[SAVE] Saving supplemented Excel file: {output_path}")
|
||||
try:
|
||||
df.to_excel(output_path, index=False, engine='openpyxl')
|
||||
print(f"[OK] Successfully saved {len(df)} records to {output_path}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to save Excel file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 验证数据完整性
|
||||
print(f"\n[VERIFY] Data Integrity Validation:")
|
||||
null_binding = df['绑定服务部门'].isna().sum()
|
||||
null_parent = df['服务部门所属部门'].isna().sum()
|
||||
|
||||
print(f" Total records: {len(df)}")
|
||||
print(f" Null values in Binding Service Department: {null_binding}")
|
||||
print(f" Null values in Parent Service Department: {null_parent}")
|
||||
print(f" Total columns: {len(df.columns)}")
|
||||
|
||||
if null_binding == 0 and null_parent == 0:
|
||||
print(f"[OK] Data integrity validation passed")
|
||||
else:
|
||||
print(f"[WARNING] Found null values, please check data source")
|
||||
|
||||
# 显示示例数据
|
||||
print(f"\n[DATA] Sample data (first 5 rows):")
|
||||
sample_cols = [unit_name_col, level_col, '绑定服务部门', '服务部门所属部门']
|
||||
print(df[sample_cols].head(10).to_string(index=False))
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 定义文件路径
|
||||
base_path = Path(__file__).parent.parent
|
||||
input_file = base_path / 'data' / '法律风险提示管理员名单_合并-20260309.xlsx'
|
||||
output_file = base_path / 'data' / '法律风险提示管理员名单_合并-20260309_supplemented.xlsx'
|
||||
|
||||
print("=" * 80)
|
||||
print("Excel服务部门层级信息补充工具")
|
||||
print("=" * 80)
|
||||
|
||||
# 检查输入文件是否存在
|
||||
if not input_file.exists():
|
||||
print(f"[ERROR] Input file does not exist: {input_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 执行补充处理
|
||||
df = supplement_excel_file(str(input_file), str(output_file))
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("[OK] Processing completed!")
|
||||
print(f"[FILE] Output file: {output_file}")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue