# _*_ coding : UTF-8 _*_ # @Time : 2025/02/13 19:20 # @UpdateTime : 2025/02/13 19:20 # @Author : sonder # @File : code.py # @Software : PyCharm # @Comment : 本程序 import os import time from datetime import datetime from typing import Optional import pandas as pd from elasticsearch.helpers import async_bulk from fastapi import APIRouter, Depends, Path, Request, Query from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse, FileResponse from annotation.log import Log from config.constant import BusinessType from config.env import ElasticSearchConfig from controller.login import LoginController from exceptions.exception import ServiceException, PermissionException from models import File, Code, QueryCode, QueryCodeLog from schemas.code import GetCodeInfoResponse, GetCodeListResponse, GetQueryCodeParams, \ DeleteCodeListParams, QueryCodeResponse, AddCodeParams, GetQueryCodeLogResponse, GetQueryCodeLogDetailResponse from schemas.common import BaseResponse from utils.log import logger from utils.response import Response codeAPI = APIRouter( prefix="/code", dependencies=[Depends(LoginController.get_current_user)] ) @codeAPI.get("/template", summary="获取上传编码模板") @Log(title="获取上传编码模板", business_type=BusinessType.SELECT) async def get_upload_template(request: Request): template_path = os.path.join(os.path.abspath(os.getcwd()), 'assets', 'templates', '上传模版.xlsx') if not os.path.exists(template_path): raise ServiceException(message="文件不存在!") return FileResponse( path=template_path, filename="上传模版.xlsx", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) @codeAPI.get("/queryTemplate", summary="获取查询编码模板") @Log(title="获取查询编码模板", business_type=BusinessType.SELECT) async def get_query_template(request: Request): template_path = os.path.join(os.path.abspath(os.getcwd()), 'assets', 'templates', '查询模版.xlsx') if not os.path.exists(template_path): raise ServiceException(message="文件不存在!") return FileResponse( path=template_path, filename="查询模版.xlsx", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) @codeAPI.post("/add", response_class=JSONResponse, response_model=BaseResponse, summary="添加编码") @Log(title="添加编码", business_type=BusinessType.INSERT) async def add_code(request: Request, params: AddCodeParams): params.code = params.code.replace(".", "").replace("/", "").replace("_", "").replace("-", "").replace("?", "").replace( ":", "").replace(":", "").replace("?", "").strip() if await Code.get_or_none(code=params.code): return Response.failure(msg="编码已存在") else: if await request.app.state.es.indices.exists(index=ElasticSearchConfig.ES_INDEX): await request.app.state.es.indices.create(index=ElasticSearchConfig.ES_INDEX, ignore=400) code = await Code.create( code=params.code, description=params.description ) if code: # 构造 Bulk 导入数据 actions = [ { "_index": ElasticSearchConfig.ES_INDEX, "_id": code.code, # 以 code 作为 ID "_source": { "code": code.code, "description": code.description } } ] success, failed = await async_bulk(request.app.state.es, actions) logger.info(f"成功导入 {success} 条数据,失败 {failed} 条") return Response.success(msg="添加成功") else: return Response.failure(msg="添加失败") @codeAPI.get("/addCode/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="导入编码") @Log(title="导入编码", business_type=BusinessType.INSERT) async def add_code_by_file(request: Request, id: str = Path(description="文件ID"), current_user=Depends(LoginController.get_current_user)): user_id = current_user.get("id") if file := await File.get_or_none(id=id): uploader_id = await file.first().values(id="uploader__id") if str(uploader_id["id"]) == user_id: try: df = pd.read_excel(file.absolute_path) for index, row in df.iterrows(): row["code"] = str(row["code"]).replace(".", "").replace("/", "").replace("_", "").replace("-", "").replace( "?", "").replace( ":", "").replace(":", "").replace("?", "").strip() await Code.create( code=row["code"], description=row["description"] ) if not await request.app.state.es.indices.exists(index=ElasticSearchConfig.ES_INDEX): await request.app.state.es.indices.create(index=ElasticSearchConfig.ES_INDEX, ignore=400) # 构造 Bulk 导入数据 actions = [ { "_index": ElasticSearchConfig.ES_INDEX, "_id": row["code"], # 以 code 作为 ID "_source": { "code": row["code"], "description": row["description"] } } for _, row in df.iterrows() ] success, failed = await async_bulk(request.app.state.es, actions) logger.info(f"成功导入 {success} 条数据,失败 {failed} 条") except ServiceException as e: logger.error(e.message) raise ServiceException(message="文件读取失败") return Response.success(msg="添加成功") else: raise PermissionException(message="权限不足") else: return Response.failure(msg="文件不存在") @codeAPI.delete("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除编码") @codeAPI.post("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除编码") @Log(title="删除编码", business_type=BusinessType.DELETE) async def delete_code_by_id(request: Request, id: str = Path(description="编码ID"), ): if code := await Code.get_or_none(id=id): await code.delete() await request.app.state.es.delete(index=ElasticSearchConfig.ES_INDEX, id=code.code) return Response.success(msg="删除成功") else: return Response.failure(msg="编码不存在") @codeAPI.delete("/deleteList", response_class=JSONResponse, response_model=BaseResponse, summary="批量删除编码") @codeAPI.post("/deleteList", response_class=JSONResponse, response_model=BaseResponse, summary="批量删除编码") @Log(title="批量删除编码", business_type=BusinessType.DELETE) async def delete_code_by_ids(request: Request, params: DeleteCodeListParams): for id in set(params.ids): if code := await Code.get_or_none(id=id): await code.delete() await request.app.state.es.delete(index=ElasticSearchConfig.ES_INDEX, id=code.code) return Response.success(msg="删除成功") @codeAPI.put("/update/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="更新编码") @codeAPI.post("/update/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="更新编码") @Log(title="更新编码", business_type=BusinessType.UPDATE) async def update_code(request: Request, params: AddCodeParams, id: str = Path(description="编码ID")): if code := await Code.get_or_none(id=id): code.code = params.code code.description = params.description await code.save() await request.app.state.es.update(index=ElasticSearchConfig.ES_INDEX, id=code.code, body={"doc": {"description": params.description}}) return Response.success(msg="更新成功") return Response.failure(msg="编码不存在") @codeAPI.get("/info/{id}", response_class=JSONResponse, response_model=GetCodeInfoResponse, summary="获取编码信息") @Log(title="获取编码信息", business_type=BusinessType.SELECT) async def get_code_info(request: Request, id: str = Path(description="编码ID")): if code := await Code.get_or_none(id=id).values( id="id", code="code", description="description", create_time="create_time", create_by="create_by", update_time="update_time", update_by="update_by" ): return Response.success(data=code) return Response.failure(msg="编码不存在") @codeAPI.get("/list", response_class=JSONResponse, response_model=GetCodeListResponse, summary="获取编码列表") @Log(title="获取编码列表", business_type=BusinessType.SELECT) async def get_code_list(request: Request, page: int = Query(default=1, description="页码"), pageSize: int = Query(default=10, description="每页数量"), code: Optional[str] = Query(default=None, description="编码"), description: Optional[str] = Query(default=None, description="商品描述")): filterArgs = { f'{k}__contains': v for k, v in { 'code': code, 'description': description }.items() if v } total = await Code.filter(**filterArgs).count() data = await Code.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).values( id="id", code="code", description="description", create_time="create_time", create_by="create_by", update_time="update_time", update_by="update_by" ) return Response.success(data={ "page": page, "pageSize": pageSize, "total": total, "result": data }) @codeAPI.post("/query", response_class=JSONResponse, response_model=QueryCodeResponse, summary="查询编码") @Log(title="查询编码", business_type=BusinessType.SELECT) async def get_code_list(request: Request, params: GetQueryCodeParams, current_user: dict = Depends(LoginController.get_current_user), ): start_time = time.time() user_id = current_user.get("id") if log := await QueryCodeLog.create( operator_id=user_id, query_count=0, result_count=0, cost_time=0, request_params=params.query_text, response_result={}, status=0, del_flag=0 ): description_list = set(params.query_text.split("\n")) query_count = 0 dataList = [] try: for description in description_list: if not description: continue query_count += 1 query = { "query": { "match": { "brief_description": { "query": description.strip(), "fuzziness": "AUTO" # 自动模糊匹配 } } }, "sort": [ { "_score": { # 按照匹配度排序 "order": "desc" # 降序 } } ] } matches = [] data = await request.app.state.es.search(index=ElasticSearchConfig.ES_INDEX, body=query, size=5) # 获取当前查询的最大 _score max_score = data["hits"].get("max_score", 1) # 处理每一条匹配结果 for hit in data["hits"]["hits"]: code = await Code.get_or_none(code=hit["_source"]["hts8"]) # 归一化匹配度,转换为百分比 match_rate = round((hit["_score"] / max_score) * 100, 2) # 归一化后计算百分比 # 将匹配结果添加到列表中 matches.append({ "id": code.id if code else None, "code": hit["_source"]["hts8"], # 获取商品编码 "description": hit["_source"]["brief_description"], # 获取商品描述 "match_rate": match_rate # 匹配度(百分比) }) query_code = await QueryCode.create( query_text=description.strip(), result_text=jsonable_encoder(matches), session_id=log.id ) dataList.append({ "id": query_code.id, "query_text": description.strip(), "result_text": jsonable_encoder(matches), "status": 1 if matches else 0, }) cost_time = float(time.time() - start_time) * 100 log.operator_id = user_id log.query_count = query_count log.result_count = len(dataList) log.cost_time = cost_time log.status = 1 if dataList else 0 log.response_result = jsonable_encoder(dataList) log.del_flag = 1 await log.save() return Response.success(data={ "id": log.id, "result_count": len(dataList), "query": params.query_text, "response_result": jsonable_encoder(dataList), "query_count": query_count, "cost_time": cost_time, "status": 1 if dataList else 0, "operation_time": log.operation_time }) except ServiceException as e: logger.error(e.message) await log.delete() raise ServiceException(message="查询失败!") return Response.failure(msg="查询失败!") @codeAPI.get("/query/{id}", response_class=JSONResponse, response_model=QueryCodeResponse, summary="查询编码") @Log(title="查询编码", business_type=BusinessType.SELECT) async def get_code_list(request: Request, id: str = Path(description="文件ID"), current_user: dict = Depends(LoginController.get_current_user), ): start_time = time.time() user_id = current_user.get("id") if file := await File.get_or_none(id=id): uploader_id = await file.first().values(id="uploader__id") if str(uploader_id["id"]) == user_id: if log := await QueryCodeLog.create( operator_id=user_id, query_count=0, result_count=0, cost_time=0, request_params="", response_result={}, status=0, del_flag=0 ): try: query_text = "" query_count = 0 dataList = [] df = pd.read_excel(file.absolute_path) for index, row in df.iterrows(): query_count += 1 query_text += row["text"] + "\n" query = { "query": { "match": { "brief_description": { "query": row["text"].strip(), "fuzziness": "AUTO" # 自动模糊匹配 } } }, "sort": [ { "_score": { # 按照匹配度排序 "order": "desc" # 降序 } } ] } matches = [] data = await request.app.state.es.search(index=ElasticSearchConfig.ES_INDEX, body=query, size=5) # 获取当前查询的最大 _score max_score = data["hits"].get("max_score", 1) # 处理每一条匹配结果 for hit in data["hits"]["hits"]: code = await Code.get_or_none(code=hit["_source"]["hts8"]) # 归一化匹配度,转换为百分比 match_rate = round((hit["_score"] / max_score) * 100, 2) # 归一化后计算百分比 # 将匹配结果添加到列表中 matches.append({ "id": code.id if code else None, "code": hit["_source"]["hts8"], # 获取商品编码 "description": hit["_source"]["brief_description"], # 获取商品描述 "match_rate": match_rate # 匹配度(百分比) }) query_code = await QueryCode.create( query_text=row['text'].strip(), result_text=jsonable_encoder(matches), session_id=log.id ) dataList.append({ "id": query_code.id, "query_text": row['text'].strip(), "result_text": jsonable_encoder(matches), "status": 1 if matches else 0, }) cost_time = float(time.time() - start_time) * 100 log.request_params = query_text log.operator_id = user_id log.query_count = query_count log.result_count = len(dataList) log.cost_time = cost_time log.status = 1 if dataList else 0 log.response_result = jsonable_encoder(dataList) log.del_flag = 1 await log.save() return Response.success(data={ "id": log.id, "result_count": len(dataList), "query": query_text, "response_result": jsonable_encoder(dataList), "query_count": query_count, "cost_time": cost_time, "status": 1 if dataList else 0, "operation_time": log.operation_time }) except ServiceException as e: logger.error(e.message) await log.delete() raise ServiceException(message="查询失败!") else: raise PermissionException(message="权限不足") else: return Response.failure(msg="文件不存在") @codeAPI.get("/logList", response_class=JSONResponse, response_model=GetQueryCodeLogResponse, summary="查询编码日志列表") @Log(title="查询编码日志列表", business_type=BusinessType.SELECT) async def get_code_log_list(request: Request, page: int = Query(default=1, description="当前页码"), pageSize: int = Query(default=10, description="每页数量"), startTime: Optional[str] = Query(default=None, description="开始时间"), endTime: Optional[str] = Query(default=None, description="结束时间"), current_user: dict = Depends(LoginController.get_current_user), ): user_id = current_user.get("id") if startTime and endTime: startTime = float(startTime) / 1000 endTime = float(endTime) / 1000 startTime = datetime.fromtimestamp(startTime) endTime = datetime.fromtimestamp(endTime) count = await QueryCodeLog.filter(operator_id=user_id, del_flag=1, operation_time__gte=startTime, operation_time__lte=endTime).count() data = await QueryCodeLog.filter(operator_id=user_id, del_flag=1, operation_time__gte=startTime, operation_time__lte=endTime).order_by("-operation_time").offset( (page - 1) * pageSize).limit(pageSize).values( id="id", query_count="query_count", result_count="result_count", cost_time="cost_time", operation_time="operation_time", status="status", request_params="request_params", response_result="response_result", create_time="create_time", update_time="update_time", operator_id="operator__id", operator_name="operator__username", operator_nickname="operator__nickname", department_id="operator__department__id", department_name="operator__department__name", ) else: count = await QueryCodeLog.filter(operator_id=user_id, del_flag=1).count() data = await QueryCodeLog.filter(operator_id=user_id, del_flag=1).order_by("-operation_time").offset( (page - 1) * pageSize).limit(pageSize).values( id="id", query_count="query_count", result_count="result_count", cost_time="cost_time", operation_time="operation_time", status="status", request_params="request_params", response_result="response_result", create_time="create_time", update_time="update_time", operator_id="operator__id", operator_name="operator__username", operator_nickname="operator__nickname", department_id="operator__department__id", department_name="operator__department__name", ) return Response.success(data={ "page": page, "pageSize": pageSize, "result": data, "total": count }) @codeAPI.get("/logInfo/{id}", response_class=JSONResponse, response_model=GetQueryCodeLogDetailResponse, summary="查询编码日志详情") @Log(title="查询编码日志详情", business_type=BusinessType.SELECT) async def get_code_log_detail(request: Request, id: str = Path(..., description="日志ID"), ): if log := await QueryCodeLog.get_or_none(id=id): data = await log.first().values( id="id", query_count="query_count", result_count="result_count", cost_time="cost_time", operation_time="operation_time", status="status", request_params="request_params", response_result="response_result", create_time="create_time", update_time="update_time", operator_id="operator__id", operator_name="operator__username", operator_nickname="operator__nickname", department_id="operator__department__id", department_name="operator__department__name", ) return Response.success(data=data) return Response.failure(msg="日志不存在!")