251202更新

This commit is contained in:
liubiren 2025-12-02 18:08:46 +08:00
parent c339fdcd9b
commit 093444f590
23 changed files with 230 additions and 34 deletions

View File

@ -87,6 +87,169 @@ class MySQLClient:
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的相关操作
使用方法

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

Before

Width:  |  Height:  |  Size: 430 KiB

After

Width:  |  Height:  |  Size: 430 KiB

View File

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 223 KiB

View File

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 263 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View File

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

View File

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 263 KiB

View File

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

View File

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 262 KiB

View File

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View File

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 256 KiB

View File

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""
普康健康_自动化录入
--优先使用深圳快瞳就增值税发票医疗发票优先使用深圳快瞳票据查验其次使用深圳快瞳票据识别最后使用本地识别
--优先考虑增值税发票
根据现普康票据理赔自动化最小化实现
功能清单
https://liubiren.feishu.cn/docx/WFjTdBpzroUjQvxxrNIcKvGnneh?from=from_copylink
"""
import hashlib
import json
@ -25,14 +23,32 @@ from jionlp import parse_location
from zen import ZenDecision, ZenEngine
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]:
"""影像件压缩并BASE64编码"""
@ -1116,7 +1132,7 @@ def disease_diagnosis(**kwargs) -> str | None:
# -------------------------
# 主逻辑
# 主逻辑部分
# -------------------------
@ -1146,15 +1162,15 @@ if __name__ == "__main__":
# 加载赔案档案模版
template = environment.get_template("template.html")
# 遍历工作目录中赔案目录,根据赔案创建赔案档案
# 遍历工作目录中赔案目录,根据赔案创建赔案档案(模拟自动化域就待自动化任务创建理赔档案)
for case_path in [
case_path for case_path in directory_path.iterdir() if case_path.is_dir()
]:
# 初始化赔案档案(简化版本)
# 初始化赔案档案(实际报案层包括保险分公司名称、报案渠道、批次号、报案号和报案时间等)
# 报案渠道包括:保司定义,例如中银项目包括总行和各地分行驻场报案和普康宝自助报案等
dossier = {
"报案层": {
"保险分公司": "中银保险有限公司广东分公司",
"保险分公司": "中银保险有限公司广东分公司", # 设定:保险分公司
"赔案号": (case_number := case_path.stem), # 设定:赔案目录名称为赔案号
},
"影像件层": [],
@ -1162,30 +1178,41 @@ if __name__ == "__main__":
# 遍历赔案目录中影像件地址
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_index := f"{image_index:02d}"),
"影像件名称": (image_name := image_path.name),
# 初始化影像件数据
image = {
"原始影像件": {
"影像件地址": 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则跳过该影像件
if (image_format := image_path.suffix.lower().lstrip(".")) not in [
"jpg",
"jpeg",
"png",
]:
dossier["影像件层"][-1]["已分类"] = "否,不支持的影像件"
continue
# 影像件读取
image = cv2.imread(image_path.as_posix(), cv2.IMREAD_GRAYSCALE)
# 若发生异常则跳过该影像件
if image is None:
dossier["影像件层"][-1]["已分类"] = "否,读取异常"
continue
print(image)
exit()
# 影像件压缩输出BASE64编码
image_guid, image_base64 = images_compression()
@ -1200,7 +1227,13 @@ if __name__ == "__main__":
if image_type is None or image_orientation is None:
dossier["影像件层"][-1]["已分类"] = "否,影像件分类异常"
continue
#
dossier["影像件层"].append(
{
"影像件序号": (image_index := f"{image_index:02d}"),
"影像件名称": (image_name := image_path.name),
}
)
# 若影像件方向非0度则影像件旋正并在此压缩
if image_orientation != "0度":
# 影像件旋正