themblem/api/products/views.py
2025-10-29 21:27:29 +00:00

2020 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import tempfile
import uuid
import re
import requests
import datetime
import subprocess
import threading
import time
import base64
import hashlib
import mimetypes
from collections import defaultdict
from tastypie.resources import ModelResource
from .models import *
from tastypie.api import Api
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from django.http import HttpResponse, JsonResponse, FileResponse
from django.db.models import Q, Count
from django.db import transaction
from django.core.cache import cache
from django.views import View
from django.core.exceptions import *
from tastypie import fields
from tastypie.authorization import Authorization
from tastypie.serializers import Serializer
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.conf import settings
from random import randint
from .aliyun import *
from notify import send_sms_code
from .ip2region import *
from .miniprogram import mplink
from .sendmsg import admin_broadcast
from .article import gen_article_css
from .aichat import AIChatService
MANAGE_PY = os.path.join(settings.BASE_DIR, 'manage.py')
v1_api = Api(api_name='v1')
def get_ip(request):
if False:
# The current deployment does have proper X-Real-IP
realip = request.data.get("realip")
if realip:
return realip
headers = ['X-Real-IP', 'X-Forwarded-For']
for h in headers:
rip = request.headers.get(h)
if rip:
return rip
return request.META.get('REMOTE_ADDR', "0.0.0.0")
class ForeignKey(fields.ForeignKey):
def build_related_resource(self, value, request=None, related_obj=None, related_name=None):
if isinstance(value, str) and value.isnumeric():
value = int(value)
if isinstance(value, int):
value = { 'pk': value }
return super().build_related_resource(value, request, related_obj, related_name)
class AuthenticationError(Exception):
pass
class BaseSerializer(Serializer):
def format_datetime(self, data):
return data.timestamp()
def get_token(request):
auth_header = request.headers.get("Authorization", "")
if not auth_header:
token = request.GET.get("token")
if token:
return token
fs = auth_header.split(' ', maxsplit=1)
if len(fs) != 2:
return None
return fs[1]
def do_authenticate(request):
token = get_token(request)
request.admin = None
request.tenant = None
if token:
t = AuthToken.objects.filter(token=token).first()
if t and t.validate():
request.admin = t.admin
request.tenant = t.tenant
return True
return False
class BaseResource(ModelResource):
def build_schema(self):
ret = super().build_schema()
return self.patch_schema(ret)
def patch_schema(self, schema):
return schema
def get_schema(self, request, **kwargs):
if not self.auth(request):
return http401()
return super().get_schema(request, **kwargs)
def auth(self, request):
ac = getattr(self.Meta, 'auth_role', 'admin')
do_authenticate(request)
if ac is None:
return True
if ac == 'tenant':
ok = request.admin or request.tenant
elif ac == 'anonymous':
ok = True
else:
ok = request.admin
return ok
def dispatch(self, request_type, request, **kwargs):
request.request_type = request_type
if not self.auth(request):
return http401()
return super().dispatch(request_type, request, **kwargs)
def filter_by_q(self, request, qs):
q = request.GET.get('q')
if q:
qf = self.Meta.default_query_field
if isinstance(qf, str):
qf = [qf]
qobj = None
for f in qf:
if f.startswith("="):
kwargs = {'%s' % f[1:]: q}
else:
kwargs = {'%s__icontains' % f: q}
if not qobj:
qobj = Q(**kwargs)
else:
qobj = qobj | Q(**kwargs)
qs = qs.filter(qobj)
return qs
def get_object_list(self, request):
qs = self.Meta.queryset
if not request.admin and self.Meta.auth_role == 'tenant':
if hasattr(self.Meta, 'tenant_query_key'):
k = self.Meta.tenant_query_key
else:
k = 'tenant'
kwargs = { k: request.tenant }
qs = qs.filter(**kwargs)
return self.filter_by_q(request, qs)
def annotate_field(self, schema, field_name, verbose_name, type_="string"):
schema['fields'][field_name] = {
'verbose_name': verbose_name,
"blank": True,
"default": "",
"help_text": "",
"nullable": True,
"primary_key": False,
"readonly": True,
"type": type_,
"unique": False,
}
return schema
def patch_product_name_schema(self, schema):
self.annotate_field(schema, 'product__name', '产品')
return schema
class BaseAuthorization(Authorization):
pass
class GlobalConfigResource(BaseResource):
class Meta:
queryset = GlobalConfig.objects.all()
resource_name = 'config'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = ['name']
auth_role = 'admin'
class ArticleResource(BaseResource):
class Meta:
queryset = Article.objects.all().order_by("-pk")
resource_name = 'article'
authorization = BaseAuthorization()
auth_role = 'tenant'
default_query_field = 'title'
def hydrate(self, bundle):
bundle.obj.tenant = bundle.request.tenant
return super().hydrate(bundle)
def dehydrate(self, bundle):
if bundle.request.request_type == 'list':
del bundle.data['body']
return bundle
class AssetFileResource(BaseResource):
class Meta:
queryset = AssetFile.objects.all().order_by('-pk');
resource_name = 'asset-file'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = 'filename'
auth_role = 'tenant'
def hydrate(self, bundle):
bundle.obj.tenant = bundle.request.tenant
return super().hydrate(bundle)
def dehydrate(self, bundle):
obj = bundle.obj
url = "/api/v1/asset/?id=%d" % obj.pk
bundle.data['url'] = url
bundle.data['tenant__id'] = obj.tenant.pk if obj.tenant else None
bundle.data['display_field'] = 'filename'
return bundle
class TenantResource(BaseResource):
service_qr_file = ForeignKey(AssetFileResource, 'service_qr_file', null=True, blank=True, full=True)
class Meta:
queryset = Tenant.objects.all().order_by('-pk')
resource_name = 'tenant'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = ['name']
auth_role = 'admin'
def patch_schema(self, schema):
schema['fields']['service_qr_file']['verbose_name'] = '人工客服二维码文件'
schema['fields']['service_qr_file']['display_field'] = 'filename'
return schema
class ProductResource(BaseResource):
article = ForeignKey(ArticleResource, 'article', null=True, blank=True, full=True)
template_asset = ForeignKey(AssetFileResource, 'template_asset', null=True, blank=True, full=True)
class Meta:
queryset = Product.objects.all().order_by('-pk')
resource_name = 'product'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = 'name'
always_return_data = True
auth_role = 'tenant'
def hydrate(self, bundle):
bundle.obj.tenant = bundle.request.tenant
return super().hydrate(bundle)
def dehydrate(self, bundle):
obj = bundle.obj
if obj.article:
bundle.data['article__id'] = obj.article.pk
bundle.data['article__title'] = obj.article.title
if obj.template_asset:
bundle.data['template_asset__filename'] = obj.template_asset.filename
return bundle
def patch_schema(self, schema):
schema['fields']['description']['longtext'] = True
self.annotate_field(schema, 'article__title', '页面')
self.annotate_field(schema, 'template_asset__filename', '模板')
schema['fields']['template_asset']['verbose_name'] = '模板文件'
schema['fields']['template_asset']['display_field'] = 'filename'
schema['fields']['article']['verbose_name'] = '产品页面'
schema['fields']['article']['display_field'] = 'title'
return schema
class ProductPropertyResource(BaseResource):
file = ForeignKey(AssetFileResource, 'file', null=True, blank=True, full=True)
class Meta:
queryset = ProductProperty.objects.all().order_by('-pk')
resource_name = 'product-property'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = 'product__id'
tenant_query_key = 'product__tenant'
always_return_data = True
auth_role = 'tenant'
def patch_schema(self, schema):
schema['fields']['richtext']['richtext'] = True
schema['fields']['memo']['multiline'] = True
schema['fields']['file']['verbose_name'] = '文件'
schema['fields']['file']['display_field'] = 'filename'
return schema
def get_object_list(self, request):
r = super().get_object_list(request)
if 'product_id' in request.GET:
product = get_object_or_404(Product, tenant=request.tenant, pk=request.GET.get('product_id'))
return r.filter(product=product)
else:
return r
def hydrate(self, bundle):
tenant = bundle.request.tenant
bundle.obj.product = get_object_or_404(Product, pk=bundle.data.get('product_id'), tenant=tenant)
bundle.obj.tenant = tenant
return super().hydrate(bundle)
class CodeBatchResource(BaseResource):
tenant = ForeignKey(TenantResource, 'tenant', null=True, blank=True)
class Meta:
queryset = CodeBatch.objects.all().order_by('-pk')
resource_name = 'code-batch'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'tenant'
always_return_data = True
default_query_field = ['name']
def dehydrate(self, bundle):
obj = bundle.obj
bundle.data['code__count'] = obj.codes.count()
if obj.tenant:
bundle.data['tenant__username'] = obj.tenant.username
return bundle
def hydrate(self, bundle):
bundle.obj.tenant = bundle.request.tenant
return super().hydrate(bundle)
def patch_schema(self, schema):
schema['fields']['description']['longtext'] = True
self.annotate_field(schema, 'code__count', '序列码数量')
self.annotate_field(schema, 'tenant__username', '租户')
return self.patch_product_name_schema(schema)
class CodeResource(BaseResource):
batch = ForeignKey(CodeBatchResource, 'batch')
class Meta:
queryset = SerialCode.objects.all().order_by('-seq_num')
resource_name = 'code'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'tenant'
default_query_field = ['=code', '=seq_num']
filtering = {"code": ("exact", "in",), }
def dehydrate(self, bundle):
obj = bundle.obj
if obj.product:
bundle.data['product__name'] = obj.product.name
bundle.data['batch__name'] = obj.batch.name
bundle.data['qr_url'] = obj.get_qr_url()
return bundle
def patch_schema(self, schema):
self.annotate_field(schema, 'batch__name', '批次名', 'str')
return self.patch_product_name_schema(schema)
class CodeBatchOpRecordResource(BaseResource):
class Meta:
queryset = SerialCodeBatchOpRecord.objects.all().order_by('-pk')
resource_name = 'code-batch-op-record'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'tenant'
default_query_field = ['op_name', 'product_name', 'product_page_name', 'seq_num_begin', 'seq_num_end', 'code_samples']
class LogResource(BaseResource):
class Meta:
queryset = SystemLog.objects.all().order_by('-datetime')
resource_name = 'log'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'admin'
default_query_field = 'log'
class ScanDataResource(BaseResource):
class Meta:
queryset = ScanData.objects.all().order_by('-pk')
resource_name = 'scan-data'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'tenant'
default_query_field = ['id', 'consumer__username',
'product__name', 'ip',
'image',
'location', 'message',
'code', 'phone_model',
'labels'
]
def dehydrate(self, bundle):
obj = bundle.obj
bundle.data['product__name'] = obj.product.name if obj.product else None
return bundle
def patch_schema(self, schema):
schema = self.patch_product_name_schema(schema)
schema['fields']['message']['multiline'] = True
schema['fields']['client_log']['multiline'] = True
return schema
def get_object_list(self, request):
qs = super().get_object_list(request)
if 'include_labels' in request.GET:
labels = request.GET.get('include_labels').split(',')
for l in labels:
qs = qs.filter(labels__contains=l)
if 'exclude_labels' in request.GET:
labels = request.GET.get('exclude_labels').split(',')
for l in labels:
qs = qs.exclude(labels__contains=l)
return qs
class MessageResource(BaseResource):
class Meta:
queryset = UserMessage.objects.all().order_by('-pk');
resource_name = 'message'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = 'subject'
auth_role = 'tenant'
def get_object_list(self, request):
qs = self.Meta.queryset
assert request.admin or request.tenant
qs = qs.filter(admin=request.admin, tenant=request.tenant)
return self.filter_by_q(request, qs)
class JobResource(BaseResource):
class Meta:
queryset = Job.objects.all().order_by('-pk');
resource_name = 'job'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'tenant'
class MiniProgramResource(BaseResource):
class Meta:
queryset = MiniProgram.objects.all().order_by('-pk');
resource_name = 'mini-program'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'admin'
class CameraRuleResource(BaseResource):
class Meta:
queryset = CameraRule.objects.all().order_by('-pk');
resource_name = 'camera-rule'
authorization = BaseAuthorization()
serializer = BaseSerializer()
default_query_field = 'name'
auth_role = 'admin'
def patch_schema(self, schema):
schema['fields']['notes']['longtext'] = True
return schema
class ABTestResource(BaseResource):
class Meta:
queryset = ABTest.objects.all().order_by('-pk')
resource_name = 'abtest'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'admin'
class ABTestSampleResource(BaseResource):
class Meta:
queryset = ABTestSample.objects.all().order_by('-pk')
resource_name = 'abtestsample'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'admin'
class DataLabelResource(BaseResource):
class Meta:
queryset = DataLabel.objects.all().order_by('-pk')
resource_name = 'datalabel'
authorization = BaseAuthorization()
serializer = BaseSerializer()
auth_role = 'admin'
for cls in BaseResource.__subclasses__():
v1_api.register(cls())
def http302(loc):
r = HttpResponse("")
r['Location'] = loc
r.status_code = 302
return r
def http401():
r = HttpResponse("Unauthorized")
r.status_code = 401
return r
def http404(resp="Requested resource is not found"):
r = HttpResponse(resp)
r.status_code = 404
return r
def http400(msg="Invalid request"):
r = HttpResponse(msg)
r.status_code = 400
return r
def http405():
r = HttpResponse("Unsupported method")
r.status_code = 405
return r
def code_to_qr_std_key(code):
code = str(code)
pref = code[:2]
return f'v5/{pref}/{code}.jpg'
class BaseView(View):
name = ''
path_prefix = 'v1/'
auth_check = None
@classmethod
def get_path(self):
return self.path_prefix + self.name + '/'
def get_sha1(self, data):
if isinstance(data, str):
data = data.encode()
return hashlib.sha1(data).hexdigest()
def save_file(self, file_upload):
tf = tempfile.NamedTemporaryFile(dir="/var/tmp/", delete=False, suffix=file_upload.name)
for c in file_upload.chunks():
tf.write(c)
tf.flush()
return tf.name
def dispatch(self, request, *args, **kwargs):
if request.content_type == 'application/json':
try:
request.data = json.loads(request.read())
except:
request.data = {}
else:
request.data = request.POST
do_authenticate(request)
if self.auth_check == 'admin' and not request.admin:
return http401()
if self.auth_check == 'tenant' and not request.admin and not request.tenant:
return http401()
return super().dispatch(request, *args, **kwargs)
def get_qr_std(self, code):
return oss_get(code_to_qr_std_key(code), bucket=settings.OSS_QRS_BUCKET, endpoint=settings.OSS_QRS_ENDPOINT)
class UserInfoView(BaseView):
name = 'userinfo'
def get(self, request):
ret = {
'is_admin': False,
'username': None,
'email': None,
}
if request.admin:
ret['is_admin'] = True
ret['username'] = request.admin.user.username
ret['email'] = request.admin.user.email
else:
if not request.tenant:
return http401()
ret['username'] = request.tenant.username
return JsonResponse(ret)
class LoginView(BaseView):
name = 'login'
def post(self, request):
username = request.data.get("username")
password = request.data.get("password")
if not username or not password:
return http401()
user = authenticate(username=username, password=password)
if user:
ai, created = AdminInfo.objects.get_or_create(user=user)
log("admin login:", user.username)
token = AuthToken.objects.create(token=str(uuid.uuid4()), admin=ai)
token.update_expire()
token_for_estor = AuthToken.objects.filter(admin=ai, expire=None).first()
if not token_for_estor:
token_for_estor = AuthToken.objects.create(token=str(uuid.uuid4()), admin=ai)
return JsonResponse({"token": token.token, 'role': 'admin', 'id': user.pk, 'token_for_estor': token_for_estor.token})
else:
tenant = Tenant.authenticate(username, password)
if tenant:
log("tenant login:", tenant.username)
token = AuthToken.objects.create(token=str(uuid.uuid4()), tenant=tenant)
token.update_expire()
return JsonResponse({"token": token.token, 'role': 'tenant', 'id': tenant.pk})
return http401()
def get_image_from_data_url(data_url):
pref = "data:image/jpeg;base64,"
if data_url and data_url.startswith(pref):
return base64.b64decode(data_url[len(pref):]), "image.jpg"
pref = "data:image/png;base64,"
if data_url and data_url.startswith(pref):
return base64.b64decode(data_url[len(pref):]), "image.png"
def get_images(request):
f = request.data.get("image_data_urls")
if f:
return [get_image_from_data_url(d) for d in f]
return [get_image(request)]
def get_image(request):
f = request.FILES.get('photo') or request.FILES.get('file')
if f:
return f.read(), f.name
f = request.data.get("image_data_url")
if f:
return get_image_from_data_url(f)
return None, None
def get_code_from_url(url):
code = None
patterns = [
'[?&]code=([0-9a-zA-Z]+)',
'[?&]id=([0-9a-zA-Z]+)',
'[?&]c=([0-9a-zA-Z]+)',
'xy.ltd/v/([0-9a-zA-Z]+)'
]
for p in patterns:
m = re.search(p, url)
if m:
return m.group(1)
class QrVerifyView(BaseView):
name = 'qr-verify'
def do_v5_qr_verify(self, imgs, messages):
files = {}
for i, img in enumerate(imgs):
files[f'frame_{i}_{img[1]}'] = img[0]
resp = requests.post(
"https://themblem.com/api/v5/qr_verify",
files=files,
)
rd = resp.json()
messages.append(f"v5 qr_verify response: {rd}")
ok = rd.get("result", {}).get("predicted_class", 0) == 1
if not ok:
raise Exception("v5 qr verify failed")
def post(self, request):
image_name = ''
tenant = None
sd = ScanData(succeeded=False)
sd.message = ''
sd.phone_model = request.data.get("phonemodel")
sd.client_log = request.data.get("log")
messages = []
try:
qrcode = request.data.get("qrcode")
if not qrcode:
return http400("qrcode is missing")
code = get_code_from_url(qrcode)
if not code:
raise Exception("Unknown URL pattern: %s" % qrcode)
sd.code = code
sd.ip = get_ip(request)
sd.consumer = get_consumer(request.data.get('username'), sd.ip)
sd.location = ip_to_region(sd.ip)
sc = SerialCode.objects.filter(code=code).first()
if not sc or not sc.product:
raise Exception('No matching product found with code: %s' % code)
product = sc.product
if not sc.batch.is_active or not sc.is_active:
raise Exception("Inactive code")
sd.product = product
tenant = product.tenant
sd.tenant = tenant
sd.batch = sc.batch
imgs = get_images(request)
if not imgs:
return http400("image is missing")
img_data, filename = imgs[0]
image_name = 'scandata_v2/' + str(uuid.uuid4()) + '-' + filename
sd.image = image_name
if settings.ENV != "DEBUG" or os.environ.get("EMBLEM_ENABLE_OSS"):
t = threading.Thread(target=oss_put, args=(image_name, img_data))
t.run()
if sc.batch.feature_comparison_threshold > 0.01:
self.do_v5_qr_verify(imgs, messages)
else:
messages.append("skip v5 qr verify")
sd.succeeded = True
article_id = None
if product.article:
article_id = product.article.pk
count(product.tenant,
"qr-verify-ok", {
'image': image_name,
'product': product.pk,
})
resp = {
'id': product.pk,
'link': article_id,
'serial_code': code,
}
except Exception as e:
messages.append(str(e))
sd.save()
count(tenant, "qr-verify-fail", {
'image': image_name,
'reason': str(e),
})
resp = {
'error': 'qr verify failed: %s' % str(e),
'ref': sd.pk,
}
finally:
sd.message = "\n".join(messages) + "\n"
sd.save()
if sd.code and not settings.DEBUG:
cmd = [MANAGE_PY, 'qrrepeatalert', code]
subprocess.Popen(cmd)
return JsonResponse(resp)
class CheckAutoTorchView(BaseView):
name = 'check-auto-torch'
def get(self, request):
qrcode = request.GET.get('qrcode')
if not qrcode:
return http404()
code = get_code_from_url(qrcode)
if not code:
return http404("cannot find serial code from qrcode")
c = get_object_or_404(SerialCode, code=code)
b = c.batch
return JsonResponse({
"code": code,
"enable_auto_torch": b.enable_auto_torch,
"camera_sensitivity": b.camera_sensitivity,
})
class RequestPasswordResetView(BaseView):
name = 'request-password-reset'
def post(self, request):
tenant = get_object_or_404(Tenant, mobile=request.data.get('mobile'))
log("tenant request password reset:", tenant.username)
code = str(randint(100000, 999999))
tenant.password_reset_code = code
expire = timezone.now() + datetime.timedelta(minutes=5)
tenant.password_reset_code_expire = expire
tenant.save()
send_sms_code(tenant.mobile, code)
return JsonResponse({})
class RequestPasswordResetView(BaseView):
name = 'reset-password'
def post(self, request):
now = timezone.now()
tenant = get_object_or_404(Tenant,
mobile=request.data.get('mobile'),
password_reset_code=request.data.get('verify_code'),
password_reset_code_expire__gt=now)
if tenant:
log("tenant reset password:", tenant.username)
tenant.password = data.get('password')
tenant.save()
return JsonResponse({})
else:
return http401()
class ExportBatchView(BaseView):
name = 'export-batch'
auth_check = 'admin'
def post(self, request):
pk = request.data.get('id')
pattern = request.data.get('pattern')
batch = get_object_or_404(CodeBatch, pk=int(pk))
tf = tempfile.NamedTemporaryFile(prefix='export-batch-%s-' % pk, suffix='.txt', mode='w', delete=False)
job = Job.objects.create(name='export-batch.%s' % pk, admin=request.admin, output='file://' + tf.name)
cmd = [MANAGE_PY, 'exportbatch', '-b', str(pk), '-o', tf.name, '-j', str(job.pk)]
if pattern:
cmd += ['-P', pattern]
subprocess.Popen(cmd)
return JsonResponse({
'job': job.pk,
})
class JobOutputView(BaseView):
name = 'job-output'
auth_check = 'admin'
def get(self, request):
pk = request.GET.get('id')
job = get_object_or_404(Job, pk=pk, status="done")
if job.output.startswith("file://"):
fn = job.output[len("file://"):]
resp = FileResponse(open(fn, 'rb'), as_attachment=True)
else:
resp = HttpResponse(job.output)
return resp
class GenCodeView(BaseView):
name = 'gen-code'
auth_check = 'admin'
def post(self, request):
batch_id = request.data.get('batch_id')
batch = get_object_or_404(CodeBatch, pk=int(batch_id))
max_batch = 10000
n = int(request.data.get("num", max_batch))
if n < 0 or n > max_batch:
n = max_batch
batch.gen_code(n)
return JsonResponse({'created': n})
class ImportCodeView(BaseView):
name = 'import-code'
auth_check = 'admin'
def post(self, request):
import_file = request.FILES.get('file')
if not import_file:
return http400('File not found in request')
batch_id = request.POST.get('batch')
batch = get_object_or_404(CodeBatch, pk=batch_id)
fn = self.save_file(import_file)
job = Job.objects.create(name='import-code.%d' % batch.pk)
cmd = [MANAGE_PY, 'importcode', '-f', fn, '-b', str(batch.pk), '-j', str(job.pk)]
subprocess.Popen(cmd)
return JsonResponse({'job': job.pk})
class BindBatchView(BaseView):
name = 'bind-batch'
auth_check = 'admin'
def post(self, request):
batch_id = request.data.get('batch_id')
batch = get_object_or_404(CodeBatch, pk=int(batch_id))
tenant = request.data.get('tenant_username')
if tenant:
tenant = get_object_or_404(Tenant, username=tenant)
batch.tenant = tenant
batch.save()
else:
batch.tenant = None
batch.save()
job = Job.objects.create(name='bind-batch.%d' % batch.pk)
cmd = [MANAGE_PY, 'bindbatch', '-b', str(batch.pk), '-j', str(job.pk)]
subprocess.Popen(cmd)
return JsonResponse({'job': job.pk})
def get_article_body(t, raw):
content = t.body
if raw:
return content
if '<html>' in content:
return content
ret = """
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="/api/v1/article.css?id={pk}">
<body>
<div class="article">
{content}
</div>
</body>
</html>
""".format(content=content, pk=t.pk)
return ret
class ArticleView(BaseView):
name = 'article'
path_prefix = ''
def get(self, request):
pk = request.GET.get('id')
if not pk or not pk.isnumeric():
return http404()
t = get_object_or_404(Article, pk=int(pk))
if t.url:
return redirect(t.url)
body = get_article_body(t, request.GET.get("raw"))
return HttpResponse(body, content_type='text/html; charset=UTF-8')
class ProductInfoView(BaseView):
name = 'product-info'
path_prefix = ''
@classmethod
def get_urls(self):
from django.urls import re_path
return [
re_path(r'product-info/(?P<code>\w+)/(?P<path>.*)$', ProductInfoView.as_view()),
]
def get(self, request, code, path):
sc = get_object_or_404(SerialCode, code=code)
if not sc.product:
return http404()
if path.split('/')[:2] == ["api", "properties"]:
return self.handle_properties(sc)
article = sc.product.article
if article:
if article.url:
return redirect(article.url)
body = get_article_body(article, request.GET.get("raw"))
return HttpResponse(body, content_type='text/html; charset=UTF-8')
if sc.product.template_asset:
return self.handle_template(sc, path)
return http404()
def handle_properties(self, sc):
ret = {}
for p in sc.product.properties.all():
ret[p.name] = {
"text": p.text,
"richtext": p.richtext,
"file": f"/api/v1/asset/?id={p.file.pk}" if p.file else None,
"memo": p.memo,
}
return JsonResponse({
"product": sc.product.name,
"code": sc.code,
"properties": ret,
})
def handle_template(self, sc, path):
if not path:
path = "index.html"
asset = sc.product.template_asset
cache_base = '/var/tmp/emblem-product-cache/'
os.makedirs(cache_base, exist_ok=True)
assetd = cache_base + str(asset.pk)
if not os.path.exists(assetd):
try:
self.fetch_template_asset(asset, assetd)
except:
# maybe another request races with us, just continue if the
# files are created concurrently
if os.path.exists(assetd):
pass
else:
raise
requested_file = os.path.join(assetd, path)
print(requested_file)
if not os.path.exists(requested_file):
return http404()
return FileResponse(open(requested_file, 'rb'), content_type=self.get_content_type(requested_file))
def get_content_type(self, fname):
return mimetypes.guess_type(fname, strict=False)[0]
def fetch_template_asset(self, asset, dest):
with tempfile.TemporaryDirectory(dir="/var/tmp") as td:
workd = td + "/target"
os.makedirs(workd)
with open(td + "/tmp.zip", 'wb') as f:
f.write(oss_get(asset.get_name()))
subprocess.check_output(['unzip', '../tmp.zip'], cwd=workd)
os.unlink(td + '/tmp.zip')
os.rename(workd, dest)
class ServiceQrView(BaseView):
name = 'service-qr'
def get(self, request):
tenant_id = request.GET.get("tenant")
url = 'https://emblem-resources.oss-accelerate.aliyuncs.com/service-qr2.png'
if not tenant_id:
return redirect(url)
else:
tenant = get_object_or_404(Tenant, pk=tenant_id)
if tenant.service_qr_file:
url = oss_sign_url(tenant.service_qr_file.get_name(), "GET")
return redirect(url)
class StatsView(BaseView):
name = 'stats'
def admin_stats(self):
resp = {
'total_tenants': Tenant.objects.all().count(),
'total_products': Product.objects.all().count(),
'total_scans': ScanData.objects.all().count(),
}
return JsonResponse(resp)
def tenant_stats(self, tenant):
resp = {
'total_products': Product.objects.filter(tenant=tenant).count(),
'total_scans': ScanData.objects.filter(product__tenant=tenant).count(),
'total_batches': CodeBatch.objects.filter(tenant=tenant).count(),
'total_codes': SerialCode.objects.filter(batch__tenant=tenant).count(),
}
return JsonResponse(resp)
def get(self, request):
if request.admin:
return self.admin_stats()
if request.tenant:
return self.tenant_stats(request.tenant)
return http401()
class AssetView(BaseView):
name = 'asset'
@classmethod
def get_urls(self):
# Historically we accepted /api/api/v1/asset/ by mistake
# and it's used as a result of relative paths generated in tiny editor in the frontend
# So do a loose matching here to accompodate for it
from django.urls import re_path
return [
re_path(r'.*v1/asset/$', AssetView.as_view()),
]
@transaction.atomic
def post(self, request):
if not request.admin and not request.tenant:
return http401()
p = request.POST
pk = p.get('id')
if not pk:
pk = p.get('pk')
f = request.FILES.get('file')
mt = p.get('mime_type', 'application/octet-stream')
u = str(uuid.uuid4())
if pk and pk.isnumeric():
if request.admin:
obj = get_object_or_404(AssetFile, pk=int(pk))
else:
obj = get_object_or_404(AssetFile, pk=int(pk), tenant=request.tenant)
if mt:
obj.mime_type = mt
obj.filename = f.name
if f:
obj.uuid = u
obj.save()
else:
obj = AssetFile.objects.create(mime_type=mt, filename=f.name, uuid=u, tenant=request.tenant)
oss_put(obj.get_name(), f)
resp = {
'location': '/api/v1/asset/?id=%d' % obj.pk,
}
return JsonResponse(resp)
def get(self, request):
pk = request.GET.get("id")
if not pk or not pk.isnumeric():
return http404()
obj = get_object_or_404(AssetFile, pk=int(pk))
return redirect(oss_sign_url(obj.get_name(), "GET"))
class ImageListView(BaseView):
name = 'image-list'
auth_check = 'tenant'
def get(self, request):
q = AssetFile.objects.all().filter(tenant=request.tenant)
ret = []
for i in q:
if i.is_image():
ret.append({'title': i.filename, 'value': '/api/v1/asset/?id=%d' % i.pk})
return JsonResponse({
"images": ret,
})
class OssImageView(BaseView):
name = 'oss-image'
auth_check = 'admin'
def get(self, request):
name = request.GET.get('name')
if not name:
return http404()
img_bytes = oss_get(name)
return HttpResponse(img_bytes, content_type='image/jpeg')
class BackupView(BaseView):
name = 'backup'
auth_check = 'admin'
def get(self, request):
cmd = [MANAGE_PY, 'dumpdata']
data = subprocess.check_output(cmd)
return HttpResponse(data, headers={
'Content-Type': "application/json",
'Content-Disposition': 'attachment; filename="backup.json"',
})
class RestoreView(BaseView):
name = 'restore'
auth_check = 'admin'
def post(self, request):
f = request.FILES.get("file")
if not f:
return http400()
with tempfile.NamedTemporaryFile(suffix="backup.json") as tf:
for chunk in f.chunks():
tf.write(chunk)
tf.flush()
cmd = [MANAGE_PY, 'loaddata', tf.name]
subprocess.check_output(cmd)
return HttpResponse()
class HealthzView(BaseView):
name = 'healthz'
def get(self, request):
return JsonResponse({"status": "running"})
class MyLocationView(BaseView):
name = 'my-location'
def get(self, request):
return JsonResponse({
'ip': get_client_ip(request),
'location': request_to_region(request),
})
class CounterHistoryView(BaseView):
name = 'counter-history'
auth_check = 'tenant'
def align_to_unit(self, t, unit):
if unit == 'minute':
return t.replace(microsecond=0, second=0)
if unit == 'hour':
return t.replace(microsecond=0, second=0, minute=0)
if unit == 'day':
return t.replace(microsecond=0, second=0, minute=0, hour=0)
return t
def get(self, request):
name = request.GET.get('name')
since = request.GET.get('since')
until = request.GET.get('until')
try:
ts = int(since)
since_t = datetime.datetime.fromtimestamp(ts, timezone.utc)
ts = int(until)
until_t = datetime.datetime.fromtimestamp(ts, timezone.utc)
except Exception as e:
return http400()
params = request.GET.get('params')
unit = request.GET.get('unit', 'minute')
if unit == 'minute':
interval_secs = 60
elif unit == 'hour':
interval_secs = 3600
elif unit == 'day':
interval_secs = 3600 * 24
elif unit == 'week':
interval_secs = 3600 * 24 * 7
else:
return http400('invalid unit')
since_t = self.align_to_unit(since_t, unit)
total_secs = (until_t - since_t).total_seconds()
r = get_history_counts(request.tenant, name, since_t, interval_secs, total_secs, params)
return JsonResponse({
'data': [(a.timestamp(), b) for a, b in r],
})
def tid_by_code(sc):
if sc.tenant:
return str(sc.tenant.pk)
def mini_prog_entry_redirect(code, tid):
sc = get_object_or_404(SerialCode, code=code)
if sc.batch.scan_redirect_url:
return redirect(sc.batch.scan_redirect_url)
query = 'tenant=' + str(tid)
key = 'mini-prog-entry.' + query
path = sc.batch.mini_prog_entry_path or 'pages/index/index'
env_version = sc.batch.mini_prog_entry_env_version or 'release'
if path != "/pages/index/index":
# the mplink path setting may not work for some reason, so we use
# redirect param of the index page as a fallback
query += '&redirect=' + path
# passing path other than /pages/index/index to mplink may get an error, use
# the redirect param of the index page as a fallback for now
url = cache.get_or_set(key, lambda: mplink(query, "/pages/index/index", env_version), 300)
return redirect(url)
class MiniProgEntryView(BaseView):
name = 'mini-prog-entry'
path_prefix = ''
def get(self, request):
code = request.GET.get('code')
if not code:
return http404()
sc = get_object_or_404(SerialCode, code=code)
tid = request.GET.get('tenant') or tid_by_code(sc)
return mini_prog_entry_redirect(code, tid)
class MiniProgEntryVerifyView(BaseView):
name = 'mini-prog-entry/aF6EKr9xMu.txt'
path_prefix = ''
@classmethod
def get_urls(self):
from django.urls import re_path
return [
re_path(r'mini-prog-entry/aF6EKr9xMu.txt', MiniProgEntryVerifyView.as_view()),
]
def get(self, request):
return HttpResponse('bd371601b5b23b418aa3cd1b18dacb51')
class MiniProgEntryVView(View):
def get(self, request):
fp = request.get_full_path()
fs = fp.split('/')
if len(fs) > 2 and fs[1] == 'v':
code = fs[2]
else:
return http404('pattern not recognized')
sc = get_object_or_404(SerialCode, code=code)
tid = request.GET.get('tenant') or tid_by_code(sc)
return mini_prog_entry_redirect(code, tid)
class MiniProgContentView(BaseView):
name = 'mini-program-content'
def post(self, request):
if not request.tenant and not request.admin:
return http401()
mpc, _ = MiniProgramContent.objects.get_or_create(tenant=request.tenant)
mpc.content = json.dumps(request.data.get("content"))
mpc.save()
return JsonResponse({
'id': mpc.pk,
})
def get(self, request):
pk = request.GET.get('tenant')
code = request.GET.get('code')
if code:
sc = get_object_or_404(SerialCode, code=code)
if not sc.tenant:
return http404('code has no tenant')
pk = str(sc.tenant.pk)
if not pk or not pk.isnumeric():
x = get_object_or_404(MiniProgramContent, tenant=None)
else:
x = get_object_or_404(MiniProgramContent, tenant__pk=pk)
data = {
'id': x.pk,
'content': x.get_content(),
}
return JsonResponse(data)
class CodeBatchOp(object):
def __init__(self, request):
op = request.data.get("operation", {})
self.op = op
self.op_name = ''
self.product = None
self.update_field = []
if 'product' in op:
self.product = Product.objects.get(tenant=request.tenant, pk=op['product'])
self.op_name = '绑定产品'
self.update_field = 'product'
if 'unbind' in op:
self.op_name = '取消绑定'
self.update_field = 'product'
if 'is_active' in op:
val = op['is_active']
self.update_field = 'is_active'
if val:
self.op_name = '激活'
else:
self.op_name = '冻结'
if not self.op_name:
raise Exception("Invalid op")
def apply(self, q):
op = self.op
if 'product' in op:
q.update(product=self.product)
if 'unbind' in op:
q.update(product=None)
if 'is_active' in op:
q.update(is_active=op['is_active'])
class CodeBatchOpView(BaseView):
name = 'code-batch-op'
auth_check = 'tenant'
def post(self, request):
if not request.tenant:
raise ValidationError()
try:
op = CodeBatchOp(request)
except Exception as e:
raise
return http400(str(e))
ret = 0
dry_run = request.data.get("dry_run")
query = self.make_query(request)
ret = query.count()
if dry_run:
return JsonResponse({
'count': query.count(),
})
job = Job.objects.create(name='batch-op', tenant=request.tenant)
cmd = [MANAGE_PY, 'codebatchop', '-j', str(job.pk), '-t', str(request.tenant.pk)]
rd = request.data
if rd.get('all'):
cmd += ['--all']
elif rd.get('seq_range'):
sr = rd['seq_range']
cmd += ['--seq-range', "%d,%d" % (sr[0], sr[1])]
elif rd.get('codes'):
tf = tempfile.NamedTemporaryFile(delete=False, prefix='code-batch-op', mode='w')
tf.write('\n'.join(rd['codes']))
tf.flush()
cmd += ['--code-file', tf.name]
reqop = rd['operation']
if 'product' in reqop:
cmd += ['--bind-product', reqop['product']]
elif 'unbind' in reqop:
cmd += ['--unbind-product']
elif 'is_active' in reqop:
if reqop['is_active']:
cmd += ['--activate']
else:
cmd += ['--deactivate']
cmd = [str(x) for x in cmd]
print(cmd)
subprocess.Popen(cmd)
samples = [x.code for x in query[:10]]
self.record(request, op, ret, samples)
return JsonResponse({
'job_id': job.pk,
})
def record(self, request, op, n, code_samples):
rec = SerialCodeBatchOpRecord(
tenant=request.tenant,
batch_size=n)
rg = request.data.get('seq_range')
if code_samples:
rec.code_samples = ", ".join(code_samples)
if rg:
rec.seq_num_begin = rg[0]
rec.seq_num_end = rg[1]
rec.op_name = op.op_name
if op.product:
rec.product_name = op.product.name
if op.product.article:
rec.product_page_name = op.product.article.title
rec.op_name = op.op_name
rec.save()
def make_query(self, request):
tenant = request.tenant
data = request.data
q = SerialCode.objects.filter(tenant=tenant)
if data.get('all'):
return q
rg = data.get('seq_range', [])
if rg:
return q.filter(seq_num__gte=rg[0], seq_num__lte=rg[1])
return q.filter(code__in=data.get('codes', []))
class MessageCountView(BaseView):
name = 'message-count'
auth_check = 'tenant'
def get(self, request):
assert request.admin or request.tenant
q = UserMessage.objects.filter(admin=request.admin, tenant=request.tenant)
return JsonResponse({
'new': q.filter(read=False).count(),
'total': q.count(),
})
class SetEmailView(BaseView):
name = 'set-email'
auth_check = 'admin'
def post(self, request):
u = request.admin.user
u.email = request.data.get('email')
u.save()
return JsonResponse({})
class SetQrVerifyAlertView(BaseView):
name = 'qr-verify-alert'
auth_check = 'admin'
def post(self, request):
admin = request.admin
time_window_seconds = request.data.get("time_window_seconds")
repeat_threshold = request.data.get("repeat_threshold")
if not isinstance(time_window_seconds, int) or not isinstance(repeat_threshold, int):
return http400()
admin.qr_verify_alert_rule = json.dumps({
'time_window_seconds': int(time_window_seconds),
'repeat_threshold': int(repeat_threshold),
})
admin.save()
return JsonResponse({})
def get(self, request):
ret = json.loads(request.admin.qr_verify_alert_rule or '{}')
return JsonResponse(ret)
class ManualVerifyRequestView(BaseView):
name = 'manual-verify-request'
def post(self, request):
name = request.data.get("name")
contact = request.data.get("contact")
comments = request.data.get("comments", '')
sdpk = request.data.get("ref")
if sdpk:
sd = get_object_or_404(ScanData, pk=sdpk)
subject = "人工验证请求 (采集失败,扫码记录%d)" % sdpk
lines = [
"多次验证失败,用户请求人工验证"
"",
"用户姓名: %s" % name,
"IP %s" % get_ip(request),
"位置: %s" % request_to_region(request),
"序列号: %s" % (sd.code or "n/a"),
"时间: %s" % sd.datetime,
"产品: %s" % (sd.product.name if sd.product else "n/a"),
"租户: %s" % (sd.tenant.username if sd.tenant else "n/a"),
"图像文件名: %s" % sd.image,
"扫码记录ID %d" % sd.pk,
"",
"联系方式: %s" % contact,
"备注: %s" % comments,
]
else:
subject = "人工验证请求 (客户端未能采集)"
lines = [
"无法采集,用户请求人工验证"
"",
"用户姓名: %s" % name,
"IP %s" % get_ip(request),
"位置: %s" % request_to_region(request),
"",
"联系方式: %s" % contact,
"备注: %s" % comments,
]
cmd = [MANAGE_PY, 'sendmsg', '-A', '-s', subject, '-c', "\n".join(lines)]
subprocess.Popen(cmd)
return JsonResponse({})
class ArticleCssView(BaseView):
name = 'article.css'
def get(self, request):
pk = request.GET.get('id')
article = get_object_or_404(Article, pk=pk)
css = gen_article_css(article)
return HttpResponse(css, content_type='text/css')
@classmethod
def get_path(self):
return self.path_prefix + self.name
class ErrorView(BaseView):
name = '_error'
def get(self, request):
raise Exception("Error view")
class ExportScanDataView(BaseView):
name = 'export-scan-data'
auth_check = 'admin'
def post(self, request):
hours = request.data.get('hours')
ts = time.time()
tf = '/var/tmp/export-scan-data-%s.zip' % ts
job = Job.objects.create(name='export-scan-data-.%s' % ts, admin=request.admin, output='file://' + tf)
cmd = [MANAGE_PY, 'exportscandata', '-o', tf, '-j', str(job.pk),
'--hours', str(hours),
]
subprocess.Popen(cmd)
return JsonResponse({
'job': job.pk,
})
class ScanStatsByProvinceView(BaseView):
name = 'scan-stats-by-province'
auth_check = 'tenant'
def get(self, request):
try:
hours = int(request.GET.get('hours'))
except Exception as e:
print(e)
hours = 1
begin = timezone.now() - datetime.timedelta(hours=hours)
x = ScanData.objects.filter(datetime__gt=begin, location__isnull=False).values('location').annotate(total=Count('location')).order_by('total')
ret = defaultdict(int)
for r in x:
loc = r['location']
if '-' in loc:
prov = loc.split('-')[0]
ret[prov] += r['total']
return JsonResponse(ret)
class QrStdView(BaseView):
name = 'qr-std'
auth_check = 'admin'
def get(self, request):
c = request.GET.get("code")
qr = self.get_qr_std(c)
if qr:
return HttpResponse(qr, content_type="image/jpeg")
return http404()
def parse_estor_params(headers):
ret = {}
for k, v in headers.items():
if k.startswith("HTTP_X_ESTOR_"):
try:
ret[k] = base64.b64decode(v).decode()
except:
ret[k] = v
else:
ret[k] = v
return ret
class QrStdUploadView(BaseView):
name = 'qr-std-upload'
auth_check = None
def post(self, request):
files = request.FILES
for f in files.values():
try:
QrStdUploadView.save_qr(f.name, f.read())
except Exception as e:
return http400(f'{f.name}: {str(e)}')
return JsonResponse({
'ok': True,
'nfiles': len(files),
})
def save_qr(fname, body):
basename = os.path.basename(fname)
name, ext = os.path.splitext(basename)
if ext != ".jpg":
raise Exception("invalid file extension")
if len(body) > 1024 * 1024 * 10:
raise Exception("file too large")
if len(name) < 2:
raise Exception("invalid file name")
key = code_to_qr_std_key(name)
oss_put(key, body, bucket=settings.OSS_QRS_BUCKET, endpoint=settings.OSS_QRS_ENDPOINT)
class EstorArchive(BaseView):
name = 'estor-archive'
auth_check = None
def post(self, request):
rh = parse_estor_params(request.META)
path = rh.get('HTTP_X_ESTOR_PATH')
if not path:
return http400("X-Estor-Path not set")
path = path.lstrip('/')
token = rh.get('HTTP_X_ESTOR_PARAM_EMBLEM_TOKEN')
t = AuthToken.objects.filter(token=token).first()
if not t or not t.admin:
return http401()
created = self.archive_file(path, request.body)
return JsonResponse({
'status': 'created' if created else 'existing',
})
def archive_file(self, filename, data):
filename_sha1 = self.get_sha1(filename)
data_sha1 = self.get_sha1(data)
er, created = EstorArchiveRecord.objects.get_or_create(filename=filename, filename_sha1=filename_sha1)
if created or er.data_sha1 != data_sha1:
oss_put(filename, data, bucket=settings.ARCHIVE_BUCKET)
er.data_sha1 = data_sha1
er.save()
return True
return False
class SmsVerifyRequestView(BaseView):
name = 'sms-verify-request'
auth_check = 'admin'
def post(self, request):
try:
action = request.data['action']
if not action:
return http400()
mobile = request.admin.mobile
code = str(randint(1000, 9999))
r = SmsVerifiedAction.objects.create(admin=request.admin, verify_code=code, action=action)
send_sms_code(mobile, code)
return JsonResponse({"ok": True, "request_id": r.pk })
except Exception as e:
raise
return JsonResponse({"ok": False, "error": str(e) })
class SmsVerifyCommitView(BaseView):
name = 'sms-verify-commit'
auth_check = 'admin'
def post(self, request):
try:
code = request.data['code']
request_id = request.data['request_id']
no_older_than = timezone.now() - datetime.timedelta(minutes=10)
r = get_object_or_404(SmsVerifiedAction, pk=request_id, verify_code=code, admin=request.admin, committed=False, request_time__gte=no_older_than)
r.committed = True
r.commit_time = timezone.now()
r.save()
return JsonResponse({"ok": True})
except Exception as e:
return JsonResponse({"ok": False, "error": str(e) })
class GlobalConfigView(BaseView):
name = 'global-config'
def get(self, request):
name = request.GET.get("name")
if name == 'camera-zoom':
# XXX: drop this once the client is switched to CameraRulesView
q = CameraRule.objects.filter(disabled=False)
return JsonResponse({
"value": make_camera_rules(q)
}, safe=False)
value = None
r = GlobalConfig.objects.filter(name=name).first()
if r:
value = r.value
return JsonResponse({"value": json.loads(value)})
return http404()
def post(self, request):
if not request.admin:
return http401()
name = request.data['name']
value = request.data['value']
r, _ = GlobalConfig.objects.get_or_create(name=name)
r.value = json.dumps(value)
r.save()
return JsonResponse({"ok": True, "id": r.pk})
class QrStdStatView(BaseView):
name = 'qr-std-stat'
auth_check = 'admin'
def get(self, request):
ret = oss_stat(bucket=settings.OSS_QRS_BUCKET, endpoint=settings.OSS_QRS_ENDPOINT)
return JsonResponse(ret)
class LogReportView(BaseView):
name = 'log-report'
def post(self, request):
log = dict(request.data)
log['ip'] = get_ip(request)
log['location'] = ip_to_region(log['ip'])
SystemLog.objects.create(log=json.dumps(log))
return JsonResponse({})
class DebugUploadView(BaseView):
name = 'debug-upload'
def post(self, request):
image, fn = get_image(request)
if not image:
return JsonResponse({"error": "image not found in request"})
qrcode = request.data.get('qrcode').split("code=")[-1]
phonemodel = request.data.get('phonemodel')
phonemodel = phonemodel.split("<")[0].strip().replace(" ", "")
r = GlobalConfig.objects.filter(name='debug_upload_dir').first()
if r:
upload_dir = r.get_value()
else:
upload_dir = "default"
save_dir = "/emblem/data/debug-upload/{}/{}".format(upload_dir, phonemodel)
if not os.path.exists(save_dir):
os.makedirs(save_dir)
r = GlobalConfig.objects.filter(name='debug_upload_neg_or_pos').first()
if r:
neg_or_pos = r.get_value();
else:
neg_or_pos = "unknown"
with tempfile.NamedTemporaryFile(suffix=fn) as tf:
tf.write(image)
tf.flush()
ts = time.time()
save_file = os.path.join(save_dir, "{}_{}_{}_{}.jpg".format(qrcode, neg_or_pos, phonemodel, ts))
url = 'http://alg:3028/alg/rectify'
with open(tf.name, 'rb') as tfr:
r = requests.post(url, data=tfr)
r.raise_for_status()
with open(save_file, 'wb') as sfw:
sfw.write(r.content)
return JsonResponse({"info": "ok"})
def make_camera_rules(q):
def make_camera_config_obj(cc):
return {
'model': cc.model_text,
'zoom': cc.zoom,
'web_view': cc.use_web_view,
}
return [make_camera_config_obj(cc) for cc in q]
class CameraRulesView(BaseView):
name = 'camera-rules'
def get(self, request):
q = CameraRule.objects.filter(disabled=False)
return JsonResponse(make_camera_rules(q), safe=False)
class CameraFrameView(BaseView):
'''
handle camera frame upload
'''
name = 'camera-frame'
def post(self, request):
filebody, filename = get_image(request)
date = timezone.now().strftime('%Y-%m-%d')
session_id = request.data.get("session_id", "unknown-session")
phone_model = request.data.get('phone_model', 'unknown phone')
seq_num = request.data.get('seq_num', time.time())
key = f'{date}/{phone_model}-{session_id}-{seq_num}-{filename}'
oss_put(key, filebody, bucket=settings.FRAMES_BUCKET)
return JsonResponse({})
class FramesView(BaseView):
'''
get frame list
'''
name = 'frames'
auth_check = 'admin'
def get(self, request):
start = request.GET.get('start', 0)
end = request.GET.get('end', 100)
filter_label = request.GET.get('label', None)
ret = sorted(oss_list(bucket=settings.FRAMES_BUCKET), reverse=True)
start = int(start)
end = int(end)
frames = []
for frame in ret[start:end]:
fo = FrameLabel.objects.filter(oss_path=frame).first()
if fo:
if filter_label and filter_label not in fo.labels.split(','):
continue
frames.append({
'path': frame,
'labels': fo.labels.split(','),
})
elif not filter_label:
frames.append({
'path': frame,
'labels': [],
})
return JsonResponse({
'frames': frames,
'total': len(ret),
'start': start,
'end': end,
})
class FrameView(BaseView):
'''
get one frame image from oss
'''
name = 'frame'
def get(self, request):
path = request.GET.get('path')
return HttpResponse(oss_get(path, bucket=settings.FRAMES_BUCKET),
content_type='image/jpeg')
class DeleteFrameView(BaseView):
'''
delete one frame from oss
'''
name = 'delete-frame'
auth_check = 'admin'
def post(self, request):
path = request.data.get('path')
oss_delete(path, bucket=settings.FRAMES_BUCKET)
FrameLabel.objects.filter(oss_path=path).delete()
return JsonResponse({})
class LabelFrameView(BaseView):
name = 'label-frame'
auth_check = 'admin'
def post(self, request):
path = request.data.get('path')
label = request.data.get('label')
fo, _ = FrameLabel.objects.get_or_create(oss_path=path)
if fo.labels.split(',').count(label) == 0:
fo.labels = fo.labels + ',' + label if fo.labels else label
else:
fo.labels = ','.join([x for x in fo.labels.split(',') if x != label])
fo.save()
return JsonResponse({
'ok': True,
'id': fo.id,
})
class ABTestView(BaseView):
name = 'ab-test'
auth_check = None
def get(self, request):
test_object = request.GET.get('test_object', None)
abtest = ABTest.objects.filter(enabled=True)
if test_object:
abtest = abtest.filter(test_object=test_object)
return JsonResponse({
'items': [
{
'name': abtest.name,
'test_object': abtest.test_object,
'spec': abtest.spec,
}
for abtest in abtest
]
})
class ABTestReportView(BaseView):
name = 'ab-test-report'
auth_check = None
def post(self, request):
name = request.data.get('name')
abtest = ABTest.objects.filter(name=name).first()
if not abtest:
return http404()
sample = ABTestSample.objects.create(abtest=abtest, data=json.dumps(request.data.get('data')))
return JsonResponse({
'ok': True,
})
class ScanDataLabelsView(BaseView):
name = 'scan-data-labels'
auth_check = 'admin'
def get(self, request):
ret = []
for x in ScanData.objects.all().filter(labels__isnull=False).order_by('pk').values('id', 'labels', 'image', 'code', 'succeeded'):
if not x.get('labels'):
continue
ret.append({
'id': x['id'],
'labels': x.get('labels'),
'code': x.get('code'),
'image': x.get('image'),
'succeeded': x.get('succeeded'),
})
return JsonResponse({
'items': ret,
})
class AIChatView(BaseView):
"""AI聊天API视图 - 仅支持发送消息,临时会话模式"""
name = 'ai-chat'
auth_check = None
def post(self, request):
"""发送消息给AI并获取回复"""
try:
# 获取请求参数
session_id = request.data.get('session_id')
message = request.data.get('message')
context = request.data.get('context', {})
chat_type = request.data.get('chat_type', 'platform')
product_id = request.data.get('product_id')
# 验证必需参数
if not message:
return JsonResponse({
'error': 'message is required'
}, status=400)
# 获取或创建会话
session = self._get_or_create_session(session_id, request.tenant, product_id)
# 确定聊天类型和产品ID
if chat_type == 'product' and product_id:
ai_chat_type = 'product'
ai_product_id = product_id
else:
ai_chat_type = 'platform'
ai_product_id = None
# 创建AI服务实例
ai_service = AIChatService(
chat_type=ai_chat_type,
product_id=ai_product_id
)
# 调用AI服务
response = ai_service.chat(message)
# 保存消息记录
self._save_message(session, 'user', message)
self._save_message(session, 'assistant', response)
return JsonResponse({
'session_id': session.session_id,
'response': response,
'chat_type': ai_chat_type,
'product_id': ai_product_id
})
except Exception as e:
return JsonResponse({
'error': f'AI chat failed: {str(e)}'
}, status=500)
def _get_or_create_session(self, session_id, tenant, product_id=None):
"""获取或创建聊天会话"""
if session_id:
# 尝试获取现有会话
session = ChatSession.objects.filter(
session_id=session_id,
tenant=tenant
).first()
if session:
return session
# 创建新会话
if not session_id:
session_id = str(uuid.uuid4())
session = ChatSession.objects.create(
session_id=session_id,
tenant=tenant,
product_id=product_id
)
return session
def _save_message(self, session, role, content, message_type='text'):
"""保存消息到数据库"""
ChatMessage.objects.create(
session=session,
role=role,
message_type=message_type,
text_content=content
)