新增证件识别接口
This commit is contained in:
@@ -0,0 +1,152 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
|
||||||
|
from config.llm import llmVision
|
||||||
|
|
||||||
|
|
||||||
|
# -------- 定义状态 --------
|
||||||
|
class State(TypedDict):
|
||||||
|
image_url: str # 证件照
|
||||||
|
type: int # 证件类别
|
||||||
|
content: str # 最终内容
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_card(state: State, prompt_text: str):
|
||||||
|
messages = [
|
||||||
|
HumanMessage(
|
||||||
|
content=[
|
||||||
|
{"type": "text", "text": prompt_text},
|
||||||
|
{"type": "image_url", "image_url": {"url": state["image_url"]}},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return llmVision.invoke(messages).content
|
||||||
|
|
||||||
|
|
||||||
|
# -------- 定义节点 --------
|
||||||
|
def decide_source(state: State, max_retry=3):
|
||||||
|
choice = analyze_card(
|
||||||
|
state,
|
||||||
|
"""
|
||||||
|
请根据图片内容判断其中的证件类型,并输出对应代号。
|
||||||
|
代号说明:
|
||||||
|
-1: 并非证件
|
||||||
|
0:身份证
|
||||||
|
1:银行卡
|
||||||
|
|
||||||
|
只输出代号数字,不要输出任何文字、标点或解释。
|
||||||
|
你的输出是:
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
print("图片类型是", choice)
|
||||||
|
try:
|
||||||
|
choice = int(choice)
|
||||||
|
state["type"] = choice
|
||||||
|
if choice == -1:
|
||||||
|
state["content"] = '{"result": "暂不支持此证件"}'
|
||||||
|
except ValueError:
|
||||||
|
state["type"] = -1
|
||||||
|
state["content"] = '{"result": "暂不支持此证件"}'
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def idcard(state: State):
|
||||||
|
state["content"] = analyze_card(
|
||||||
|
state,
|
||||||
|
"""
|
||||||
|
你是一个 OCR 信息提取专家,请从图片中识别出身份证的全部可见信息,并输出严格的 JSON 格式。
|
||||||
|
要求:
|
||||||
|
1. 无论是正面还是反面,都要识别所有字段。
|
||||||
|
2. 如果某个字段无法辨认,请将值设为 null。
|
||||||
|
3. 只输出 JSON,不要输出任何解释或多余内容。
|
||||||
|
4. 严格保持 JSON 格式,键名统一为英文。
|
||||||
|
5. "side" 字段为身份证方向,填写:人像面 或 国徽面。
|
||||||
|
|
||||||
|
JSON 示例格式:
|
||||||
|
{
|
||||||
|
"side": "国徽面",
|
||||||
|
"name": "持证人姓名",
|
||||||
|
"gender": "性别",
|
||||||
|
"ethnicity": "民族",
|
||||||
|
"id_number": "身份证号",
|
||||||
|
"birth_date": "出生日期",
|
||||||
|
"address": "住址",
|
||||||
|
"issuing_authority": "签发机关",
|
||||||
|
"valid_period_start": "有效期开始日期",
|
||||||
|
"valid_period_end": "有效期结束日期",
|
||||||
|
"notes": "其他可见文字或备注"
|
||||||
|
}
|
||||||
|
|
||||||
|
请确保输出的 JSON 可以被严格解析。
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def bankcard(state: State):
|
||||||
|
state["content"] = analyze_card(
|
||||||
|
state,
|
||||||
|
"""
|
||||||
|
你是一个 OCR 信息提取专家,请从图片中识别出银行卡上的全部可见信息,并输出严格的 JSON 格式。
|
||||||
|
要求:
|
||||||
|
1. 识别卡号、发卡行、持卡人、有效期等关键信息。
|
||||||
|
2. 如果某个字段无法辨认,请将值设为 null。
|
||||||
|
3. 只输出 JSON,不要输出任何解释或附加内容。
|
||||||
|
4. 严格保持 JSON 格式,键名统一为英文。
|
||||||
|
|
||||||
|
JSON 示例格式:
|
||||||
|
{
|
||||||
|
"bank_name": "发卡行名称",
|
||||||
|
"card_number": "卡号",
|
||||||
|
"card_holder": "持卡人姓名",
|
||||||
|
"expiry_date": "有效期",
|
||||||
|
"card_type": "卡种类型(如借记卡/信用卡/Visa/MasterCard)",
|
||||||
|
"issuer_country": "发卡国家",
|
||||||
|
"notes": "其他可见文字或符号"
|
||||||
|
}
|
||||||
|
|
||||||
|
请确保输出的 JSON 可以被严格解析。
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------ 构建有向图 --------
|
||||||
|
workflow = StateGraph(State)
|
||||||
|
workflow.add_node("decide", decide_source)
|
||||||
|
workflow.add_node("idcard", idcard)
|
||||||
|
workflow.add_node("bankcard", bankcard)
|
||||||
|
workflow.set_entry_point("decide")
|
||||||
|
# 条件边:根据 path 决定走向
|
||||||
|
workflow.add_conditional_edges(
|
||||||
|
"decide",
|
||||||
|
lambda state: state["type"],
|
||||||
|
{
|
||||||
|
-1: END,
|
||||||
|
0: "idcard",
|
||||||
|
1: "bankcard",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
workflow.add_edge("idcard", END)
|
||||||
|
workflow.add_edge("bankcard", END)
|
||||||
|
graph = workflow.compile()
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
# 执行函数
|
||||||
|
def get_license_response(image_url: str):
|
||||||
|
final_state = graph.invoke(
|
||||||
|
{
|
||||||
|
"image_url": image_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 去掉 ```json 和 ``` 包裹
|
||||||
|
final_state["content"] = final_state["content"]
|
||||||
|
final_state["content"] = re.sub(
|
||||||
|
r"^```json\s*|\s*```$", "", final_state["content"].strip()
|
||||||
|
)
|
||||||
|
print("最终识别内容:", final_state["content"])
|
||||||
|
return final_state
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from langchain_core.messages import HumanMessage
|
|
||||||
|
|
||||||
from config.llm import *
|
|
||||||
from langchain.prompts import PromptTemplate
|
|
||||||
|
|
||||||
from langchain.prompts import PromptTemplate
|
|
||||||
from langchain.schema import HumanMessage
|
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
from langchain.prompts import PromptTemplate
|
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langchain_core.output_parsers import JsonOutputParser
|
|
||||||
from langchain.schema import HumanMessage
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
import json
|
|
||||||
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
import requests
|
|
||||||
|
|
||||||
class CocoonSample(BaseModel):
|
|
||||||
moisture_content: float = Field(
|
|
||||||
...,
|
|
||||||
description="茧的含水量,单位为百分比(%),浮点数"
|
|
||||||
)
|
|
||||||
cocoon_weight: float = Field(
|
|
||||||
...,
|
|
||||||
description="下足茧的重量,单位为克,可带小数"
|
|
||||||
)
|
|
||||||
defective_pupa_count: int = Field(
|
|
||||||
...,
|
|
||||||
description="非好蛹粒数,即不合格蛹的数量,整数"
|
|
||||||
)
|
|
||||||
fresh_shell_weight: float = Field(
|
|
||||||
...,
|
|
||||||
description="鲜壳重量,单位为克,可带小数"
|
|
||||||
)
|
|
||||||
sample_count: int = Field(
|
|
||||||
...,
|
|
||||||
description="小样粒数,用于检测的茧粒数,整数"
|
|
||||||
)
|
|
||||||
net_weight_total: float = Field(
|
|
||||||
...,
|
|
||||||
description="所有样品的净重合计,单位为克,浮点数"
|
|
||||||
)
|
|
||||||
evaluator: Optional[str] = Field(
|
|
||||||
None,
|
|
||||||
description="仪评人姓名,可能为空"
|
|
||||||
)
|
|
||||||
reviewer: Optional[str] = Field(
|
|
||||||
None,
|
|
||||||
description="复核人员姓名,可能为空"
|
|
||||||
)
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
from langchain_postgres import PostgresChatMessageHistory
|
from langchain_postgres import PostgresChatMessageHistory
|
||||||
@@ -559,3 +558,80 @@ def insert_ticket_image(
|
|||||||
new_id = cursor.fetchone()[0]
|
new_id = cursor.fetchone()[0]
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
# ————————————————————————————————————————————————————证件照片识别———————————————————————————————
|
||||||
|
|
||||||
|
|
||||||
|
def insert_license_image(
|
||||||
|
created_by, file_name, resolution, size, name, oss, type, content
|
||||||
|
):
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO license_images (
|
||||||
|
created_by, created_at, file_name, resolution, size, name, oss,
|
||||||
|
type, content
|
||||||
|
)
|
||||||
|
VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s )
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
(created_by, file_name, resolution, size, name, oss, type, content),
|
||||||
|
)
|
||||||
|
new_id = cursor.fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_license_image_list(user_id, page=1, page_size=10):
|
||||||
|
"""
|
||||||
|
获取用户已分析图片列表,带分页
|
||||||
|
"""
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
# 1️⃣ 查询总条数
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM license_images
|
||||||
|
WHERE created_by = %s
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# 2️⃣ 查询当前页数据
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT created_at, file_name, resolution, size, name, oss, id, type, content
|
||||||
|
FROM license_images
|
||||||
|
WHERE created_by = %s
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
""",
|
||||||
|
(user_id, page_size, offset),
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"created_at": MyUtils.format_datetime(row[0]),
|
||||||
|
"file_name": row[1],
|
||||||
|
"resolution": row[2],
|
||||||
|
"size": round(row[3] / 1024, 2),
|
||||||
|
"name": row[4],
|
||||||
|
"oss_url": get_temp_url("image-license", row[5]),
|
||||||
|
"id": row[6],
|
||||||
|
"type": row[7],
|
||||||
|
"content": row[8],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return total, result
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from config.app import F8_SERVER_USER_ID
|
from config.app import F8_SERVER_USER_ID
|
||||||
from models.BaseResponse import BaseResponse
|
from models.BaseResponse import BaseResponse
|
||||||
|
from models.F8ImageRequest import F8ImageRequest
|
||||||
from models.F8ImageRequestV2 import F8ImageRequestV2
|
from models.F8ImageRequestV2 import F8ImageRequestV2
|
||||||
from service.vision import process_ticket_image
|
from service.vision import process_ticket_image, process_license_image
|
||||||
from utils import MyUtils
|
from utils import MyUtils
|
||||||
|
|
||||||
publicRouter = APIRouter()
|
publicRouter = APIRouter()
|
||||||
@@ -30,3 +31,27 @@ async def cocoonTicket(data: F8ImageRequestV2):
|
|||||||
return BaseResponse(data=json_data)
|
return BaseResponse(data=json_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@publicRouter.post("/recognize-license")
|
||||||
|
async def cocoon_license(data: F8ImageRequest):
|
||||||
|
input_data = data.image
|
||||||
|
if "," in input_data:
|
||||||
|
input_data = input_data.split(",")[1]
|
||||||
|
try:
|
||||||
|
img_bytes = base64.b64decode(input_data)
|
||||||
|
json_data = await MyUtils.async_task(
|
||||||
|
process_license_image,
|
||||||
|
img_bytes,
|
||||||
|
f"{data.title}.jpg",
|
||||||
|
data.title,
|
||||||
|
F8_SERVER_USER_ID,
|
||||||
|
)
|
||||||
|
data = json.loads(json_data.get("content"))
|
||||||
|
data["type"] = json_data.get("type")
|
||||||
|
return BaseResponse(data=data)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, File, Form, Depends
|
from fastapi import APIRouter, File, Form, Depends, Query
|
||||||
|
|
||||||
import db.postgres as pg
|
import db.postgres as pg
|
||||||
from config.security import get_user_id_from_token
|
from config.security import get_user_id_from_token
|
||||||
from llm.ticketLLM import *
|
from llm.ticketLLM import *
|
||||||
from models.BaseResponse import BaseResponse
|
from models.BaseResponse import BaseResponse
|
||||||
from models.ImageRequest import ImageRequest
|
from models.ImageRequest import ImageRequest
|
||||||
from service.vision import process_ticket_image
|
from service.vision import process_ticket_image, process_license_image
|
||||||
from utils import MyUtils
|
from utils import MyUtils
|
||||||
|
|
||||||
visionRouter = APIRouter()
|
visionRouter = APIRouter()
|
||||||
@@ -51,6 +51,25 @@ def cocoonTicket(user_id: UUID = Depends(get_user_id_from_token)):
|
|||||||
return BaseResponse(data=pg.get_ticket_image_list(user_id))
|
return BaseResponse(data=pg.get_ticket_image_list(user_id))
|
||||||
|
|
||||||
|
|
||||||
|
@visionRouter.post("/createTicketImageTaskV2")
|
||||||
|
async def createTicketImageTask(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
projectName: str = Form(...),
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
try:
|
||||||
|
contents = await file.read()
|
||||||
|
await MyUtils.async_task(
|
||||||
|
process_ticket_image, 2, True, contents, file.filename, projectName, user_id
|
||||||
|
)
|
||||||
|
return BaseResponse(data=None)
|
||||||
|
except Exception as e:
|
||||||
|
print(str(e))
|
||||||
|
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||||
|
|
||||||
|
|
||||||
@visionRouter.post("/createTicketImageTaskV2")
|
@visionRouter.post("/createTicketImageTaskV2")
|
||||||
async def createTicketImageTask(
|
async def createTicketImageTask(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
@@ -66,5 +85,39 @@ async def createTicketImageTask(
|
|||||||
)
|
)
|
||||||
return BaseResponse(data=json_data)
|
return BaseResponse(data=json_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(str(e))
|
|
||||||
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||||
|
|
||||||
|
|
||||||
|
@visionRouter.post("/createLicenseImageTask")
|
||||||
|
async def createLicenseImageTask(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
projectName: str = Form(...),
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
try:
|
||||||
|
contents = await file.read()
|
||||||
|
await MyUtils.async_task(
|
||||||
|
process_license_image, contents, file.filename, projectName, user_id
|
||||||
|
)
|
||||||
|
return BaseResponse(data=None)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||||
|
|
||||||
|
|
||||||
|
@visionRouter.get("/getLicenseImageList")
|
||||||
|
def cocoonLicense(
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(10, ge=1, le=100),
|
||||||
|
):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
total, items = pg.get_license_image_list(user_id, page=page, page_size=page_size)
|
||||||
|
return BaseResponse(
|
||||||
|
data={
|
||||||
|
"total": total,
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
import config.minIO as minIO
|
import config.minIO as minIO
|
||||||
import db.postgres as pg
|
import db.postgres as pg
|
||||||
|
from agent.licenseImageAgent import get_license_response
|
||||||
from config.minIO import minio_client
|
from config.minIO import minio_client
|
||||||
from llm.ticketLLM import *
|
from llm.ticketLLM import *
|
||||||
from llm.ticketLLMv2 import get_ticket_response_v2
|
from llm.ticketLLMv2 import get_ticket_response_v2
|
||||||
@@ -65,3 +66,43 @@ def process_ticket_image(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return json_data
|
return json_data
|
||||||
|
|
||||||
|
|
||||||
|
def process_license_image(
|
||||||
|
img_bytes=None,
|
||||||
|
file_name: str = None,
|
||||||
|
project_name: str = None,
|
||||||
|
user_id: UUID = None,
|
||||||
|
):
|
||||||
|
# 上传到 OSS,使用 UUID 做对象名
|
||||||
|
if img_bytes is None:
|
||||||
|
img_bytes = []
|
||||||
|
object_name = str(uuid.uuid4())
|
||||||
|
file_bytes = BytesIO(img_bytes)
|
||||||
|
bucket_name = "image-license"
|
||||||
|
if not minio_client.bucket_exists(bucket_name):
|
||||||
|
minio_client.make_bucket(bucket_name)
|
||||||
|
|
||||||
|
minIO.push_file(bucket_name, object_name, file_bytes, img_bytes, "image/jpeg")
|
||||||
|
oss_url = minIO.get_temp_url(bucket_name, object_name)
|
||||||
|
|
||||||
|
# 调用分析方法获取 JSON
|
||||||
|
json_data = get_license_response(oss_url)
|
||||||
|
# 获取图片分辨率和大小
|
||||||
|
img = Image.open(BytesIO(img_bytes))
|
||||||
|
resolution = f"{img.width}x{img.height}"
|
||||||
|
size_kb = round(len(img_bytes) / 1024, 2)
|
||||||
|
|
||||||
|
# 插入数据库
|
||||||
|
pg.insert_license_image(
|
||||||
|
created_by=user_id,
|
||||||
|
file_name=file_name,
|
||||||
|
resolution=resolution,
|
||||||
|
size=size_kb,
|
||||||
|
name=project_name if project_name else object_name[:8],
|
||||||
|
oss=object_name,
|
||||||
|
type=json_data.get("type"),
|
||||||
|
content=json_data.get("content"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return json_data
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
| | 9100 | | ce_node_exporter | Prometheus | Prometheus监控主机的工具 |
|
| | 9100 | | ce_node_exporter | Prometheus | Prometheus监控主机的工具 |
|
||||||
| | | | | | |
|
| | | | | | |
|
||||||
| | | | | | |
|
| | | | | | |
|
||||||
| **13011** | 13011 | 13011 | Harbor | Harbor | Harbor,admin:bbit |
|
| **13011** | 13011 | 13011 | Harbor | Harbor | Harbor,admin:Bbit000000 |
|
||||||
| 8088 | | | | | 建议后续关闭,原Android远程框架,现已由网关控制 |
|
| 8088 | | | | | 建议后续关闭,原Android远程框架,现已由网关控制 |
|
||||||
| 8089 | | | | | 建议后续关闭,原Ktor后端服务,现已由网关控制 |
|
| 8089 | | | | | 建议后续关闭,原Ktor后端服务,现已由网关控制 |
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ docker image prune -f
|
|||||||
|
|
||||||
4. (2x客户端)登录
|
4. (2x客户端)登录
|
||||||
```
|
```
|
||||||
docker login http://s1.ronsunny.cn:13011
|
docker login ai.ronsunny.cn:13011
|
||||||
```
|
```
|
||||||
|
|
||||||
- Prometheus
|
- Prometheus
|
||||||
@@ -226,6 +226,14 @@ docker image prune -f
|
|||||||
|
|
||||||
## 五、其他
|
## 五、其他
|
||||||
|
|
||||||
|
docker 权限
|
||||||
|
|
||||||
|
sudo groupadd docker
|
||||||
|
|
||||||
|
sudo usermod -aG docker kaijihar
|
||||||
|
|
||||||
|
newgrp docker
|
||||||
|
|
||||||
### 旧部署
|
### 旧部署
|
||||||
|
|
||||||
后端
|
后端
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './iva';
|
export * from './iva';
|
||||||
|
export * from './license';
|
||||||
export * from './sca';
|
export * from './sca';
|
||||||
export * from './ticket';
|
export * from './ticket';
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { pyRequestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已分析的图片列表
|
||||||
|
*/
|
||||||
|
export async function refreshLicenseImageList(page = 1, pageSize = 10) {
|
||||||
|
return pyRequestClient.get('/llm/getLicenseImageList', {
|
||||||
|
params: { page, page_size: pageSize },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片分析任务
|
||||||
|
*/
|
||||||
|
export async function createLicenseImageTask(formData: FormData) {
|
||||||
|
return pyRequestClient.post('/llm/createLicenseImageTask', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'ic:round-remove-red-eye',
|
icon: 'ic:round-remove-red-eye',
|
||||||
authority: ['iva', 'sca', 'ysa', 'ticket'],
|
authority: ['iva', 'sca', 'ysa', 'ticket', 'license'],
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
order: 2,
|
order: 2,
|
||||||
title: $t('计算机视觉'),
|
title: $t('计算机视觉'),
|
||||||
@@ -59,6 +59,17 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
component: () => import('#/views/cv/ticket/index.vue'),
|
component: () => import('#/views/cv/ticket/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'LICENSE',
|
||||||
|
path: '/cv/license',
|
||||||
|
meta: {
|
||||||
|
authority: ['license'],
|
||||||
|
icon: 'mdi:certificate',
|
||||||
|
title: $t('证件照片分析'),
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
component: () => import('#/views/cv/license/index.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'CVAT',
|
name: 'CVAT',
|
||||||
path: '/cv/cvat',
|
path: '/cv/cvat',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: IFrameView,
|
component: IFrameView,
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:wall-fire',
|
icon: 'mdi:wall-fire',
|
||||||
iframeSrc: 'http://ai.ronsunny.cn:13010/',
|
iframeSrc: 'http://s1.ronsunny.cn:13010/',
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
title: 'RAG Flow',
|
title: 'RAG Flow',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onDeactivated, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Form, Input, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import * as api from '#/api';
|
||||||
|
|
||||||
|
const list = ref<any[]>([]);
|
||||||
|
const error = ref<null | string>(null);
|
||||||
|
const selectedItem = ref<any>(null);
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
error.value = null;
|
||||||
|
const res =
|
||||||
|
(await api.refreshLicenseImageList(page.value, pageSize.value)) || [];
|
||||||
|
list.value = res.items;
|
||||||
|
total.value = res.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTask() {
|
||||||
|
modalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(9);
|
||||||
|
const total = ref(0); // 总条数
|
||||||
|
// 计算总页数
|
||||||
|
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
||||||
|
|
||||||
|
function changePage(newPage) {
|
||||||
|
page.value = newPage;
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
async function selectItem(item: any) {
|
||||||
|
selectedItem.value = item;
|
||||||
|
refreshLineChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadList();
|
||||||
|
});
|
||||||
|
|
||||||
|
const showInfoStr = ref<Record<string, number | string>>({});
|
||||||
|
const showInfoStr2 = ref<Record<string, number | string>>({});
|
||||||
|
// key 映射表
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
// 身份证
|
||||||
|
side: '证件面',
|
||||||
|
name: '姓名',
|
||||||
|
gender: '性别',
|
||||||
|
ethnicity: '民族',
|
||||||
|
id_number: '身份证号',
|
||||||
|
birth_date: '出生日期',
|
||||||
|
address: '住址',
|
||||||
|
issuing_authority: '签发机关',
|
||||||
|
valid_period_start: '开始日期',
|
||||||
|
valid_period_end: '结束日期',
|
||||||
|
notes: '备注',
|
||||||
|
// 银行卡
|
||||||
|
bank_name: '发卡行',
|
||||||
|
card_number: '卡号',
|
||||||
|
card_holder: '持卡人',
|
||||||
|
expiry_date: '有效期',
|
||||||
|
card_type: '卡种',
|
||||||
|
result: '识别结果',
|
||||||
|
issuer_country: '发卡国家',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归替换函数
|
||||||
|
function translateKeys(obj: any, map: Record<string, string>): any {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map((item) => translateKeys(item, map));
|
||||||
|
} else if (obj !== null && typeof obj === 'object') {
|
||||||
|
const newObj: Record<string, any> = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
const newKey = map[key] || key; // 没映射就用原来的 key
|
||||||
|
newObj[newKey] = translateKeys(obj[key], map);
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
return obj; // 基本类型直接返回
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLineChart() {
|
||||||
|
const data = selectedItem.value;
|
||||||
|
showInfoStr.value = {
|
||||||
|
项目名: data.name,
|
||||||
|
上传时间: data.created_at,
|
||||||
|
文件名: data.file_name,
|
||||||
|
文件大小: `${data.size} MB`,
|
||||||
|
分辨率: data.resolution,
|
||||||
|
证件类型:
|
||||||
|
data.type === -1 ? '未知证件' : data.type === 0 ? '身份证' : '银行卡',
|
||||||
|
};
|
||||||
|
showInfoStr2.value = translateKeys(data.content, keyMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
// 离开路由时清理状态
|
||||||
|
selectedItem.value = null;
|
||||||
|
showInfoStr.value = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectName = ref('');
|
||||||
|
const fileName = ref('');
|
||||||
|
const selectedFile = ref<File | null>(null);
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
title: '新建证件照片分析任务',
|
||||||
|
class: 'w-[600px]',
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
message.warning('请选择证件照片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadFile();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
async function uploadFile() {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
message.warning('请选择证件照');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先关闭弹窗
|
||||||
|
modalApi.close();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', selectedFile.value);
|
||||||
|
formData.append('projectName', projectName.value);
|
||||||
|
await api.createLicenseImageTask(formData);
|
||||||
|
|
||||||
|
// 接口完成后再触发事件
|
||||||
|
message.success('分析任务完成');
|
||||||
|
loadList();
|
||||||
|
|
||||||
|
// 清空表单
|
||||||
|
selectedFile.value = null;
|
||||||
|
projectName.value = '';
|
||||||
|
fileName.value = '';
|
||||||
|
} catch {
|
||||||
|
message.error('上传失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFile() {
|
||||||
|
fileInputRef.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileChange(event: Event) {
|
||||||
|
const files = (event.target as HTMLInputElement).files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
selectedFile.value = files[0];
|
||||||
|
fileName.value = files[0].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-[90dvh] w-full flex-col">
|
||||||
|
<Modal>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="任务名称">
|
||||||
|
<Input v-model:value="projectName" placeholder="可为空,将取随机值" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="证件照片" required>
|
||||||
|
<div
|
||||||
|
@click="selectFile"
|
||||||
|
style="
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ fileName || '点击选择文件' }}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
ref="fileInputRef"
|
||||||
|
@change="handleFileChange"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
<BaseModal />
|
||||||
|
<div class="flex h-full w-full bg-gray-50">
|
||||||
|
<!-- 左侧:筛选 + 列表 -->
|
||||||
|
<div class="flex w-64 flex-col border-r bg-white p-4">
|
||||||
|
<!-- 按钮组 -->
|
||||||
|
<div class="mb-4 flex justify-between space-x-2">
|
||||||
|
<Button type="primary" @click="createTask" class="flex-1">
|
||||||
|
新建任务
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<!-- 列表 -->
|
||||||
|
<div class="flex-1 space-y-2 overflow-auto">
|
||||||
|
<div
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.v_id"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
|
||||||
|
:class="{ 'bg-gray-100': item.id === selectedItem?.id }"
|
||||||
|
>
|
||||||
|
<div class="text-base font-medium">{{ item.name }}</div>
|
||||||
|
<div class="text-sm text-gray-400">{{ item.created_at }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
:disabled="page === 1"
|
||||||
|
@click="changePage(page - 1)"
|
||||||
|
class="rounded border px-1"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span>第 {{ page }} 页 / 共 {{ totalPages }} 页</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:disabled="page === totalPages"
|
||||||
|
@click="changePage(page + 1)"
|
||||||
|
class="rounded border px-1"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:Tab 内容区 -->
|
||||||
|
<div class="flex flex-1 flex-col overflow-hidden p-6">
|
||||||
|
<div
|
||||||
|
v-if="!selectedItem"
|
||||||
|
class="flex h-full items-center justify-center text-gray-400"
|
||||||
|
>
|
||||||
|
请先选择左侧列表中的分析任务
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex h-full flex-col gap-4">
|
||||||
|
<!-- 主内容区域:左右结构 -->
|
||||||
|
<div class="flex flex-1 gap-4">
|
||||||
|
<!-- 左侧 -->
|
||||||
|
<div class="flex w-72 flex-col gap-4">
|
||||||
|
<!-- 视频基础信息展示 -->
|
||||||
|
<div
|
||||||
|
class="w-full rounded border bg-white p-4"
|
||||||
|
id="video_base_info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(value, key) in showInfoStr"
|
||||||
|
:key="key"
|
||||||
|
class="mb-2 flex text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
<div class="w-28 font-medium text-gray-900">
|
||||||
|
{{ key }}:
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 break-all text-gray-600">
|
||||||
|
{{ value || '—' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下:空白卡片 -->
|
||||||
|
<div class="flex-1 rounded border bg-white p-4">
|
||||||
|
<div
|
||||||
|
v-for="(value, key) in showInfoStr2"
|
||||||
|
:key="key"
|
||||||
|
class="mb-2 flex text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
<div class="w-32 font-medium text-gray-900">
|
||||||
|
{{ key }}:
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 break-all text-gray-600">
|
||||||
|
{{ value || '—' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧 -->
|
||||||
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
|
<!-- 上:左右两个图片显示 -->
|
||||||
|
<!-- 左图 -->
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full items-center justify-center rounded border bg-white p-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="selectedItem?.oss_url"
|
||||||
|
alt="左图"
|
||||||
|
class="h-[80dvh] w-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -94,15 +94,14 @@ async function uploadFile() {
|
|||||||
formData.append('file', selectedFile.value);
|
formData.append('file', selectedFile.value);
|
||||||
formData.append('projectName', projectName.value);
|
formData.append('projectName', projectName.value);
|
||||||
await api.createTicketImageTask(formData);
|
await api.createTicketImageTask(formData);
|
||||||
|
|
||||||
// 接口完成后再触发事件
|
|
||||||
message.success('分析任务完成');
|
|
||||||
loadList();
|
|
||||||
|
|
||||||
// 清空表单
|
// 清空表单
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
projectName.value = '';
|
projectName.value = '';
|
||||||
fileName.value = '';
|
fileName.value = '';
|
||||||
|
|
||||||
|
// 接口完成后再触发事件
|
||||||
|
message.success('分析任务完成');
|
||||||
|
loadList();
|
||||||
} catch {
|
} catch {
|
||||||
message.error('上传失败');
|
message.error('上传失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ const cv: WorkbenchQuickNavItem[] = [
|
|||||||
title: '仪评指标联分析',
|
title: '仪评指标联分析',
|
||||||
url: '/cv/ticket',
|
url: '/cv/ticket',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
color: '#3fb27f',
|
||||||
|
authority: ['license'],
|
||||||
|
icon: 'mdi:certificate',
|
||||||
|
title: '证件照片分析',
|
||||||
|
url: '/cv/license',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
color: '#3fb27f',
|
color: '#3fb27f',
|
||||||
icon: 'ion:bar-chart-outline',
|
icon: 'ion:bar-chart-outline',
|
||||||
|
|||||||
Reference in New Issue
Block a user