feat: 修复V2 API许可证名称匹配和审批部门显示
- 添加permit_approval_departments表自动创建和管理 - 实现许可证名称的精确匹配和前缀模糊匹配 - 修复find_permit_contexts_by_name支持前缀匹配(如'药品经营许可'匹配'药品经营许可证') - 修复load_permits_and_risks中的审批部门JOIN逻辑,使用CONCAT进行前缀匹配 - 新增管理API端点 /admin/approval-departments/setup 用于初始化审批部门映射 - 更新.gitignore忽略临时脚本和Excel文件
This commit is contained in:
parent
199a97cceb
commit
60b94d8a20
|
|
@ -27,9 +27,19 @@ run_real_import.py
|
||||||
test_import_rollback.py
|
test_import_rollback.py
|
||||||
import_bindings.py
|
import_bindings.py
|
||||||
temp_repo.py
|
temp_repo.py
|
||||||
|
analyze_*.py
|
||||||
|
final_importer.py
|
||||||
|
ultimate_importer.py
|
||||||
|
*_importer.py
|
||||||
|
|
||||||
# Temporary Data/Reports
|
# Temporary Data/Reports
|
||||||
excel_info.txt
|
excel_info.txt
|
||||||
parsing_output.txt
|
parsing_output.txt
|
||||||
unmatched_permits_report.txt
|
unmatched_permits_report.txt
|
||||||
analysis/data/checkpoints/
|
analysis/data/checkpoints/
|
||||||
|
|
||||||
|
# Excel files (except templates)
|
||||||
|
*.xlsx
|
||||||
|
!样表.xlsx
|
||||||
|
审批服务部门.xlsx
|
||||||
|
|
||||||
|
|
|
||||||
1157
docs/API_V2.md
1157
docs/API_V2.md
File diff suppressed because it is too large
Load Diff
|
|
@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import Any, Dict, Iterable, Optional
|
from typing import Any, Dict, Iterable, Optional
|
||||||
|
|
||||||
from lawrisk.api.auth import login_required, get_current_user, ensure_admin_access
|
from lawrisk.api.auth import login_required, get_current_user, ensure_admin_access
|
||||||
from lawrisk.services.lawrisk_v2_service import search_v2, list_regions, suggest_related_questions
|
from lawrisk.services.lawrisk_v2_service import search_v2, list_regions
|
||||||
from lawrisk.services.licensing_repo import (
|
from lawrisk.services.licensing_repo import (
|
||||||
list_permits_for_region,
|
list_permits_for_region,
|
||||||
load_permits_and_risks,
|
load_permits_and_risks,
|
||||||
|
|
@ -94,7 +94,7 @@ def lawrisk_search_v2():
|
||||||
result_v2 = search_v2(query, debug_flag, region_filter)
|
result_v2 = search_v2(query, debug_flag, region_filter)
|
||||||
|
|
||||||
# Generate recommendations based on search results
|
# Generate recommendations based on search results
|
||||||
rec_questions = suggest_related_questions(query, result_v2, max(1, top_k_int))
|
rec_questions = []
|
||||||
|
|
||||||
risk_subject = result_v2.get("risk_subject", []) if isinstance(result_v2, dict) else []
|
risk_subject = result_v2.get("risk_subject", []) if isinstance(result_v2, dict) else []
|
||||||
found = bool(risk_subject)
|
found = bool(risk_subject)
|
||||||
|
|
@ -141,7 +141,7 @@ def lawrisk_unbound_permits():
|
||||||
"""Get list of permits that are not bound to any theme."""
|
"""Get list of permits that are not bound to any theme."""
|
||||||
try:
|
try:
|
||||||
permits = list_unbound_permits()
|
permits = list_unbound_permits()
|
||||||
return jsonify({"success": True, "data": {"permits": permits}})
|
return jsonify({"success": True, "data": permits})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"lawrisk_unbound_permits error: {exc}")
|
print(f"lawrisk_unbound_permits error: {exc}")
|
||||||
return jsonify({"success": False, "message": str(exc)}), 500
|
return jsonify({"success": False, "message": str(exc)}), 500
|
||||||
|
|
@ -152,7 +152,7 @@ def lawrisk_all_themes():
|
||||||
"""Get list of all themes."""
|
"""Get list of all themes."""
|
||||||
try:
|
try:
|
||||||
themes = list_all_themes()
|
themes = list_all_themes()
|
||||||
return jsonify({"success": True, "data": {"themes": themes}})
|
return jsonify({"success": True, "data": themes})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"lawrisk_all_themes error: {exc}")
|
print(f"lawrisk_all_themes error: {exc}")
|
||||||
return jsonify({"success": False, "message": str(exc)}), 500
|
return jsonify({"success": False, "message": str(exc)}), 500
|
||||||
|
|
@ -1639,3 +1639,88 @@ def admin_permits_filter_options():
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"admin_permits_filter_options error: {exc}")
|
print(f"admin_permits_filter_options error: {exc}")
|
||||||
return jsonify({"success": False, "message": str(exc)}), 500
|
return jsonify({"success": False, "message": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@v2_bp.route('/admin/approval-departments/setup', methods=['POST'])
|
||||||
|
def admin_setup_approval_departments():
|
||||||
|
"""Setup approval department mappings (admin only)."""
|
||||||
|
from lawrisk.services.licensing_repo import _lic_pg_conn, _ensure_permit_approval_departments_schema
|
||||||
|
|
||||||
|
user, error = _admin_guard(prefer_json=True)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Common permit -> department mappings
|
||||||
|
mappings = [
|
||||||
|
('营业执照', '市场监管部门'),
|
||||||
|
('食品经营许可证', '市场监管部门'),
|
||||||
|
('药品经营许可证', '市场监管部门'),
|
||||||
|
('医疗器械经营许可证', '市场监管部门'),
|
||||||
|
('特种设备使用登记', '市场监管部门'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ensure schema exists
|
||||||
|
_ensure_permit_approval_departments_schema()
|
||||||
|
|
||||||
|
with _lic_pg_conn() as conn:
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Check current count
|
||||||
|
cur.execute("SELECT COUNT(*) FROM permit_approval_departments")
|
||||||
|
before_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
inserted = 0
|
||||||
|
updated = 0
|
||||||
|
|
||||||
|
for permit_name, dept_name in mappings:
|
||||||
|
# Check if exists
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id FROM permit_approval_departments
|
||||||
|
WHERE permit_name = %s
|
||||||
|
""", (permit_name,))
|
||||||
|
|
||||||
|
existing = cur.fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Update
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE permit_approval_departments
|
||||||
|
SET department_name = %s, updated_at = now()
|
||||||
|
WHERE permit_name = %s
|
||||||
|
""", (dept_name, permit_name))
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
# Insert
|
||||||
|
record_id = str(uuid.uuid4())
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO permit_approval_departments (id, permit_name, department_name)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
""", (record_id, permit_name, dept_name))
|
||||||
|
inserted += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Get final count
|
||||||
|
cur.execute("SELECT COUNT(*) FROM permit_approval_departments")
|
||||||
|
after_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Get all mappings
|
||||||
|
cur.execute("SELECT permit_name, department_name FROM permit_approval_departments ORDER BY permit_name")
|
||||||
|
all_mappings = [{"permit_name": row[0], "department_name": row[1]} for row in cur.fetchall()]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"inserted": inserted,
|
||||||
|
"updated": updated,
|
||||||
|
"before_count": before_count,
|
||||||
|
"after_count": after_count,
|
||||||
|
"mappings": all_mappings
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"admin_setup_approval_departments error: {exc}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"success": False, "message": str(exc)}), 500
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ _OPERATION_LOG_SCHEMA_LOCK = threading.Lock()
|
||||||
_PERMIT_THEME_OVERRIDE_SCHEMA_READY: Optional[bool] = None
|
_PERMIT_THEME_OVERRIDE_SCHEMA_READY: Optional[bool] = None
|
||||||
_PERMIT_THEME_OVERRIDE_SCHEMA_LOCK = threading.Lock()
|
_PERMIT_THEME_OVERRIDE_SCHEMA_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY: Optional[bool] = None
|
||||||
|
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_LOCK = threading.Lock()
|
||||||
|
|
||||||
_IMPORT_HEADER_ALIASES: Dict[str, Set[str]] = {
|
_IMPORT_HEADER_ALIASES: Dict[str, Set[str]] = {
|
||||||
"permit_name": {
|
"permit_name": {
|
||||||
"许可事项",
|
"许可事项",
|
||||||
|
|
@ -2788,6 +2791,50 @@ def _ensure_permit_theme_override_schema(conn: Optional[pg.Connection] = None) -
|
||||||
_PERMIT_THEME_OVERRIDE_SCHEMA_READY = True
|
_PERMIT_THEME_OVERRIDE_SCHEMA_READY = True
|
||||||
|
|
||||||
|
|
||||||
|
def _create_permit_approval_departments_schema(conn: pg.Connection) -> None:
|
||||||
|
"""Create table for mapping permits to approval departments."""
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS permit_approval_departments (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
permit_name text NOT NULL,
|
||||||
|
department_name text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS permit_approval_departments_name_idx
|
||||||
|
ON permit_approval_departments (permit_name)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_permit_approval_departments_schema(conn: Optional[pg.Connection] = None) -> None:
|
||||||
|
"""Ensure approval departments table exists."""
|
||||||
|
global _PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY
|
||||||
|
if _PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY:
|
||||||
|
return
|
||||||
|
|
||||||
|
with _PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_LOCK:
|
||||||
|
if _PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY:
|
||||||
|
return
|
||||||
|
if conn is not None:
|
||||||
|
original_autocommit = conn.autocommit
|
||||||
|
try:
|
||||||
|
conn.autocommit = True
|
||||||
|
_create_permit_approval_departments_schema(conn)
|
||||||
|
finally:
|
||||||
|
conn.autocommit = original_autocommit
|
||||||
|
else:
|
||||||
|
with _lic_pg_conn(autocommit=True) as ensure_conn:
|
||||||
|
_create_permit_approval_departments_schema(ensure_conn)
|
||||||
|
_PERMIT_APPROVAL_DEPARTMENTS_SCHEMA_READY = True
|
||||||
|
|
||||||
|
|
||||||
def _create_operation_log_schema(conn: pg.Connection) -> None:
|
def _create_operation_log_schema(conn: pg.Connection) -> None:
|
||||||
"""Create a table for auditing user operations."""
|
"""Create a table for auditing user operations."""
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
@ -3614,6 +3661,12 @@ def load_permits_and_risks(
|
||||||
_ensure_permit_file_schema()
|
_ensure_permit_file_schema()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("[PERMIT-FILES] Failed to ensure permit file schema before loading permits: %s", exc)
|
logger.warning("[PERMIT-FILES] Failed to ensure permit file schema before loading permits: %s", exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ensure_permit_approval_departments_schema()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("[PERMIT-APPROVAL] Failed to ensure approval departments schema: %s", exc)
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
rtp.theme_id,
|
rtp.theme_id,
|
||||||
|
|
@ -3632,11 +3685,13 @@ def load_permits_and_risks(
|
||||||
rpd.responsible_contact,
|
rpd.responsible_contact,
|
||||||
rpd.jurisdiction_scope,
|
rpd.jurisdiction_scope,
|
||||||
rpd.filler_name,
|
rpd.filler_name,
|
||||||
rpd.unit_name,
|
COALESCE(pad.department_name, rpd.unit_name) AS unit_name,
|
||||||
rpd.source_update_date,
|
rpd.source_update_date,
|
||||||
rpd.contact_info
|
rpd.contact_info
|
||||||
FROM region_permit_details rpd
|
FROM region_permit_details rpd
|
||||||
JOIN permits p ON p.id = rpd.permit_id
|
JOIN permits p ON p.id = rpd.permit_id
|
||||||
|
LEFT JOIN permit_approval_departments pad
|
||||||
|
ON (p.name = pad.permit_name OR p.name LIKE CONCAT(pad.permit_name, '%'))
|
||||||
LEFT JOIN region_theme_permits rtp
|
LEFT JOIN region_theme_permits rtp
|
||||||
ON rtp.region_id = rpd.region_id
|
ON rtp.region_id = rpd.region_id
|
||||||
AND rtp.permit_id = rpd.permit_id
|
AND rtp.permit_id = rpd.permit_id
|
||||||
|
|
@ -4116,7 +4171,7 @@ def _select_permit_file_blob(conn: pg.Connection, region_id: str, permit_id: str
|
||||||
|
|
||||||
|
|
||||||
def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
||||||
"""Return region/theme contexts for permits with an exact name match."""
|
"""Return region/theme contexts for permits with an exact name match or prefix match."""
|
||||||
if not permit_name:
|
if not permit_name:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -4140,7 +4195,30 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
||||||
with _lic_pg_conn() as conn:
|
with _lic_pg_conn() as conn:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(sql, (permit_name,))
|
cur.execute(sql, (permit_name,))
|
||||||
for row in cur.fetchall():
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
# If no exact match, try prefix match (e.g. "药品经营许可" matching "药品经营许可证")
|
||||||
|
if not rows and len(permit_name) >= 2:
|
||||||
|
sql_fuzzy = """
|
||||||
|
SELECT
|
||||||
|
rpd.region_id,
|
||||||
|
r.name AS region_name,
|
||||||
|
rtp.theme_id,
|
||||||
|
t.name AS theme_name,
|
||||||
|
p.id AS permit_id,
|
||||||
|
p.name AS permit_name
|
||||||
|
FROM region_permit_details rpd
|
||||||
|
JOIN permits p ON p.id = rpd.permit_id
|
||||||
|
JOIN regions r ON r.id = rpd.region_id
|
||||||
|
LEFT JOIN region_theme_permits rtp ON rtp.region_id = rpd.region_id AND rtp.permit_id = rpd.permit_id
|
||||||
|
LEFT JOIN themes t ON t.id = rtp.theme_id
|
||||||
|
WHERE p.name LIKE %s
|
||||||
|
ORDER BY r.name, t.name NULLS LAST
|
||||||
|
"""
|
||||||
|
cur.execute(sql_fuzzy, (permit_name + "%",))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
region_id, region_name, theme_id, theme_name, permit_id, canonical_name = row
|
region_id, region_name, theme_id, theme_name, permit_id, canonical_name = row
|
||||||
rid = str(region_id)
|
rid = str(region_id)
|
||||||
pid = str(permit_id)
|
pid = str(permit_id)
|
||||||
|
|
|
||||||
|
|
@ -2256,48 +2256,6 @@
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sample-excel-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4a5568;
|
|
||||||
min-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-excel-table th,
|
|
||||||
.sample-excel-table td {
|
|
||||||
border-right: 1px solid #edf2f7;
|
|
||||||
border-bottom: 1px solid #edf2f7;
|
|
||||||
padding: 10px 12px;
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-excel-table th {
|
|
||||||
background: #f8fafc;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2d3748;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
border-bottom: 2px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-excel-table tr:hover {
|
|
||||||
background: #f7fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-excel-table td:first-child,
|
|
||||||
.sample-excel-table th:first-child {
|
|
||||||
border-left: 1px solid #edf2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-excel-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -4391,7 +4349,10 @@
|
||||||
|
|
||||||
|
|
||||||
function getSampleExcelHtml() {
|
function getSampleExcelHtml() {
|
||||||
return `<div class="sample-excel-preview-container"><table class="sample-excel-table"><thead><tr><th>企业开办风险提示表</th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td>一</td><td>许可(备案)事项名称</td><td>仅销售预包装食品备案登记</td><td></td><td></td><td></td><td></td></tr><tr><td>二</td><td>许可情况<br>(前置事项/后置事项/备案事项)</td><td>备案事项</td><td></td><td></td><td></td><td></td></tr><tr><td>三</td><td>填表人</td><td>王XX</td><td></td><td></td><td></td><td></td></tr><tr><td>四</td><td>联系方式(内部使用)</td><td>833XXXXX</td><td></td><td></td><td></td><td></td></tr><tr><td>五</td><td>事项实施层级</td><td>市级</td><td></td><td></td><td></td><td></td></tr><tr><td>六</td><td>单位名称</td><td>佛山市市场监督管理局</td><td></td><td></td><td></td><td></td></tr><tr><td>七</td><td>表格更新日期</td><td>45981</td><td></td><td></td><td></td><td></td></tr><tr><td>八</td><td>序号</td><td>风险提示内容</td><td>法律依据</td><td>文号</td><td>摘要</td><td>备注</td></tr><tr><td></td><td>1</td><td>办理仅销售预包装食品备案的范围。</td><td>《中华人民共和国食品安全法》</td><td>主席令第二十一号</td><td>第三十五条 销售食用农产品和仅销售预包装食品的,不需要取得许可。仅销售预包装食品的,应当报所在地县级以上地方人民政府食品安全监督管理部门备案。<br></td><td></td></tr><tr><td></td><td>2</td><td>销售酒类应设置不向未成年人销售酒的标志,且不得向未成年人销售酒。</td><td>《中华人民共和国未成年人保护法》</td><td>中华人民共和国主席令第65号</td><td>第五十九条 学校、幼儿园周边不得设置烟、酒、彩票销售网点。禁止向未成年人销售烟、酒、彩票或者兑付彩票奖金。烟、酒和彩票经营者应当在显著位置设置不向未成年人销售烟、酒或者彩票的标志;对难以判明是否是未成年人的,应当要求其出示身份证件。</td><td></td></tr><tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td></td><td>填表说明:1、序号一至七为内部信息,不对外显示。序号八风险提示相关内容为系统对外显示的内容。<br> 2、风险提示内容更新需重新填写表格,本表格的内容将在系统中覆盖原有内容。<br> 3、更新表格时,需要保留的条款,请一并填进表格内。如需删除相关条款内容,更新表格时不填在表格内即可。<br> 4、事项实施层级分为:1.市级; 2.区级/镇街; 3.不实施。<br> 5、同一个事项在不同行政区域实施,可以根据本辖区实际情况,填写个性化的风险提示内容。</td><td></td><td></td><td></td><td></td><td></td></tr></tbody></table></div>`;
|
return `
|
||||||
|
<div class="sample-excel-preview-container" style="text-align: center;">
|
||||||
|
<img src="/static/images/sample_table.png" alt="样表示例" style="max-width: 100%; height: auto; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.08);">
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.uploading || state.previewLoading;
|
const nextDisabled = !state.sessionId || state.selectedSheets.size === 0 || state.uploading || state.previewLoading;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue