From 6da0d075ad6909641ca46fc643c0291c2df1d2b6 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 21 Feb 2025 16:49:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=BE=E7=89=87=E5=8F=96?= =?UTF-8?q?=E4=BB=B6=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/ocr.py | 32 +++++++ app/api/endpoints/order.py | 167 ++++++++++++++++++++----------------- app/core/ocr_service.py | 154 ++++++++++++++++++++++++++++++++++ app/main.py | 3 +- app/models/order.py | 19 ++++- 5 files changed, 294 insertions(+), 81 deletions(-) create mode 100644 app/api/endpoints/ocr.py create mode 100644 app/core/ocr_service.py diff --git a/app/api/endpoints/ocr.py b/app/api/endpoints/ocr.py new file mode 100644 index 0000000..e051f68 --- /dev/null +++ b/app/api/endpoints/ocr.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, Depends, UploadFile, File +from app.core.response import success_response, error_response, ResponseModel +from app.core.ocr_service import ocr_service +from app.api.deps import get_current_user +from app.models.user import UserDB + +router = APIRouter() + +@router.post("/pickup_code", response_model=ResponseModel) +async def recognize_pickup_code( + file: UploadFile = File(...), + current_user: UserDB = Depends(get_current_user) +): + """识别收件码图片""" + try: + # 检查文件类型 + if not file.content_type.startswith('image/'): + return error_response(code=400, message="只能上传图片文件") + + # 读取文件内容 + content = await file.read() + + # 调用OCR服务识别图片 + result = await ocr_service.recognize_pickup_code(content) + + if not result.get("stations") or not any(station["pickup_codes"] for station in result["stations"]): + return error_response(code=400, message="未能识别到取件码") + + return success_response(data=result) + + except Exception as e: + return error_response(code=500, message=f"识别失败: {str(e)}") \ No newline at end of file diff --git a/app/api/endpoints/order.py b/app/api/endpoints/order.py index a56d9c2..66aac5e 100644 --- a/app/api/endpoints/order.py +++ b/app/api/endpoints/order.py @@ -51,20 +51,23 @@ def calculate_price(price_request: OrderPriceCalculateRequest,user: UserDB,db: S OrderPriceResult: 包含价格信息和使用的优惠券/积分信息 """ # 计算所有包裹中的取件码总数 - package_count = sum( - # 如果package.pickup_codes是空字符串,则取0 - 0 if len(package.pickup_codes.split(',')) == 0 else len(package.pickup_codes.split(',')) - for package in price_request.packages - if package.pickup_codes - ) + if price_request.package_count > 0: + package_count = price_request.package_count + else: + package_count = sum( + # 如果package.pickup_codes是空字符串,则取0 + 0 if len(package.pickup_codes.split(',')) == 0 else len(package.pickup_codes.split(',')) + for package in price_request.packages + if package.pickup_codes + ) result = OrderPriceResult( price_info=OrderPriceInfo( package_count=package_count, - original_amount=settings.ORDER_BASE_PRICE, coupon_discount_amount=0, points_discount_amount=0, - final_amount=settings.ORDER_BASE_PRICE + original_amount=0, + final_amount=0 ), price_detail_text=settings.ORDER_PREORDER_PRICE_TEXT ) @@ -185,6 +188,7 @@ async def create_order( address_community_name=address.community_name, address_community_building_name=address.community_building_name, address_detail=address.address_detail, + pickup_images=order.price_request.pickup_images, package_count=price_info.package_count, original_amount=original_amount, coupon_discount_amount=coupon_discount, @@ -197,20 +201,21 @@ async def create_order( db.add(db_order) # 创建订单包裹 - for package in order.price_request.packages: - # 如果包裹有取件码,则创建包裹 - if len(package.pickup_codes) > 0: - station = db.query(StationDB).filter( - StationDB.id == package.station_id - ).first() + if order.price_request.packages: + for package in order.price_request.packages: + # 如果包裹有取件码,则创建包裹 + if len(package.pickup_codes) > 0: + station = db.query(StationDB).filter( + StationDB.id == package.station_id + ).first() - db_package = ShippingOrderPackageDB( - orderid=orderid, - station_id=package.station_id, - station_name=station.name, - pickup_codes=package.pickup_codes - ) - db.add(db_package) + db_package = ShippingOrderPackageDB( + orderid=orderid, + station_id=package.station_id, + station_name=station.name, + pickup_codes=package.pickup_codes + ) + db.add(db_package) try: # 如果使用了优惠券,更新优惠券状态 @@ -293,17 +298,6 @@ async def get_order_detail( if not order: return error_response(code=404, message="订单不存在") - - # 查询包裹信息,包含驿站名称 - packages = db.query( - ShippingOrderPackageDB, - StationDB.name.label('station_name') - ).join( - StationDB, - ShippingOrderPackageDB.station_id == StationDB.id - ).filter( - ShippingOrderPackageDB.orderid == orderid - ).all() # 如果有配送员 id,则获取配送员信息 if order.deliveryman_user_id: @@ -326,16 +320,32 @@ async def get_order_detail( # 计算配送员分账金额 deliveryman_share = round(order.original_amount * settings.ORDER_DELIVERYMAN_SHARE_RATIO, 1) - # 构建完成图片 - complete_images = [] - if order.complete_images: - complete_images = order.complete_images.split(",") - complete_images = [process_image(image).thumbnail(500, 500).format(ImageFormat.WEBP).build() for image in complete_images] + + # 查询包裹信息 + packages = db.query( + ShippingOrderPackageDB + ).filter( + ShippingOrderPackageDB.orderid == orderid + ).all() + + if packages: + # 构建包裹信息,包含驿站名称 + package_list = [{ + "id": p.id, + "orderid": p.orderid, + "station_id": p.station_id, + "station_name": p.station_name, + "pickup_codes": p.pickup_codes, + "create_time": p.create_time + } for p in packages] + else: + package_list = [] # 构建响应数据 order_data = { "orderid": order.orderid, "userid": order.userid, + "pickup_images": order.optimized_pickup_images, "package_count": order.package_count, "original_amount": order.original_amount, "coupon_discount_amount": order.coupon_discount_amount, @@ -343,7 +353,8 @@ async def get_order_detail( "final_amount": order.final_amount, "deliveryman_share": deliveryman_share, "status": order.status, - "complete_images": complete_images, + "complete_images": order.optimized_complete_images, + "packages": package_list, "create_time": order.create_time, "complete_time": order.completed_time, @@ -366,20 +377,11 @@ async def get_order_detail( "community_id": order.address_community_id, "community_name": order.address_community_name } + - # 构建包裹信息,包含驿站名称 - package_list = [{ - "id": p.ShippingOrderPackageDB.id, - "orderid": p.ShippingOrderPackageDB.orderid, - "station_id": p.ShippingOrderPackageDB.station_id, - "station_name": p.station_name, - "pickup_codes": p.ShippingOrderPackageDB.pickup_codes, - "create_time": p.ShippingOrderPackageDB.create_time - } for p in packages] return success_response(data={ - "order": order_data, - "packages": package_list + "order": order_data }) # 提供一个接口,传入community_id,返回订单状态数量 @@ -526,13 +528,15 @@ async def get_user_orders( ).all() # 格式化包裹信息 - package_list = [{ - "id": package.id, - "station_id": package.station_id, - "station_name": package.station_name, - "pickup_codes": package.pickup_codes - } for package in packages] - + if packages: + package_list = [{ + "id": package.id, + "station_id": package.station_id, + "station_name": package.station_name, + "pickup_codes": package.pickup_codes + } for package in packages] + else: + package_list = [] #查询子订单 sub_orders = db.query(PointProductOrderDB).filter( @@ -543,6 +547,7 @@ async def get_user_orders( "orderid": order.orderid, "userid": order.userid, "status": order.status, + "pickup_images": order.optimized_pickup_images, "package_count": order.package_count, "create_time": order.create_time, "delivery_method": order.delivery_method, @@ -706,22 +711,21 @@ async def get_deliveryman_orders( for order in results: # 查询订单包裹信息 packages = db.query( - ShippingOrderPackageDB, - StationDB.name.label('station_name') - ).join( - StationDB, - ShippingOrderPackageDB.station_id == StationDB.id + ShippingOrderPackageDB ).filter( ShippingOrderPackageDB.orderid == order.orderid ).all() # 格式化包裹信息 - package_list = [{ - "id": package.ShippingOrderPackageDB.id, - "station_id": package.ShippingOrderPackageDB.station_id, - "station_name": package.station_name, - "pickup_codes": package.ShippingOrderPackageDB.pickup_codes - } for package in packages] + if packages: + package_list = [{ + "id": package.id, + "station_id": package.station_id, + "station_name": package.station_name, + "pickup_codes": package.pickup_codes + } for package in packages] + else: + package_list = [] # 查询子订单 sub_orders = db.query(PointProductOrderDB).filter( @@ -732,9 +736,14 @@ async def get_deliveryman_orders( "orderid": order.orderid, "userid": order.userid, "status": order.status, + "pickup_images": order.optimized_pickup_images, "package_count": order.package_count, "create_time": order.create_time, "delivery_method": order.delivery_method, + "original_amount": order.original_amount, + "coupon_discount_amount": order.coupon_discount_amount, + "point_discount_amount": order.point_discount_amount, + "final_amount": order.final_amount, "packages": package_list, "sub_orders": [PointProductOrderInfo.model_validate(sub_order) for sub_order in sub_orders], "address": { @@ -1087,27 +1096,29 @@ async def get_admin_orders( for order in results: # 查询订单包裹信息 packages = db.query( - ShippingOrderPackageDB, - StationDB.name.label('station_name') - ).join( - StationDB, - ShippingOrderPackageDB.station_id == StationDB.id + ShippingOrderPackageDB ).filter( ShippingOrderPackageDB.orderid == order.orderid ).all() # 格式化包裹信息 package_list = [{ - "id": package.ShippingOrderPackageDB.id, - "station_id": package.ShippingOrderPackageDB.station_id, + "id": package.id, + "station_id": package.station_id, "station_name": package.station_name, - "pickup_codes": package.ShippingOrderPackageDB.pickup_codes + "pickup_codes": package.pickup_codes } for package in packages] + + # 查询子订单 + sub_orders = db.query(PointProductOrderDB).filter( + PointProductOrderDB.delivery_order_id == order.orderid + ).all() orders.append({ "orderid": order.orderid, "userid": order.userid, "status": order.status, + "pickup_images": order.optimized_pickup_images, "package_count": order.package_count, "create_time": order.create_time, "delivery_method": order.delivery_method, @@ -1116,13 +1127,17 @@ async def get_admin_orders( "point_discount_amount": order.point_discount_amount, "final_amount": order.final_amount, "packages": package_list, + "sub_orders": [PointProductOrderInfo.model_validate(sub_order) for sub_order in sub_orders], "address": { "name": order.address_customer_name, "phone": order.address_customer_phone, + "gender": order.address_customer_gender, + "community_id": order.address_community_id, "community_name": order.address_community_name, + "building_id": order.address_community_building_id, "building_name": order.address_community_building_name, "address_detail": order.address_detail - } + }, }) return success_response(data={ diff --git a/app/core/ocr_service.py b/app/core/ocr_service.py new file mode 100644 index 0000000..861ba91 --- /dev/null +++ b/app/core/ocr_service.py @@ -0,0 +1,154 @@ +from tencentcloud.common import credential +from tencentcloud.common.profile.client_profile import ClientProfile +from tencentcloud.common.profile.http_profile import HttpProfile +from tencentcloud.ocr.v20181119 import ocr_client, models +from app.core.config import settings +import json +import base64 + +class OCRService: + def __init__(self): + cred = credential.Credential(settings.TENCENT_SECRET_ID, settings.TENCENT_SECRET_KEY) + httpProfile = HttpProfile() + httpProfile.endpoint = "ocr.tencentcloudapi.com" + + clientProfile = ClientProfile() + clientProfile.httpProfile = httpProfile + self.client = ocr_client.OcrClient(cred, settings.TENCENT_REGION, clientProfile) + + async def recognize_pickup_code(self, image_content: bytes) -> dict: + """识别收件码图片""" + try: + # 将图片内容转为base64 + img_base64 = base64.b64encode(image_content).decode() + + req = models.GeneralAccurateOCRRequest() + req.ImageBase64 = img_base64 + + resp = self.client.GeneralAccurateOCR(req) + result = json.loads(resp.to_json_string()) + + print(result) + + # 解析文本内容 + text_list = [] + for item in result.get("TextDetections", []): + text_list.append(item["DetectedText"]) + + # 提取关键信息 + pickup_info = self._extract_pickup_info(text_list) + return pickup_info + + except Exception as e: + raise Exception(f"识别失败: {str(e)}") + + def _is_valid_pickup_code(self, text: str) -> bool: + """验证是否是有效的取件码格式""" + import re + # 匹配格式:xx-x-xxx 或 xx-xx-xxx 等类似格式 + patterns = [ + r'\b\d{1,2}-\d{1,2}-\d{2,3}\b', # 15-4-223 + r'\b\d{4,8}\b', # 普通4-8位数字 + ] + + for pattern in patterns: + if re.search(pattern, text): + return True + return False + + def _extract_pickup_info(self, text_list: list) -> dict: + """提取收件码信息""" + # 存储所有驿站信息 + stations = [] + current_station = None + current_codes = [] + + pickup_info = { + "stations": [], # 驿站列表 + "app_type": None # APP类型(菜鸟/京东等) + } + + # 识别APP类型 + app_keywords = { + "菜鸟": "CAINIAO", + "京东": "JD", + "顺丰": "SF" + } + + for text in text_list: + # 查找APP类型 + for keyword, app_type in app_keywords.items(): + if keyword in text: + pickup_info["app_type"] = app_type + break + + # 查找驿站名称 + is_station = False + if "驿站" in text: + is_station = True + elif "站点" in text: + is_station = True + elif "仓" in text: + is_station = True + elif "站" in text: + is_station = True + elif "分拨" in text: + is_station = True + elif "分拣" in text: + is_station = True + elif "分拨" in text: + is_station = True + + if is_station: + # 如果之前有未保存的驿站信息,先保存 + if current_station and current_codes: + stations.append({ + "station_name": current_station, + "pickup_codes": current_codes + }) + # 开始新的驿站信息收集 + current_station = text + current_codes = [] + + # 查找取件码 + if self._is_valid_pickup_code(text): + # 清理文本中的多余字符 + cleaned_text = ''.join(c for c in text if c.isdigit() or c == '-') + # 提取所有匹配的取件码 + import re + for pattern in [r'\d{1,2}-\d{1,2}-\d{2,3}', r'\d{4,8}']: + matches = re.finditer(pattern, cleaned_text) + for match in matches: + code = match.group() + # 如果已找到驿站,将取件码添加到当前驿站 + if current_station and code not in current_codes: + current_codes.append(code) + # 如果还没找到驿站,暂存取件码 + elif code not in current_codes: + current_codes.append(code) + + # 保存最后一个驿站的信息 + if current_station and current_codes: + stations.append({ + "station_name": current_station, + "pickup_codes": current_codes + }) + # 如果有未分配到驿站的取件码,创建一个默认驿站 + elif current_codes: + stations.append({ + "station_name": None, + "pickup_codes": current_codes + }) + + # 如果找到了取件码但没找到APP类型,根据取件码格式推测 + if stations and not pickup_info["app_type"]: + # 如果任一取件码包含连字符,判定为菜鸟 + for station in stations: + if any('-' in code for code in station["pickup_codes"]): + pickup_info["app_type"] = "CAINIAO" + break + + pickup_info["stations"] = stations + return pickup_info + +ocr_service = OCRService() \ No newline at end of file diff --git a/app/main.py b/app/main.py index f5698d4..b2f8a68 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order, coupon_activity +from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order, coupon_activity, ocr from app.models.database import Base, engine from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -58,6 +58,7 @@ app.include_router(message.router, prefix="/api/message", tags=["消息中心"]) app.include_router(upload.router, prefix="/api/upload", tags=["文件上传"]) app.include_router(config.router, prefix="/api/config", tags=["系统配置"]) app.include_router(log.router, prefix="/api/logs", tags=["系统日志"]) +app.include_router(ocr.router, prefix="/api/ai/ocr", tags=["图像识别"]) @app.get("/") diff --git a/app/models/order.py b/app/models/order.py index 1ff6986..18b96ee 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -51,6 +51,8 @@ class ShippingOrderDB(Base): address_community_building_name = Column(String(50), nullable=False, default='') # 楼栋名称快照 address_detail = Column(String(100), nullable=False, default='') # 详细地址快照 + # 取件图片 + pickup_images = Column(String(1000), nullable=True) # 取件图片URL,多个URL用逗号分隔 delivery_method = Column(Enum(DeliveryMethod), nullable=False) package_count = Column(Integer, nullable=False) @@ -81,6 +83,12 @@ class ShippingOrderDB(Base): if self.complete_images: return [process_image(image).quality(80).thumbnail(width=450, height=450).format(ImageFormat.WEBP).build() for image in self.complete_images.split(",")] return [] + + @property + def optimized_pickup_images(self): + if self.pickup_images: + return [process_image(image).quality(80).thumbnail(width=450, height=450).format(ImageFormat.WEBP).build() for image in self.pickup_images.split(",")] + return [] class ShippingOrderPackageDB(Base): __tablename__ = "shipping_order_packages" @@ -99,7 +107,9 @@ class OrderPackage(BaseModel): # 先定义 OrderPriceCalculateRequest class OrderPriceCalculateRequest(BaseModel): - packages: List[OrderPackage] + package_count: Optional[int] = None + pickup_images: Optional[str] = None + packages: Optional[List[OrderPackage]] = None # 然后再定义 OrderCreate class OrderCreate(BaseModel): @@ -123,6 +133,7 @@ class OrderInfo(BaseModel): address_detail: str address_customer_gender: Gender + pickup_images: Optional[str] = None package_count: int original_amount: float coupon_discount_amount: float @@ -165,12 +176,12 @@ class OrderPackageInfo(BaseModel): from_attributes = True class OrderPriceInfo(BaseModel): - package_count: int - original_amount: float + package_count: int = 0 + original_amount: float = 0 coupon_discount_amount: float = 0 points_discount_amount: float = 0 coupon_id: Optional[int] = None - final_amount: float + final_amount: float = 0 # 添加取消订单请求模型 class OrderCancel(BaseModel):