1
0
forked from erp-dev/erp

feat: search image by tencent tiia

This commit is contained in:
2026-01-18 14:44:29 +08:00
parent a39b9648a4
commit 7e4e5e9cc1
5 changed files with 290 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ from .views import (
stateflow, stateflow,
users, users,
print_count, print_count,
tiia,
) )
from .views.business.purchase import views as purchase_views from .views.business.purchase import views as purchase_views
from .views.business.sales import views as sales_views from .views.business.sales import views as sales_views
@@ -101,6 +102,9 @@ urlpatterns = [
# 产品图片上传 API # 产品图片上传 API
path('products/<int:product_id>/image/', product_image.ProductImageUploadView.as_view(), name='product_image_upload'), path('products/<int:product_id>/image/', product_image.ProductImageUploadView.as_view(), name='product_image_upload'),
# Tencent Cloud (TIIA) API
path('tiia/search-image/', tiia.TiiaSearchImageView.as_view(), name='tiia_search_image'),
# Stateflow API (使用 Router) # Stateflow API (使用 Router)
path('stateflow/', include(stateflow_router.urls)), path('stateflow/', include(stateflow_router.urls)),

View File

@@ -45,6 +45,90 @@ class SimpleRateLimiter:
time.sleep(self._min_interval - elapsed) time.sleep(self._min_interval - elapsed)
self._last_ts = time.monotonic() self._last_ts = time.monotonic()
def search_image_url_in_tencent_tiia(
*,
image_url: str,
limit: int | None = 10,
offset: int | None = 0,
match_threshold: int | None = None,
rate_limiter: SimpleRateLimiter | None = None,
) -> dict:
"""
Search an image by URL in Tencent Cloud TIIA gallery using SearchImage.
This is the counterpart of CreateImage (upload) and is typically used for "similar image search".
Required settings:
- TENCENTCLOUD_SECRET_ID
- TENCENTCLOUD_SECRET_KEY
- TENCENTCLOUD_TIIA_GROUP_ID
Optional settings:
- TENCENTCLOUD_TOKEN (STS token, if applicable)
- TENCENTCLOUD_TIIA_REGION (default: ap-guangzhou)
- TENCENTCLOUD_TIIA_ENDPOINT (default: tiia.tencentcloudapi.com)
Args:
image_url: Absolute URL to search.
limit/offset/match_threshold: Optional SearchImage request parameters.
rate_limiter: Optional in-process limiter to keep requests under QPS.
Returns:
Parsed JSON response dict from TencentCloud SDK.
"""
image_url = (image_url or '').strip()
if not image_url:
raise ValueError('image_url 不能为空')
parsed = urlparse(image_url)
if not (parsed.scheme and parsed.netloc):
raise ValueError('image_url 必须为绝对 URL包含 scheme 与 host')
group_id = (getattr(settings, 'TENCENTCLOUD_TIIA_GROUP_ID', '') or '').strip()
if not group_id:
raise ValueError('缺少 settings: TENCENTCLOUD_TIIA_GROUP_ID')
secret_id = (getattr(settings, 'TENCENTCLOUD_SECRET_ID', '') or '').strip()
secret_key = (getattr(settings, 'TENCENTCLOUD_SECRET_KEY', '') or '').strip()
token = (getattr(settings, 'TENCENTCLOUD_TOKEN', '') or '').strip() or None
if not secret_id or not secret_key:
raise ValueError('缺少 settings: TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY')
region = (getattr(settings, 'TENCENTCLOUD_TIIA_REGION', None) or 'ap-guangzhou').strip()
endpoint = (getattr(settings, 'TENCENTCLOUD_TIIA_ENDPOINT', None) or 'tiia.tencentcloudapi.com').strip()
try:
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.tiia.v20190529 import tiia_client, models
except Exception as e: # pragma: no cover
raise RuntimeError(
"TencentCloud SDK 未安装或不可用请先安装依赖tencentcloud-sdk-python"
) from e
if rate_limiter is not None:
rate_limiter.wait()
cred = credential.Credential(secret_id, secret_key, token)
http_profile = HttpProfile(endpoint=endpoint, reqTimeout=30)
client_profile = ClientProfile(httpProfile=http_profile)
client = tiia_client.TiiaClient(cred, region, client_profile)
req = models.SearchImageRequest()
req.GroupId = str(group_id)
req.ImageUrl = str(image_url)
if limit is not None:
req.Limit = int(limit)
if offset is not None:
req.Offset = int(offset)
if match_threshold is not None:
req.MatchThreshold = int(match_threshold)
resp = client.SearchImage(req)
return json.loads(resp.to_json_string())
def upload_image_url_to_tencent_tiia(*, image_url: str, entity_id: str) -> dict: def upload_image_url_to_tencent_tiia(*, image_url: str, entity_id: str) -> dict:
""" """
@@ -239,6 +323,7 @@ def upload_plate_order_images_to_tencent_tiia(*, plate_order_id: int, rate_limit
__all__ = [ __all__ = [
'SimpleRateLimiter', 'SimpleRateLimiter',
'upload_image_url_to_tencent_tiia', 'upload_image_url_to_tencent_tiia',
'search_image_url_in_tencent_tiia',
'upload_plate_order_images_to_tencent_tiia', 'upload_plate_order_images_to_tencent_tiia',
] ]

View File

@@ -0,0 +1,50 @@
"""
TIIA SearchImage simplified API tests.
"""
from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient
from unittest.mock import patch
User = get_user_model()
class TiiaSearchImageAPITestCase(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='test_tiia_search_user',
password='testpass123',
email='test_tiia_search@example.com',
)
def test_search_image_unauthenticated(self):
resp = self.client.post(
'/api/v1/tiia/search-image/',
data={'imageUrl': 'https://example.com/a.png'},
format='json',
)
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
@patch('api_v1.views.tiia.search_image_url_in_tencent_tiia')
def test_search_image_success(self, mock_search):
self.client.force_authenticate(self.user)
mock_search.return_value = {'Candidates': [{'EntityId': '1', 'Score': 99}]}
resp = self.client.post(
'/api/v1/tiia/search-image/',
data={'imageUrl': 'https://example.com/a.png'},
format='json',
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.json(), {'Candidates': [{'EntityId': '1', 'Score': 99}]})
def test_search_image_missing_image_url(self):
self.client.force_authenticate(self.user)
resp = self.client.post('/api/v1/tiia/search-image/', data={}, format='json')
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('error', resp.json())

57
api_v1/views/tiia.py Normal file
View File

@@ -0,0 +1,57 @@
"""
Tencent Cloud TIIA simplified APIs.
Currently provides:
- SearchImage (by ImageUrl) with fixed groupId/region from settings.
"""
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from django.conf import settings
from api_v1.utils.tencentcloud_tiia import SimpleRateLimiter, search_image_url_in_tencent_tiia
class TiiaSearchImageView(APIView):
"""
Tencent Cloud TIIA: SearchImage (simplified)
POST /api/v1/tiia/search-image/
Body:
{
"imageUrl": "https://example.com/xxx.png"
}
Notes:
- groupId/region/endpoint are configured via Django settings (TENCENTCLOUD_TIIA_*).
- Enforces QPS using settings.TENCENTCLOUD_TIIA_QPS (default 10).
"""
permission_classes = [IsAuthenticated]
def post(self, request):
image_url = (
request.data.get('imageUrl')
or request.data.get('ImageUrl')
or request.data.get('image_url')
or request.data.get('ImageURL')
)
try:
limiter = SimpleRateLimiter(qps=float(getattr(settings, 'TENCENTCLOUD_TIIA_QPS', 10)))
resp = search_image_url_in_tencent_tiia(
image_url=str(image_url or ''),
rate_limiter=limiter,
)
return Response(resp, status=status.HTTP_200_OK)
except ValueError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response(
{'error': '腾讯云搜图失败', 'message': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

View File

@@ -13,6 +13,9 @@
-`GET /api/v1/stateflow/business-objects/` 增加过滤条件:`state_id` + `param_key` + `param_value` -`GET /api/v1/stateflow/business-objects/` 增加过滤条件:`state_id` + `param_key` + `param_value`
- 增加 PostgreSQL JSONB 查询性能优化:为 `StateLogParameterRecord.parameters` 添加 GIN`jsonb_path_ops`)索引 - 增加 PostgreSQL JSONB 查询性能优化:为 `StateLogParameterRecord.parameters` 添加 GIN`jsonb_path_ops`)索引
- 补齐/更新测试用例,验证过滤行为与撤销记录处理 - 补齐/更新测试用例,验证过滤行为与撤销记录处理
- 输出该查询能力的单独说明文档(用途/出入参/示例/性能与索引)
- Shipment 模块:为 `Shipment` 增加 `area` 字段并贯通所有相关 APIcreate/update/返回/文档/测试)
- Printing/定时任务实现“PlateOrder 图片按 URL 上传到腾讯云图库”的能力(上传函数/按 plate_order_id 批量上传/每日 03:00 定时任务/限速/失败落库)
--- ---
@@ -49,3 +52,94 @@
另外,为保证 `stateflow` 测试集可持续运行,恢复了 `stateflow.services.clone_business_object()` 的实现(此前曾临时禁用导致测试失败)。 另外,为保证 `stateflow` 测试集可持续运行,恢复了 `stateflow.services.clone_business_object()` 的实现(此前曾临时禁用导致测试失败)。
### 4) 单独说明文档stateflow 查询能力)
新增文档:`docs/stateflow_business_objects_query_by_completed_state_and_param.md`,包含:
- 能力用途/背景(“已完成某节点 + 工艺参数 key/value”反查 `BusinessObject`
- 接口定义与查询参数(`state_id/param_key/param_value``content_type_str` 的配套用法)
- 过滤语义(排除撤销记录、参数绑定范围)
- 返回结构(与 `BusinessObjectListSerializer` 对齐)
- 性能建议GIN 索引与验证方式)
### 5) Shipment增加 area 字段并贯通 APIcreate/update/返回/文档/测试)
新增字段:
- `shipment.models.Shipment.area``CharField(max_length=30, blank=True, default='')`
- 迁移:`shipment/migrations/0008_add_area_to_shipment.py`
接口/序列化器调整:
- `api_v1/views/shipment/serializers.py`
- `ShipmentSerializer`:响应中新增 `area`
- `ShipmentCreateNormalSerializer` / `ShipmentCreateExternalSerializer`:支持写入 `area`
- 新增 `ShipmentUpdateSerializer`用于更新入口PATCH/PUT
- `api_v1/views/shipment/views.py`
- 普通创建与 external 创建:将 `area` 透传并落库
- `ShipmentDetailView`:新增 `PATCH/PUT /api/v1/shipment/shipments/<id>/`,支持更新 `area` 并回显(同时保持 merchant 隔离与 customer 校验)
- `shipment/services.py``create_shipment/create_external_shipment` 支持 `area` 并写入
测试与文档:
- `api_v1/views/shipment/test_api.py`:补齐 create 回显 + PATCH 更新回显用例
- `docs/shipment_api.md`:补齐 `area` 的请求/响应示例与字段说明
### 6) PrintingPlateOrder 图片上传腾讯云图库(函数 + 定时任务 + 限速 + 失败落库)
依赖:
- `pyproject.toml`:加入 `tencentcloud-sdk-python`TIIA SDK
上传函数(基础能力):
- `api_v1/utils/tencentcloud_tiia.py`
- `upload_image_url_to_tencent_tiia(image_url, entity_id)`:调用 TIIA `CreateImage`,通过 URL 上传图片到图库
- 腾讯云凭证/配置改为从 `flower/settings.py` 读取(支持 `.env` 注入)
- `SimpleRateLimiter`:简单限速器,确保不超过 10 次/秒(可由 `TENCENTCLOUD_TIIA_QPS` 配置)
按 PlateOrder 批量上传(包装函数):
- `api_v1/utils/tencentcloud_tiia.py`
- `upload_plate_order_images_to_tencent_tiia(plate_order_id, rate_limiter=...)`
-`PlateOrder.plate_image(JSONField)` 提取所有图片 URL/Path兼容 `url/path/imageUrl/字符串`),逐张上传
- `entity_id` 统一使用 `str(plate_order_id)`
每日定时任务03:00 跑“昨天创建的订单”):
- `printing/tasks.py`
- `upload_yesterday_plate_order_images_to_tencent_tiia`
- 扫描昨天创建的 PlateOrder逐单上传遇错记录失败 ID 并继续
- `flower/settings.py`
- `CELERY_BEAT_SCHEDULE` 新增 `daily_plate_order_tiia_image_upload`03:00
失败记录表(用于可追踪/可重试):
- `printing.models.PlateOrderTiiaUploadFailure`
- 字段:`run_date/plate_order_id/error/details/attempts/last_attempt_at`
- 唯一约束:`(run_date, plate_order_id)`,重复失败会累加 `attempts`
- 迁移:`printing/migrations/0031_plateorder_tiia_upload_failure.py`
- `printing/admin.py`:在 admin 中注册失败记录表,便于排查
测试:
- `api_v1/test_tencentcloud_tiia_plate_order_upload.py`mock uploader验证包装函数能提取并上传多张图片、entity_id 绑定正确
- `printing/test_tiia_upload_task.py`mock uploader + sleep验证“记录失败 ID 但不中断继续执行”
补充配置项settings
- `flower/settings.py`
- `TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY / TENCENTCLOUD_TOKEN`
- `TENCENTCLOUD_TIIA_GROUP_ID / TENCENTCLOUD_TIIA_REGION / TENCENTCLOUD_TIIA_ENDPOINT`
- `TENCENTCLOUD_TIIA_PIC_NAME_PREFIX`
- `TENCENTCLOUD_TIIA_QPS`(默认 10用于任务限速
### 7) 临时跑批(一次性全量上传现有 PlateOrder
为支持上线前手工验证/补数据,提供了可直接在 `manage.py shell` 中执行的一次性跑批脚本(遍历全部 `PlateOrder`,逐单上传 `plate_image` 里的所有图片,遇错记录失败 ID 并继续)。
说明:
- `entity_id` 使用 `plate_order_id`
- 通过 `SimpleRateLimiter(qps=10)` 遵守腾讯云限速
- 失败会写入 `PlateOrderTiiaUploadFailure`(便于后续排查与重试)