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
|
||||
import_bindings.py
|
||||
temp_repo.py
|
||||
analyze_*.py
|
||||
final_importer.py
|
||||
ultimate_importer.py
|
||||
*_importer.py
|
||||
|
||||
# Temporary Data/Reports
|
||||
excel_info.txt
|
||||
parsing_output.txt
|
||||
unmatched_permits_report.txt
|
||||
analysis/data/checkpoints/
|
||||
|
||||
# Excel files (except templates)
|
||||
*.xlsx
|
||||
!样表.xlsx
|
||||
审批服务部门.xlsx
|
||||
|
||||
|
|
|
|||
1161
docs/API_V2.md
1161
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 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 (
|
||||
list_permits_for_region,
|
||||
load_permits_and_risks,
|
||||
|
|
@ -94,7 +94,7 @@ def lawrisk_search_v2():
|
|||
result_v2 = search_v2(query, debug_flag, region_filter)
|
||||
|
||||
# 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 []
|
||||
found = bool(risk_subject)
|
||||
|
|
@ -141,7 +141,7 @@ def lawrisk_unbound_permits():
|
|||
"""Get list of permits that are not bound to any theme."""
|
||||
try:
|
||||
permits = list_unbound_permits()
|
||||
return jsonify({"success": True, "data": {"permits": permits}})
|
||||
return jsonify({"success": True, "data": permits})
|
||||
except Exception as exc:
|
||||
print(f"lawrisk_unbound_permits error: {exc}")
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
|
@ -152,7 +152,7 @@ def lawrisk_all_themes():
|
|||
"""Get list of all themes."""
|
||||
try:
|
||||
themes = list_all_themes()
|
||||
return jsonify({"success": True, "data": {"themes": themes}})
|
||||
return jsonify({"success": True, "data": themes})
|
||||
except Exception as exc:
|
||||
print(f"lawrisk_all_themes error: {exc}")
|
||||
return jsonify({"success": False, "message": str(exc)}), 500
|
||||
|
|
@ -1639,3 +1639,88 @@ def admin_permits_filter_options():
|
|||
except Exception as exc:
|
||||
print(f"admin_permits_filter_options error: {exc}")
|
||||
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_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]] = {
|
||||
"permit_name": {
|
||||
"许可事项",
|
||||
|
|
@ -2788,6 +2791,50 @@ def _ensure_permit_theme_override_schema(conn: Optional[pg.Connection] = None) -
|
|||
_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:
|
||||
"""Create a table for auditing user operations."""
|
||||
cur = conn.cursor()
|
||||
|
|
@ -3614,6 +3661,12 @@ def load_permits_and_risks(
|
|||
_ensure_permit_file_schema()
|
||||
except Exception as 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 = """
|
||||
SELECT
|
||||
rtp.theme_id,
|
||||
|
|
@ -3632,11 +3685,13 @@ def load_permits_and_risks(
|
|||
rpd.responsible_contact,
|
||||
rpd.jurisdiction_scope,
|
||||
rpd.filler_name,
|
||||
rpd.unit_name,
|
||||
COALESCE(pad.department_name, rpd.unit_name) AS unit_name,
|
||||
rpd.source_update_date,
|
||||
rpd.contact_info
|
||||
FROM region_permit_details rpd
|
||||
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
|
||||
ON rtp.region_id = rpd.region_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]]:
|
||||
"""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:
|
||||
return []
|
||||
|
||||
|
|
@ -4140,7 +4195,30 @@ def find_permit_contexts_by_name(permit_name: str) -> List[Dict[str, str]]:
|
|||
with _lic_pg_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
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
|
||||
rid = str(region_id)
|
||||
pid = str(permit_id)
|
||||
|
|
|
|||
|
|
@ -2256,48 +2256,6 @@
|
|||
max-height: 600px;
|
||||
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>
|
||||
</head>
|
||||
|
||||
|
|
@ -4391,7 +4349,10 @@
|
|||
|
||||
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue