forked from erp-dev/erp
feat: search image by tencent tiia
This commit is contained in:
@@ -9,6 +9,7 @@ from .views import (
|
||||
stateflow,
|
||||
users,
|
||||
print_count,
|
||||
tiia,
|
||||
)
|
||||
from .views.business.purchase import views as purchase_views
|
||||
from .views.business.sales import views as sales_views
|
||||
@@ -100,6 +101,9 @@ urlpatterns = [
|
||||
|
||||
# 产品图片上传 API
|
||||
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)
|
||||
path('stateflow/', include(stateflow_router.urls)),
|
||||
|
||||
@@ -45,6 +45,90 @@ class SimpleRateLimiter:
|
||||
time.sleep(self._min_interval - elapsed)
|
||||
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:
|
||||
"""
|
||||
@@ -239,6 +323,7 @@ def upload_plate_order_images_to_tencent_tiia(*, plate_order_id: int, rate_limit
|
||||
__all__ = [
|
||||
'SimpleRateLimiter',
|
||||
'upload_image_url_to_tencent_tiia',
|
||||
'search_image_url_in_tencent_tiia',
|
||||
'upload_plate_order_images_to_tencent_tiia',
|
||||
]
|
||||
|
||||
|
||||
50
api_v1/views/test_tiia_search_image_api.py
Normal file
50
api_v1/views/test_tiia_search_image_api.py
Normal 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
57
api_v1/views/tiia.py
Normal 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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user