diff --git a/lawrisk/api/v2.py b/lawrisk/api/v2.py index 7dd3848..461a1e9 100644 --- a/lawrisk/api/v2.py +++ b/lawrisk/api/v2.py @@ -932,20 +932,43 @@ def admin_permit_import_upload(): filename = file_storage.filename or 'import.xlsx' file_bytes = file_storage.read() user = get_current_user() or {} + user_role = user.get("role") + is_super_admin = (user_role == "admin") + uploaded_by = user.get("display_name") or user.get("username") or user.get("id") content_type = file_storage.mimetype or "application/octet-stream" binding_mode = (request.form.get("binding_mode") or "auto").strip().lower() if binding_mode not in {"none", "department", "auto", "municipal", "district"}: binding_mode = "auto" + bound_department_id = (request.form.get("bound_department_id") or "").strip() or None - if not bound_department_id: - bound_department_id = (user.get("department") or {}).get("id") - if bound_department_id and binding_mode == "auto": - # 只要用户绑定了部门,导入时就强制绑定到该部门所属的区域 - # 避免市级管理员上传的数据根据 Sheet 名称意外流向其他区划 - binding_mode = "department" uploader_department_id = (user.get("department") or {}).get("id") + if not is_super_admin: + # Non-super admins are forced to use their own department's region + if not uploader_department_id: + return jsonify({"success": False, "message": "该账号未绑定服务部门,无法执行导入"}), 403 + bound_department_id = uploader_department_id + binding_mode = "department" + else: + # Super admins default to their own department if none specified, but can use 'auto' + if not bound_department_id: + bound_department_id = uploader_department_id + + # If a super admin manually chose a department OR has one and left it as auto, + # but they didn't explicitly ask for 'auto', we might want to default to department. + # However, the user said "super admins can manually specify", so 'auto' vs 'department' + # should probably be respected if they chose it. + # Existing logic forced 'department' if ANY bound_dept was present. + # We relax this for super admins to allow 'auto'. + if bound_department_id and binding_mode == "auto": + # If the super admin didn't explicitly request 'auto' in the form, + # maybe we still want to default to department? + # Actually, usually 'auto' is the frontend default. + # To allow super admin to use 'auto', we only force 'department' if THEY didn't send 'auto'. + # But the form usually sends 'auto'. + pass + if not file_bytes: return jsonify({"success": False, "message": "上传的文件为空"}), 400 @@ -1114,10 +1137,29 @@ def admin_reimport_permit_file(file_id: str): return jsonify({"success": False, "message": "file_id 不能为空"}), 400 user = get_current_user() or {} + user_role = user.get("role") + is_super_admin = (user_role == "admin") + requested_by = user.get("display_name") or user.get("username") or user.get("id") + uploader_department_id = (user.get("department") or {}).get("id") + + binding_mode = "auto" + bound_department_id = None + + if not is_super_admin: + if not uploader_department_id: + return jsonify({"success": False, "message": "该账号未绑定服务部门,无法执行重新导入"}), 403 + bound_department_id = uploader_department_id + binding_mode = "department" try: - data = start_import_session_from_file(file_id, requested_by=requested_by) + data = start_import_session_from_file( + file_id, + requested_by=requested_by, + uploader_department_id=uploader_department_id, + bound_department_id=bound_department_id, + binding_mode=binding_mode + ) return jsonify({"success": True, "data": data}) except ValueError as exc: return jsonify({"success": False, "message": str(exc)}), 404 diff --git a/lawrisk/services/licensing_repo.py b/lawrisk/services/licensing_repo.py index f0ea263..75abb42 100644 --- a/lawrisk/services/licensing_repo.py +++ b/lawrisk/services/licensing_repo.py @@ -1779,6 +1779,26 @@ def describe_permit_import_session(session_id: str) -> Dict[str, Any]: sheet_risk_total += len(permit_rows) total_risks += len(permit_rows) + permit_risks = [] + common_meta = { + "responsible_contact": "", + "jurisdiction_scope": "", + "unit_name": "", + "permit_status": "" + } + + for row in permit_rows: + permit_risks.append({ + "serial_number": row.get("serial_number"), + "risk_content": row.get("risk_content"), + "legal_basis": row.get("legal_basis"), + "summary": row.get("summary"), + "remark": row.get("remark"), + }) + for key in common_meta: + if not common_meta[key] and row.get(key): + common_meta[key] = row.get(key) + # Try to resolve themes via rules (Base Table Logic) resolved_theme_names = _resolve_themes_for_permit(conn, permit_name) @@ -1791,6 +1811,8 @@ def describe_permit_import_session(session_id: str) -> Dict[str, Any]: "is_new": permit_name in new_permits, "default_theme_names": [], "resolved_theme_names": resolved_theme_names, + "risks": permit_risks, + "common_meta": common_meta, "sample_serial": permit_rows[0].get("serial_number") if permit_rows else None, "sample_risk": permit_rows[0].get("risk_content") if permit_rows else "", } @@ -4124,7 +4146,14 @@ def delete_stored_permit_file(file_id: str) -> bool: return cur.rowcount > 0 -def start_import_session_from_file(file_id: str, *, requested_by: Optional[str] = None) -> Dict[str, Any]: +def start_import_session_from_file( + file_id: str, + *, + requested_by: Optional[str] = None, + uploader_department_id: Optional[str] = None, + bound_department_id: Optional[str] = None, + binding_mode: str = "auto", +) -> Dict[str, Any]: """Create a fresh import session using an archived permit file.""" normalized = _clean_text(file_id) if not normalized: @@ -4160,6 +4189,9 @@ def start_import_session_from_file(file_id: str, *, requested_by: Optional[str] filename=filename or "许可导入.xlsx", content_type=content_type or "application/octet-stream", uploaded_by=effective_uploader, + uploader_department_id=uploader_department_id, + bound_department_id=bound_department_id, + binding_mode=binding_mode, ) diff --git a/static/db_admin.html b/static/db_admin.html index 6715323..1dff0ef 100644 --- a/static/db_admin.html +++ b/static/db_admin.html @@ -563,6 +563,79 @@ color: #b91c1c; } + .preview-permit-details-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px 14px; + background: #f8fafc; + padding: 10px; + border-radius: 8px; + font-size: 13px; + } + + .detail-item { + color: #475569; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .detail-item span { + color: #64748b; + font-weight: 500; + } + + .preview-risk-scroll-area { + max-height: 240px; + overflow-y: auto; + border: 1px solid #f1f5f9; + border-radius: 8px; + background: #fff; + padding: 2px; + } + + .preview-risk-item { + padding: 10px; + border-bottom: 1px solid #f8fafc; + display: flex; + gap: 12px; + font-size: 13px; + line-height: 1.5; + } + + .preview-risk-item:last-child { + border-bottom: none; + } + + .risk-serial { + flex-shrink: 0; + width: 24px; + height: 24px; + background: #f1f5f9; + color: #64748b; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + } + + .risk-body { + flex: 1; + } + + .risk-text { + color: #1e293b; + margin-bottom: 4px; + } + + .risk-basis { + font-size: 12px; + color: #64748b; + font-style: italic; + } + .preview-permit-meta { font-size: 12px; color: #4b5563; @@ -1569,7 +1642,7 @@ .preview-permit-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 16px; } @@ -4509,20 +4582,48 @@ const badgeClass = permit.is_duplicate ? 'duplicate' : (permit.is_new ? 'new' : ''); const badgeLabel = permit.is_duplicate ? '覆盖现有' : (permit.is_new ? '新增事项' : '已有事项'); + const meta = permit.common_meta || {}; + const risks = permit.risks || []; + html += `
`; html += '
'; - html += `
${escapeHtml(permit.permit_name)}${badgeLabel}
`; - html += `风险 ${permit.risk_count || 0} 条`; - // Excel themes display removed as they are no longer in the template - if (permit.sample_risk) { - html += `示例风险:${escapeHtml(truncateText(permit.sample_risk, 80))}`; - } + html += `
+ ${escapeHtml(permit.permit_name)} + ${badgeLabel} +
`; + + // Detailed Metadata + html += '
'; + if (meta.unit_name) html += `
单位:${escapeHtml(meta.unit_name)}
`; + if (meta.responsible_contact) html += `
联系人:${escapeHtml(meta.responsible_contact)}
`; + if (meta.permit_status) html += `
许可情况:${escapeHtml(meta.permit_status)}
`; + if (meta.jurisdiction_scope) html += `
管辖范围:${escapeHtml(meta.jurisdiction_scope)}
`; html += '
'; + html += `
共 ${permit.risk_count || 0} 条风险提示
`; + html += '
'; // end preview-permit-header + + // Risk List + if (risks.length > 0) { + html += '
'; + risks.forEach((r, idx) => { + html += ` +
+
${escapeHtml(r.serial_number || (idx + 1))}
+
+
${escapeHtml(r.risk_content)}
+ ${r.legal_basis ? `
依据:${escapeHtml(r.legal_basis)}
` : ''} +
+
+ `; + }); + html += '
'; + } + if (selectedThemes.length) { - html += `
预定绑定主题:${selectedThemes.map(t => `${escapeHtml(t)}`).join('')}
`; + html += `
绑定主题:${selectedThemes.map(t => `${escapeHtml(t)}`).join('')}
`; } else { - html += '
尚未配置自动绑定规则,系统将默认设为“不涉及”。
'; + html += '
尚未配置自动绑定规则,默认设为“不涉及”。
'; } html += '
';