parent
d232b65335
commit
c9a025d2f6
|
|
@ -617,7 +617,7 @@ class GenerateDraft:
|
|||
|
||||
|
||||
# ======================== 调用示例(使用抽象后的方法) ========================
|
||||
def execute_workflow():
|
||||
def direct():
|
||||
"""生成剪映草稿"""
|
||||
# 实例化
|
||||
draft = GenerateDraft(
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
def general_text_recognize(image) -> str:
|
||||
"""
|
||||
通用文本识别
|
||||
:param image: 影像件
|
||||
:return: 识别文本
|
||||
"""
|
||||
# 请求深圳快瞳通用文本识别接口
|
||||
response = http_client.post(
|
||||
url=(url := "https://ai.inspirvision.cn/s/api/ocr/general"),
|
||||
headers={
|
||||
"X-RequestId-Header": image["影像件唯一标识"]
|
||||
}, # 以影像件唯一标识作为请求唯一标识,用于双方联查
|
||||
data={
|
||||
"token": authenticator.get_token(servicer="szkt"), # 获取深圳快瞳访问令牌
|
||||
"imgBase64": f"data:image/{image["影像件格式"].lstrip(".")};base64,{image["影像件BASE64编码"]}",
|
||||
},
|
||||
guid=md5((url + image["影像件唯一标识"]).encode("utf-8")).hexdigest().upper(),
|
||||
)
|
||||
# TODO: 若响应非成功则流转至人工处理
|
||||
if not (response.get("status") == 200 and response.get("code") == 0):
|
||||
raise RuntimeError("请求深圳快瞳通用文本识别接口发生异常")
|
||||
|
||||
blocks = []
|
||||
for block in response["data"]:
|
||||
# noinspection PyTypeChecker
|
||||
blocks.append(
|
||||
[
|
||||
int(block["itemPolygon"]["x"]), # 文本块左上角的X坐标
|
||||
int(block["itemPolygon"]["y"]), # 文本块左上角的Y坐标
|
||||
int(block["itemPolygon"]["height"]), # 文本块左上角的高度
|
||||
block["value"], # 文本块的文本内容
|
||||
]
|
||||
)
|
||||
# 使用俄罗斯方块方法整理文本块,先按照文本块的Y坐标升序(从上到下)
|
||||
blocks.sort(key=lambda x: x[1])
|
||||
|
||||
lines = []
|
||||
for idx, block in enumerate(blocks[1:]):
|
||||
if idx == 0:
|
||||
line = [blocks[0]]
|
||||
continue
|
||||
# 若当前文本块的Y坐标和当前文本行的平均Y坐标差值小于阈值则归为同一文本行,否则另起一文本行(分行)
|
||||
if (
|
||||
block[1] - numpy.array([e[1] for e in line]).mean()
|
||||
< numpy.array([e[2] for e in line]).mean()
|
||||
):
|
||||
line.append(block)
|
||||
else:
|
||||
lines.append(line)
|
||||
line = [block]
|
||||
lines.append(line)
|
||||
|
||||
blocks = []
|
||||
for line in lines:
|
||||
blocks.extend(
|
||||
[re.sub(r"\s", "", x[3]) for x in sorted(line, key=lambda x: x[0])]
|
||||
) # 按照文本块的X坐标升序(从左到右)并去除文本块的文本内容中所有空字符
|
||||
return "\n".join(blocks)
|
||||
|
||||
class JiojioTokenizer:
|
||||
"""中文分词器"""
|
||||
|
||||
def __init__(self):
|
||||
# 初始化jiojio分词器
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
jiojio.init()
|
||||
except:
|
||||
raise RuntimeError("初始化jiojio分词器发生异常")
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@staticmethod
|
||||
def callback(text: str, flags: int, cursor) -> None:
|
||||
"""
|
||||
分词回调函数
|
||||
:param text: 待分词文本
|
||||
:param flags: FTS5分词场景标记位
|
||||
:param cursor: FTS5分词回传游标
|
||||
return 无
|
||||
"""
|
||||
if not text or not isinstance(text, str):
|
||||
return
|
||||
|
||||
tokens = []
|
||||
begin_idx = 0 # 当前分词开始索引
|
||||
for word in jiojio.cut(text):
|
||||
if word.strip() == "":
|
||||
begin_idx += len(word)
|
||||
continue
|
||||
tokens.append(
|
||||
(word, begin_idx, end_idx := begin_idx + len(word))
|
||||
) # SQLite FTS5要求回传分词语音文本开始和结束索引
|
||||
begin_idx = end_idx
|
||||
|
||||
for token, begin_idx, end_idx in tokens:
|
||||
cursor.send((token, begin_idx, end_idx))
|
||||
|
||||
# 实例化jiojio分词器
|
||||
self.threads.jiojio_tokenizer = self.JiojioTokenizer()
|
||||
|
||||
# 创建分词器方法
|
||||
def create_tokenizer_module(tokenizer):
|
||||
class JiojioTokenizerModule:
|
||||
"""创建jiojio分词器方法"""
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@staticmethod
|
||||
def tokenize(text: str, flags: int, cursor) -> None:
|
||||
tokenizer.callback(text, flags, cursor)
|
||||
|
||||
return JiojioTokenizerModule()
|
||||
|
||||
self.threads.connection.create_module(
|
||||
"jiojio_fts5_module",
|
||||
create_tokenizer_module(self.threads.jiojio_tokenizer),
|
||||
)
|
||||
|
||||
self.threads.connection.execute(
|
||||
"""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS jiojio_tokenizer USING fts5tokenizer(jiojio_fts5_module)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
{
|
||||
"code": 0,
|
||||
"status": 200,
|
||||
"message": "success",
|
||||
"serialNo": "3a08935648632621760512",
|
||||
"data": [
|
||||
{"desc": "金额", "value": "175.22"},
|
||||
{
|
||||
"desc": "项目名称",
|
||||
"value": "*化学药品制剂*[海露]玻璃酸钠滴眼液0.1%*10ml支/盒",
|
||||
},
|
||||
{"desc": "数量", "value": "2"},
|
||||
{"desc": "规格型号", "value": ""},
|
||||
{"desc": "税额", "value": "22.78"},
|
||||
{"desc": "税率", "value": "13%"},
|
||||
{"desc": "单位", "value": ""},
|
||||
{"desc": "单价", "value": "87.61"},
|
||||
{"desc": "金额1", "value": "-69.42"},
|
||||
{
|
||||
"desc": "项目名称1",
|
||||
"value": "*化学药品制剂*[海露]玻璃酸钠滴眼液0.1%*10ml/支/盒",
|
||||
},
|
||||
{"desc": "数量1", "value": ""},
|
||||
{"desc": "规格型号1", "value": ""},
|
||||
{"desc": "税额1", "value": "-9.02"},
|
||||
{"desc": "税率1", "value": "13%"},
|
||||
{"desc": "单位1", "value": ""},
|
||||
{"desc": "单价1", "value": ""},
|
||||
{"desc": "发票名称", "value": "电子发票(普通发票)"},
|
||||
{"desc": "全电票标签", "value": ""},
|
||||
{"desc": "发票号码", "value": "25447200000045325946"},
|
||||
{"desc": "开票日期", "value": "2025年01月20日"},
|
||||
{"desc": "购买方名称", "value": "唐敏华"},
|
||||
{"desc": "购买方识别号", "value": ""},
|
||||
{"desc": "销售方名称", "value": "广州美团大药房有限公司"},
|
||||
{"desc": "销售方识别号", "value": "91440100MAC1CAJH27"},
|
||||
{"desc": "合计金额", "value": "¥105.80"},
|
||||
{"desc": "合计税额", "value": "¥13.76"},
|
||||
{"desc": "金额小计", "value": ""},
|
||||
{"desc": "税额小计", "value": ""},
|
||||
{"desc": "价税合计(大写)", "value": "壹佰壹拾玖圆伍角陆分"},
|
||||
{"desc": "小写金额", "value": "¥119.56"},
|
||||
{"desc": "备注", "value": ""},
|
||||
{"desc": "开票人", "value": "张景景"},
|
||||
{"desc": "发票类型", "value": "电子发票(普通发票)"},
|
||||
{"desc": "监制章存在性判断", "value": "True"},
|
||||
{"desc": "总页数", "value": ""},
|
||||
{"desc": "当前页数", "value": ""},
|
||||
],
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
with open(f"dossiers/{case_number}.html", "w", encoding="utf-8") as file:
|
||||
file.write(
|
||||
template.render(
|
||||
{
|
||||
"dossier": dossier,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from masterdata import MasterData
|
||||
from utils.rule_engine import RuleEngine
|
||||
|
||||
# 初始化赔案档案(保险公司将提供投保公司、保险分公司和报案时间等,TPA作业系统签收后生成赔案号)
|
||||
dossier = {
|
||||
"report_layer": {}, # 报案层
|
||||
"images_layer": [], # 影像件层
|
||||
"insured_person_layer": {}, # 出险人层
|
||||
"insured_persons_layer": [], # 被保险人层
|
||||
"receipts_layer": [], # 票据层
|
||||
"adjustment_layer": {}, # 理算层
|
||||
}
|
||||
|
||||
# 实例化主数据
|
||||
master_data = MasterData()
|
||||
|
||||
# 实例化规则引擎
|
||||
rule_engine = RuleEngine(rules_path=Path("rules"))
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
1598
票据理赔自动化/main.py
1598
票据理赔自动化/main.py
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,321 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from utils.client import SQLiteClient
|
||||
|
||||
|
||||
class MasterData(SQLiteClient):
|
||||
"""主数据"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
初始化主数据
|
||||
"""
|
||||
# 初始化SQLite客户端
|
||||
super().__init__(database="database.db")
|
||||
try:
|
||||
with self:
|
||||
# 初始化团单表
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS group_policies
|
||||
(
|
||||
--团单唯一标识
|
||||
guid TEXT PRIMARY KEY,
|
||||
--团单号
|
||||
group_policy TEXT NOT NULL,
|
||||
--保险分公司名称
|
||||
insurer_company TEXT NOT NULL,
|
||||
--保险起期
|
||||
commencement_date TEXT NOT NULL,
|
||||
--保险止期
|
||||
termination_date TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
# 初始化个单表
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS person_policies
|
||||
(
|
||||
--个单唯一标识
|
||||
guid TEXT PRIMARY KEY,
|
||||
--个单号
|
||||
person_policy TEXT NOT NULL,
|
||||
--保险起期
|
||||
commencement_date TEXT NOT NULL,
|
||||
--保险止期
|
||||
termination_date TEXT NOT NULL,
|
||||
--团单唯一标识,用于联查团案
|
||||
group_policy_guid TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
# 初始化被保险人表,保司推送赔案时,一般无团单号,需先根据保险分公司名称、被保险人姓名、证件类型和证件号码查询被保人,再在票据理算时根据事故起期确定个单和相应责任
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS insured_persons
|
||||
(
|
||||
--被保险人唯一标识
|
||||
guid TEXT PRIMARY KEY,
|
||||
--被保险人姓名
|
||||
insured_person TEXT NOT NULL,
|
||||
--证件类型
|
||||
identity_type TEXT NOT NULL,
|
||||
--证件号码
|
||||
identity_number TEXT NOT NULL,
|
||||
--与主被保险人关系,包括本人、父母、配偶和子女等
|
||||
relationship TEXT NOT NULL,
|
||||
--个单唯一标识,用于联查个单
|
||||
person_policy_guid TEXT NOT NULL
|
||||
|
||||
)
|
||||
"""
|
||||
)
|
||||
# 初始化责任表
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS liabilities
|
||||
(
|
||||
--责任唯一标识
|
||||
guid TEXT PRIMARY KEY,
|
||||
--责任名称
|
||||
liability TEXT NOT NULL,
|
||||
--出险事故
|
||||
accident TEXT NOT NULL,
|
||||
--个人自费理算比例
|
||||
personal_self_ratio TEXT NOT NULL,
|
||||
--个人自付理算比例
|
||||
non_medical_ratio TEXT NOT NULL,
|
||||
--合理理算比例
|
||||
reasonable_ratio TEXT NOT NULL,
|
||||
--理算保单唯一标识
|
||||
adjust_policy_guid TEXT NOT NULL,
|
||||
--个单唯一标识
|
||||
person_policy_guid TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
# 初始化保额变动表
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS coverage_changes
|
||||
(
|
||||
--保额变动唯一标识
|
||||
guid TEXT PRIMARY KEY,
|
||||
--变动类型,包括承保和理算等
|
||||
change_type TEXT NOT NULL,
|
||||
--变动前金额
|
||||
before_change_amount TEXT NOT NULL,
|
||||
--变动金额
|
||||
change_amount TEXT NOT NULL,
|
||||
--变动后金额
|
||||
after_change_amount TEXT NOT NULL,
|
||||
--变动时间
|
||||
change_time TEXT NOT NULL,
|
||||
--变动保单唯一标识
|
||||
change_policy_guid TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
# 初始化购药及就医机构表
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS institutions
|
||||
(
|
||||
--购药及就医机构
|
||||
institution TEXT PRIMARY KEY,
|
||||
--购药及就医机构类型
|
||||
institution_type TEXT NOT NULL,
|
||||
--所在省
|
||||
province TEXT NOT NULL,
|
||||
--所在市
|
||||
city TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
# 初始化药品表
|
||||
self._execute(
|
||||
sql="""
|
||||
CREATE TABLE IF NOT EXISTS medicines
|
||||
(
|
||||
--药品/医疗服务
|
||||
medicine TEXT PRIMARY KEY
|
||||
)
|
||||
"""
|
||||
)
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"初始化数据库发生异常:{str(exception)}")
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def query_liabilities(
|
||||
self,
|
||||
insurer_company: str,
|
||||
insured_person: str,
|
||||
identity_type: str,
|
||||
identity_number: str,
|
||||
report_date: str,
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
根据保险分公司名称、被保险人姓名、证件类型、证件号码和出险时间查询责任列表
|
||||
:param insurer_company: 保险分公司名称
|
||||
:param insured_person: 被保险人姓名
|
||||
:param identity_type: 证件类型
|
||||
:param identity_number: 证件号码
|
||||
:param report_date: 报案时间
|
||||
:return: 责任列表
|
||||
"""
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
with self:
|
||||
# noinspection SqlResolve
|
||||
result = self._query_all(
|
||||
sql="""
|
||||
SELECT group_policies.group_policy,
|
||||
group_policies.insurer_company,
|
||||
person_policies.person_policy,
|
||||
person_policy_coverage_changes.after_change_amount AS remaining_amount,
|
||||
master_insured_persons.insured_person AS master_insured_person,
|
||||
insured_persons.insured_person,
|
||||
insured_persons.identity_type,
|
||||
insured_persons.identity_number,
|
||||
insured_persons.relationship,
|
||||
MAX(group_policies.commencement_date,
|
||||
person_policies.commencement_date) AS commencement_date,
|
||||
MIN(group_policies.termination_date,
|
||||
person_policies.termination_date) AS termination_date,
|
||||
liabilities.liability,
|
||||
liabilities.accident,
|
||||
liabilities.personal_self_ratio,
|
||||
liabilities.non_medical_ratio,
|
||||
liabilities.reasonable_ratio,
|
||||
liabilities.adjust_policy_guid
|
||||
FROM insured_persons
|
||||
INNER JOIN insured_persons master_insured_persons
|
||||
ON person_policies.guid = master_insured_persons.person_policy_guid
|
||||
AND master_insured_persons.relationship = "本人"
|
||||
INNER JOIN person_policies
|
||||
ON insured_persons.person_policy_guid = person_policies.guid
|
||||
INNER JOIN group_policies
|
||||
ON person_policies.group_policy_guid = group_policies.guid
|
||||
INNER JOIN liabilities
|
||||
ON person_policies.guid = liabilities.person_policy_guid
|
||||
INNER JOIN coverage_changes person_policy_coverage_changes
|
||||
ON person_policies.guid =
|
||||
person_policy_coverage_changes.change_policy_guid
|
||||
AND
|
||||
person_policy_coverage_changes.change_time = (SELECT MAX(change_time)
|
||||
FROM coverage_changes
|
||||
WHERE change_policy_guid = person_policies.guid)
|
||||
INNER JOIN coverage_changes
|
||||
ON liabilities.adjust_policy_guid = coverage_changes.change_policy_guid
|
||||
AND coverage_changes.change_time = (SELECT MAX(change_time)
|
||||
FROM coverage_changes
|
||||
WHERE liabilities.adjust_policy_guid = change_policy_guid)
|
||||
WHERE group_policies.insurer_company = ?
|
||||
AND insured_persons.insured_person = ?
|
||||
AND insured_persons.identity_type = ?
|
||||
AND insured_persons.identity_number = ?
|
||||
AND ? BETWEEN group_policies.commencement_date AND group_policies.termination_date
|
||||
AND ? BETWEEN person_policies.commencement_date AND person_policies.termination_date
|
||||
AND CAST(coverage_changes.after_change_amount AS REAL) > 0
|
||||
""",
|
||||
parameters=(
|
||||
insurer_company,
|
||||
insured_person,
|
||||
identity_type,
|
||||
identity_number,
|
||||
report_date,
|
||||
report_date,
|
||||
),
|
||||
)
|
||||
if result:
|
||||
return [
|
||||
{
|
||||
k: (
|
||||
datetime.strptime(v, "%Y-%m-%d")
|
||||
if k in ["commencement_date", "termination_date"]
|
||||
else (
|
||||
Decimal(v).quantize(
|
||||
Decimal("0.00"),
|
||||
rounding=ROUND_HALF_UP,
|
||||
)
|
||||
if k
|
||||
in [
|
||||
"remaining_amount",
|
||||
"personal_self_ratio",
|
||||
"non_medical_ratio",
|
||||
"reasonable_ratio",
|
||||
]
|
||||
else v
|
||||
)
|
||||
) # 就保险起期、止期则转为日期时间(datetime对象),个人自费比例、个人自付比例和合理比例转为小数(decimal对象)
|
||||
for k, v in e.items()
|
||||
}
|
||||
for e in result
|
||||
] # 将保险起期和保险止期转为日期(datetime对象)
|
||||
raise RuntimeError("查无数据")
|
||||
# TODO: 若根据保险分公司名称、被保险人姓名、证件类型、证件号码和出险时间查询被保险人发生异常则流转至主数据人工处理
|
||||
except Exception as exception:
|
||||
raise RuntimeError(f"{str(exception)}")
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def query_institution_type(self, institution: str) -> Optional[str]:
|
||||
"""
|
||||
根据购药及就医机构查询购药及就医机构类型
|
||||
:param institution: 购药及就医机构
|
||||
:return: 购药及就医机构类型
|
||||
"""
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
with self:
|
||||
# noinspection SqlResolve
|
||||
result = self._query_one(
|
||||
sql="""
|
||||
SELECT institution_type
|
||||
FROM institutions
|
||||
WHERE institution = ?
|
||||
""",
|
||||
parameters=(institution,),
|
||||
)
|
||||
if result:
|
||||
return result["institution_type"]
|
||||
raise
|
||||
# TODO: 若根据购药及就医机构查询购药及就医机构类型发生异常则流转至主数据人工处理
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def query_medicine(
|
||||
self,
|
||||
content: str,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
根据明细项中具体内容查询药品/医疗服务
|
||||
:param content: 明细项具体内容
|
||||
:return: 药品/医疗服务
|
||||
"""
|
||||
# TODO: 暂仅支持查询药品、通过药品/医疗服务包含明细项中具体内容查询
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
with self:
|
||||
# noinspection SqlResolve
|
||||
result = self._query_all(
|
||||
sql="""
|
||||
SELECT medicine
|
||||
FROM medicines
|
||||
WHERE ? LIKE '%' || medicine || '%'
|
||||
""",
|
||||
parameters=(content,),
|
||||
)
|
||||
if result:
|
||||
return max(result, key=lambda x: len(x["medicine"]))[
|
||||
"medicine"
|
||||
] # 返回最大长度的药品/医疗服务
|
||||
raise
|
||||
# TODO: 若根据明细项中具体内容查询药品/医疗服务发生异常则流转至主数据人工处理
|
||||
except Exception:
|
||||
raise
|
||||
Loading…
Reference in New Issue