251202更新
163
utils/client.py
|
|
@ -87,6 +87,169 @@ class MySQLClient:
|
||||||
raise RuntimeError("执行SQL查询并返回DATAFRAME发生其它异常")
|
raise RuntimeError("执行SQL查询并返回DATAFRAME发生其它异常")
|
||||||
|
|
||||||
|
|
||||||
|
class CacheClient:
|
||||||
|
"""缓存客户端"""
|
||||||
|
|
||||||
|
def __init__(self, cache_ttl: int = 360, database: str = "Caches.db"):
|
||||||
|
"""
|
||||||
|
:param cache_ttl: 缓存生存时间(单位:天),默认为360天
|
||||||
|
:param database: 缓存数据库名称
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.cache_ttl = cache_ttl
|
||||||
|
|
||||||
|
# 创建缓存数据库连接
|
||||||
|
self.connection = sqlite3.connect(
|
||||||
|
database=database,
|
||||||
|
check_same_thread=False,
|
||||||
|
timeout=30, # 缓存数据库锁超时时间(单位:秒),默认为30秒,避免并发锁死
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建缓存数据库连接(使用SQLite)
|
||||||
|
self.cache_connection = sqlite3.connect(
|
||||||
|
database="SQLite.db", check_same_thread=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建缓存表
|
||||||
|
self.connection.execute(
|
||||||
|
"""CREATE TABLE IF NOT EXISTS caches (
|
||||||
|
guid TEXT PRIMARY KEY,
|
||||||
|
scene TEXT,
|
||||||
|
cache TEXT NOT NULL,
|
||||||
|
timestamp REAL NOT NULL
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建时间戳索引(优化过期缓存查询效率)
|
||||||
|
self.connection.execute(
|
||||||
|
"""CREATE INDEX IF NOT EXISTS index_timestamp ON caches(timestamp)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# 删除过期缓存
|
||||||
|
self.connection.execute(
|
||||||
|
"DELETE FROM caches WHERE timestamp < ?",
|
||||||
|
(time.time() - self.cache_ttl * 86400,),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 提交事务
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
except Exception as exception:
|
||||||
|
if self.connection:
|
||||||
|
self.connection.close()
|
||||||
|
raise f"{str(exception)}" from exception
|
||||||
|
|
||||||
|
def _query_response(self, guid: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
私有方法:根据guid查询有效缓存记录(未过期)
|
||||||
|
:param guid: 记录唯一标识
|
||||||
|
:return: 未过期的响应数据(Dict),不存在/过期/异常时返回None
|
||||||
|
"""
|
||||||
|
if not self.cache_connection:
|
||||||
|
logger.error("查询失败:缓存数据库未连接")
|
||||||
|
return None
|
||||||
|
|
||||||
|
with threading.Lock(): # 线程锁,保证并发安全
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
cursor = self.cache_connection.cursor()
|
||||||
|
# 查询条件:guid匹配 + 未过期
|
||||||
|
expire_time = time.time() - self.cache_ttl * 86400
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT response FROM caches WHERE guid = ? AND timestamp >= ?",
|
||||||
|
(guid, expire_time),
|
||||||
|
)
|
||||||
|
result = cursor.fetchone() # 获取单条记录(guid唯一)
|
||||||
|
if result:
|
||||||
|
logger.info(f"查询缓存成功:guid={guid}")
|
||||||
|
return json.loads(result[0]) # JSON字符串转Dict
|
||||||
|
logger.info(f"未查询到有效缓存:guid={guid}(不存在或已过期)")
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(
|
||||||
|
f"缓存数据解析失败(JSON格式错误):guid={guid}", exc_info=True
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询缓存异常:guid={guid}", exc_info=True)
|
||||||
|
self.cache_connection.rollback() # 异常回滚事务
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close() # 确保游标关闭,释放资源
|
||||||
|
|
||||||
|
def _save_response(self, guid: str, response: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
私有方法:添加/更新缓存记录(存在则覆盖,不存在则新增)
|
||||||
|
:param guid: 记录唯一标识
|
||||||
|
:param response: 待保存的响应数据(Dict)
|
||||||
|
:return: 保存成功返回True,失败返回False
|
||||||
|
"""
|
||||||
|
if not self.cache_connection:
|
||||||
|
logger.error("保存失败:缓存数据库未连接")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with threading.Lock(): # 线程锁,保证并发安全
|
||||||
|
cursor = None
|
||||||
|
try:
|
||||||
|
cursor = self.cache_connection.cursor()
|
||||||
|
# 转换Dict为JSON字符串(ensure_ascii=False支持中文)
|
||||||
|
response_str = json.dumps(response, ensure_ascii=False, indent=2)
|
||||||
|
# INSERT OR REPLACE:存在则更新,不存在则插入
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR REPLACE INTO caches (guid, response, timestamp) VALUES (?, ?, ?)",
|
||||||
|
(guid, response_str, time.time()), # timestamp存储当前时间戳
|
||||||
|
)
|
||||||
|
self.cache_connection.commit() # 提交事务
|
||||||
|
logger.info(f"保存缓存成功:guid={guid}")
|
||||||
|
return True
|
||||||
|
except json.JSONEncoderError as e:
|
||||||
|
logger.error(
|
||||||
|
f"缓存数据序列化失败(Dict转JSON错误):guid={guid}", exc_info=True
|
||||||
|
)
|
||||||
|
self.cache_connection.rollback()
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存缓存异常:guid={guid}", exc_info=True)
|
||||||
|
self.cache_connection.rollback() # 异常回滚事务
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close() # 确保游标关闭
|
||||||
|
|
||||||
|
def query_or_save_response(
|
||||||
|
self, guid: str, response: Optional[Dict] = None
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
二合一公开方法:支持查询记录 / 添加/更新记录(灵活复用)
|
||||||
|
:param guid: 记录唯一标识(必填)
|
||||||
|
:param response: 待保存的响应数据(可选):
|
||||||
|
- 不传:仅查询有效记录,返回Dict/None
|
||||||
|
- 传入:添加/更新记录,返回保存后的有效记录/Dict
|
||||||
|
:return: 查询到的记录 / 保存后的记录 / 失败时返回None
|
||||||
|
"""
|
||||||
|
# 参数校验:guid不能为空
|
||||||
|
if not guid or not isinstance(guid, str):
|
||||||
|
logger.error("guid无效:必须是非空字符串")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 仅查询模式(未传入response)
|
||||||
|
if response is None:
|
||||||
|
return self._query_response(guid)
|
||||||
|
|
||||||
|
# 添加/更新模式(传入response):先保存,再查询返回最新记录
|
||||||
|
if self._save_response(guid, response):
|
||||||
|
return self._query_response(guid)
|
||||||
|
logger.error(f"保存缓存失败,无法返回记录:guid={guid}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭数据库连接(程序退出时调用)"""
|
||||||
|
if self.cache_connection:
|
||||||
|
self.cache_connection.close()
|
||||||
|
self.cache_connection = None
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
封装urllib.request的相关操作
|
封装urllib.request的相关操作
|
||||||
使用方法:
|
使用方法:
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 254 KiB |
|
|
@ -1,11 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
普康健康_自动化录入
|
根据现普康票据理赔自动化最小化实现
|
||||||
|
功能清单
|
||||||
--优先使用深圳快瞳,就增值税发票、医疗发票优先使用深圳快瞳票据查验、其次使用深圳快瞳票据识别,最后使用本地识别
|
https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink
|
||||||
--优先考虑增值税发票
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
@ -25,14 +23,32 @@ from jionlp import parse_location
|
||||||
from zen import ZenDecision, ZenEngine
|
from zen import ZenDecision, ZenEngine
|
||||||
|
|
||||||
from utils.client import Authenticator, HTTPClient
|
from utils.client import Authenticator, HTTPClient
|
||||||
from utils.ocr import fuzzy_match
|
|
||||||
|
|
||||||
|
# from utils.ocr import fuzzy_match
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# 工具函数
|
# 封装方法
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def image_read():
|
||||||
|
"""本地读取影像件"""
|
||||||
|
# 影像件读取(默认转为单通道灰度图,实际需下载影像件)
|
||||||
|
image_ndarray = cv2.imread(globals()["image_path"].as_posix(), cv2.IMREAD_GRAYSCALE)
|
||||||
|
if image_ndarray is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 编码为图像字节流
|
||||||
|
# noinspection PyShadowingNames
|
||||||
|
success, image_bytes = cv2.imencode(f".{globals()["image_format"]}", image_ndarray)
|
||||||
|
if not success or image_bytes is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return image_bytes
|
||||||
|
|
||||||
|
|
||||||
def images_compression(**kwargs) -> tuple[str | None, str | None]:
|
def images_compression(**kwargs) -> tuple[str | None, str | None]:
|
||||||
"""影像件压缩并BASE64编码"""
|
"""影像件压缩并BASE64编码"""
|
||||||
|
|
||||||
|
|
@ -1116,7 +1132,7 @@ def disease_diagnosis(**kwargs) -> str | None:
|
||||||
|
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# 主逻辑
|
# 主逻辑部分
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1146,15 +1162,15 @@ if __name__ == "__main__":
|
||||||
# 加载赔案档案模版
|
# 加载赔案档案模版
|
||||||
template = environment.get_template("template.html")
|
template = environment.get_template("template.html")
|
||||||
|
|
||||||
# 遍历工作目录中赔案目录,根据赔案创建赔案档案
|
# 遍历工作目录中赔案目录,根据赔案创建赔案档案(模拟自动化域就待自动化任务创建理赔档案)
|
||||||
for case_path in [
|
for case_path in [
|
||||||
case_path for case_path in directory_path.iterdir() if case_path.is_dir()
|
case_path for case_path in directory_path.iterdir() if case_path.is_dir()
|
||||||
]:
|
]:
|
||||||
|
# 初始化赔案档案(实际报案层包括保险分公司名称、报案渠道、批次号、报案号和报案时间等)
|
||||||
# 初始化赔案档案(简化版本)
|
# 报案渠道包括:保司定义,例如中银项目包括总行和各地分行驻场报案和普康宝自助报案等
|
||||||
dossier = {
|
dossier = {
|
||||||
"报案层": {
|
"报案层": {
|
||||||
"保险分公司": "中银保险有限公司广东分公司",
|
"保险分公司": "中银保险有限公司广东分公司", # 设定:保险分公司
|
||||||
"赔案号": (case_number := case_path.stem), # 设定:赔案目录名称为赔案号
|
"赔案号": (case_number := case_path.stem), # 设定:赔案目录名称为赔案号
|
||||||
},
|
},
|
||||||
"影像件层": [],
|
"影像件层": [],
|
||||||
|
|
@ -1162,30 +1178,41 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
# 遍历赔案目录中影像件地址
|
# 遍历赔案目录中影像件地址
|
||||||
for image_index, image_path in enumerate(
|
for image_index, image_path in enumerate(
|
||||||
sorted(case_path.glob(pattern="*"), key=lambda x: x.stat().st_ctime), 1
|
sorted(
|
||||||
|
[
|
||||||
|
image_path
|
||||||
|
for image_path in case_path.glob(pattern="*")
|
||||||
|
if image_path.is_file()
|
||||||
|
and image_path.suffix.lower() in [".jpg", ".jpeg", ".png"]
|
||||||
|
], # 实际作业亦仅支持JPG、JPEG或PNG
|
||||||
|
key=lambda x: x.name,
|
||||||
|
),
|
||||||
|
1,
|
||||||
):
|
):
|
||||||
dossier["影像件层"].append(
|
# 初始化影像件数据
|
||||||
{
|
image = {
|
||||||
"影像件序号": (image_index := f"{image_index:02d}"),
|
"原始影像件": {
|
||||||
"影像件名称": (image_name := image_path.name),
|
"影像件地址": image_path.as_posix(),
|
||||||
}
|
"影像件名称": (image_name := image_path.stem),
|
||||||
|
"影像件格式": (image_format := image_path.suffix.lower()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 本地读取影像件(实际从影像件服务器下载)
|
||||||
|
image_bytes = image_read()
|
||||||
|
if image_bytes is None:
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
image["影像件唯一标识"] = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 生成影像件唯一标识
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
image["影像件唯一标识"] = (
|
||||||
|
image_guid := hashlib.md5(image_bytes.tobytes()).hexdigest().upper()
|
||||||
)
|
)
|
||||||
|
|
||||||
# 若影像件格式非JPG/JPEG/PNG则跳过该影像件
|
print(image)
|
||||||
if (image_format := image_path.suffix.lower().lstrip(".")) not in [
|
exit()
|
||||||
"jpg",
|
|
||||||
"jpeg",
|
|
||||||
"png",
|
|
||||||
]:
|
|
||||||
dossier["影像件层"][-1]["已分类"] = "否,不支持的影像件"
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 影像件读取
|
|
||||||
image = cv2.imread(image_path.as_posix(), cv2.IMREAD_GRAYSCALE)
|
|
||||||
# 若发生异常则跳过该影像件
|
|
||||||
if image is None:
|
|
||||||
dossier["影像件层"][-1]["已分类"] = "否,读取异常"
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 影像件压缩(输出BASE64编码)
|
# 影像件压缩(输出BASE64编码)
|
||||||
image_guid, image_base64 = images_compression()
|
image_guid, image_base64 = images_compression()
|
||||||
|
|
@ -1200,7 +1227,13 @@ if __name__ == "__main__":
|
||||||
if image_type is None or image_orientation is None:
|
if image_type is None or image_orientation is None:
|
||||||
dossier["影像件层"][-1]["已分类"] = "否,影像件分类异常"
|
dossier["影像件层"][-1]["已分类"] = "否,影像件分类异常"
|
||||||
continue
|
continue
|
||||||
|
#
|
||||||
|
dossier["影像件层"].append(
|
||||||
|
{
|
||||||
|
"影像件序号": (image_index := f"{image_index:02d}"),
|
||||||
|
"影像件名称": (image_name := image_path.name),
|
||||||
|
}
|
||||||
|
)
|
||||||
# 若影像件方向非0度,则影像件旋正并在此压缩
|
# 若影像件方向非0度,则影像件旋正并在此压缩
|
||||||
if image_orientation != "0度":
|
if image_orientation != "0度":
|
||||||
# 影像件旋正
|
# 影像件旋正
|
||||||
|
Can't render this file because it is too large.
|