import os import json import uuid import datetime import random from django.conf import settings from django.utils import timezone from django.contrib.auth.models import User from django.db import models class AdminInfo(models.Model): user = models.ForeignKey(User, related_name="admininfo", on_delete=models.CASCADE) mobile = models.CharField(max_length=128, unique=True, null=True, blank=True, verbose_name="手机号") qr_verify_alert_rule = models.TextField(null=True, blank=True) def __str__(self): return self.user.username class GlobalConfig(models.Model): name = models.CharField(max_length=128, db_index=True, unique=True, verbose_name="配置名称") value = models.TextField(null=True, verbose_name="配置内容") def __str__(self): return self.name def get_value(self): if self.value: return json.loads(self.value) return None class Tenant(models.Model): username = models.CharField(max_length=128, unique=True, db_index=True, verbose_name="用户名") display_name = models.CharField(max_length=128, null=True, blank=True, verbose_name="显示名称") mobile = models.CharField(max_length=128, unique=True, null=True, blank=True, verbose_name="手机号") password = models.CharField(max_length=256, verbose_name="密码") password_reset_code = models.CharField(max_length=128, db_index=True, null=True, blank=True) password_reset_code_expire = models.DateTimeField(null=True, blank=True) welcome_page_config = models.TextField(null=True, blank=True) next_seq_num = models.IntegerField(default=1) service_qr_file = models.ForeignKey('AssetFile', related_name='tenant_service_qrs', on_delete=models.PROTECT, null=True, blank=True, verbose_name="客服二维码文件") def alloc_seq_nums(self, n=1): ret = self.next_seq_num self.next_seq_num += n self.save() return ret def __str__(self): return self.username @classmethod def authenticate(cls, username, password): return Tenant.objects.filter(username=username, password=password).first() class AuthToken(models.Model): token = models.CharField(primary_key=True, max_length=128, verbose_name="API Token") admin = models.ForeignKey(AdminInfo, null=True, blank=True, related_name="tokens", on_delete=models.CASCADE) tenant = models.ForeignKey(Tenant, null=True, blank=True, related_name="tokens", on_delete=models.CASCADE) expire = models.DateTimeField(null=True) def validate(self): if self.expire and self.expire < timezone.now(): return False self.update_expire() return True def update_expire(self): new_expire = timezone.now() + datetime.timedelta(minutes=settings.TOKEN_EXPIRE_MINUTES) if not self.expire or new_expire > self.expire: self.expire = new_expire self.save() class MiniProgramContent(models.Model): tenant = models.ForeignKey(Tenant, null=True, unique=True, related_name="mini_program_content", on_delete=models.CASCADE) content = models.TextField(default='') def get_content(self): try: return json.loads(self.content) except: return {} class Product(models.Model): tenant = models.ForeignKey(Tenant, related_name="products", on_delete=models.CASCADE) name = models.CharField(max_length=128, verbose_name="名称") description = models.TextField(verbose_name="备注") article = models.ForeignKey('Article', null=True, blank=True, verbose_name="产品信息页面", related_name="products", on_delete=models.CASCADE) template_asset = models.ForeignKey('AssetFile', null=True, blank=True, verbose_name="产品信息页面模板文件", related_name="template_of", on_delete=models.PROTECT) def __str__(self): return "%s - %s" % (self.tenant.username, self.name) class Meta: unique_together = ('tenant', 'name') class ProductProperty(models.Model): product = models.ForeignKey(Product, related_name="properties", on_delete=models.CASCADE, verbose_name="产品") name = models.CharField(max_length=128, verbose_name="名称") text = models.TextField(null=True, blank=True, verbose_name="纯文本内容(可选)") richtext = models.TextField(null=True, blank=True, verbose_name="图文内容(可选)") file = models.ForeignKey('AssetFile', null=True, blank=True, related_name='product_properties', on_delete=models.PROTECT, verbose_name="关联文件") memo = models.TextField(null=True, blank=True, verbose_name="备注") class Meta: unique_together = ('product', 'name') def __str__(self): return "%s - %s" % (self.product.name, self.name) class Article(models.Model): title = models.CharField(max_length=128, null=True, blank=True, verbose_name="标题") body = models.TextField() tenant = models.ForeignKey(Tenant, related_name="articles", on_delete=models.CASCADE, null=True, blank=True) options = models.TextField(default='') url = models.TextField(null=True) class AssetFile(models.Model): filename = models.TextField(verbose_name="文件名") uuid = models.CharField(max_length=256) mime_type = models.CharField(max_length=256, default="application/octet-stream") tenant = models.ForeignKey(Tenant, related_name="assets", on_delete=models.CASCADE, null=True, blank=True) datetime = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") def get_name(self): return "%s-%s" % (self.uuid, self.filename) def is_image(self): if self.mime_type.startswith("image/"): return True _, ext = os.path.splitext(self.filename) if ext in ['.png', '.jpg', '.gif', '.jpeg']: return True class CodeBatch(models.Model): tenant = models.ForeignKey(Tenant, related_name="batches", null=True, on_delete=models.CASCADE) description = models.TextField(null=True, blank=True, verbose_name='备注') qr_angle = models.FloatField(default=0.0, verbose_name="网线角度") qr_angle_allowed_error = models.FloatField(default=1.0, verbose_name="角度验证误差范围") feature_comparison_threshold = models.FloatField(default=0.0, verbose_name="特征对比阈值(0=禁用)") datetime = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") code_prefix = models.CharField(max_length=64, verbose_name="序列码前缀") num_digits = models.IntegerField(default=10, verbose_name="尾号长度") is_active = models.BooleanField(default=True, verbose_name="已激活") name = models.CharField(null=True, max_length=255, db_index=True, verbose_name="名称") detection_service = models.TextField(null=True, blank=True, verbose_name="指定检测后端微服务地址(可选) ") scan_redirect_url = models.TextField(null=True, blank=True, verbose_name="自定义扫码重定向URL(可选)") enable_auto_torch = models.BooleanField(default=False, verbose_name="自动打开闪光灯") camera_sensitivity = models.FloatField(default=1.0, verbose_name="摄像头灵敏度") mini_prog_entry_path = models.TextField(null=True, blank=True, default='/pages/index/index', verbose_name="小程序入口路径(可选)") mini_prog_entry_env_version = models.CharField(max_length=128, null=True, blank=True, default='release', verbose_name="小程序入口环境版本(可选), 默认release。如为trial则进入体验版") def gen_code(self, n): a = 10 ** (self.num_digits - 1) b = 10 ** self.num_digits - 1 tenant = self.tenant target = self.codes.count() + n while True: rem = target - self.codes.count() if rem <= 0: break scs = [] batchsize = min(rem, 10000) seq_num = self.tenant.alloc_seq_nums(batchsize) if self.tenant else None for x in range(batchsize): rn = random.randint(a, b) code = "%s%d" % (self.code_prefix, rn) sc = SerialCode(batch=self, code=code, tenant=tenant, seq_num=seq_num) if seq_num: seq_num += 1 scs.append(sc) SerialCode.objects.bulk_create(scs, ignore_conflicts=True) def __str__(self): try: return f"{self.pk} {self.qr_angle} {self.tenant} - {self.description}" except: return f"{self.pk}" class SerialCode(models.Model): seq_num = models.IntegerField(verbose_name="流水号", null=True, db_index=True) code = models.CharField(max_length=128, unique=True, db_index=True, verbose_name="序列码") batch = models.ForeignKey(CodeBatch, related_name="codes", on_delete=models.CASCADE) is_active = models.BooleanField(default=False, verbose_name="已激活") tenant = models.ForeignKey(Tenant, related_name="codes", null=True, on_delete=models.CASCADE) product = models.ForeignKey(Product, null=True, blank=True, related_name="batches", on_delete=models.CASCADE, verbose_name="产品") datetime = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") class Meta: unique_together = ('tenant', 'seq_num') def get_qr_url(self): tenant_id = str(self.tenant.pk) if self.tenant else '' return 'https://emblem.hondcloud.com/api/mini-prog-entry/?code=%s&tenant=%s' % (self.code, tenant_id) def __str__(self): return f"{self.code} (# {self.seq_num} {self.batch})" class SerialCodeBatchOpRecord(models.Model): seq_num_begin = models.BigIntegerField(null=True, verbose_name="开始流水号") seq_num_end = models.BigIntegerField(null=True, verbose_name="结束流水号") code_samples = models.TextField(null=True, verbose_name="序列码摘要") batch_size = models.IntegerField(verbose_name='序列码数量') datetime = models.DateTimeField(auto_now_add=True, verbose_name="操作时间") op_name = models.CharField(max_length=128, verbose_name="操作类型") product_name = models.TextField(null=True, verbose_name="绑定产品名称") product_page_name = models.TextField(null=True, verbose_name="页面名称") tenant = models.ForeignKey(Tenant, related_name="code_batch_ops", on_delete=models.CASCADE) class ConsumerInfo(models.Model): create_time = models.DateTimeField(auto_now_add=True) username = models.CharField(max_length=128, null=True, blank=True, verbose_name="用户名") platform = models.CharField(max_length=128, null=True, blank=True, default='wechat') ip = models.CharField(max_length=128, null=True, blank=True, verbose_name="IP") class Meta: unique_together = ('platform', 'username') def get_consumer(username, ip, platform='wechat', gender=None): ci, created = ConsumerInfo.objects.get_or_create(platform=platform, username=username) if created: ci.ip = ip ci.gender = gender ci.save() return ci class ScanData(models.Model): consumer = models.ForeignKey(ConsumerInfo, related_name="scans", null=True, on_delete=models.CASCADE) product = models.ForeignKey(Product, related_name="scans", null=True, on_delete=models.CASCADE) tenant = models.ForeignKey(Tenant, null=True, on_delete=models.CASCADE) datetime = models.DateTimeField(auto_now_add=True, verbose_name="时间") ip = models.CharField(max_length=64, verbose_name="IP地址") location = models.CharField(max_length=128, null=True, verbose_name='位置') message = models.TextField(null=True, verbose_name='信息') image = models.TextField() succeeded = models.BooleanField(default=True, verbose_name="验证结果") code = models.TextField(null=True, verbose_name="序列号") phone_model = models.TextField(null=True, verbose_name="手机型号") client_log = models.TextField(null=True, blank=True, verbose_name="采集日志") labels = models.TextField(default="", blank=True, verbose_name="标签") class SystemLog(models.Model): datetime = models.DateTimeField(auto_now_add=True, db_index=True) log = models.TextField() class Counter(models.Model): tenant = models.ForeignKey(Tenant, null=True, related_name='counters', on_delete=models.CASCADE) name = models.CharField(max_length=128, db_index=True) params = models.TextField(null=True) datetime = models.DateTimeField(auto_now_add=True, db_index=True) count = models.IntegerField() class UserMessage(models.Model): sender = models.TextField(null=True, verbose_name='发送自') datetime = models.DateTimeField(auto_now_add=True, verbose_name="时间") read = models.BooleanField(default=False, verbose_name='已读') admin = models.ForeignKey(AdminInfo, null=True, related_name="messages", on_delete=models.CASCADE) tenant = models.ForeignKey(Tenant, null=True, related_name="messages", on_delete=models.CASCADE) subject = models.TextField(verbose_name='标题') content = models.TextField(verbose_name='内容') content_type = models.CharField(max_length=128, default='text/plain') def send_message(user, subject, content, sender=None): m = UserMessage(sender=sender, subject=subject, content=content) if isinstance(user, AdminInfo): m.receiver_admin = user else: m.receiver_tenant = user m.save() def params_repr(params): return "".join(["%s=%s;" % (k, str(v)) for k, v in params.items()]) def count(tenant, name, params={}, count=1): ps = params_repr(params) prev = Counter.objects.filter(name=name, params=ps).order_by('-pk').first() Counter.objects.create(tenant=tenant, name=name, params=ps, count=count) def get_sum(name): r = Counter.objects.filter(name=name).aggregate(models.Sum('count')) return r['count__sum'] or 0 def get_history_counts(tenant, name, since, interval_secs, total_secs=None, params=None): dt = datetime.timedelta(seconds=interval_secs) if total_secs: until = since + datetime.timedelta(seconds=total_secs) else: until = datetime.datetime.now() total_secs = int((until - since).total_seconds()) q = Counter.objects.filter(name=name, datetime__gte=since, datetime__lt=until) if tenant: q = q.filter(tenant=tenant) q = q.order_by('pk') if params: q = q.filter(params=params_repr(params)) npoints = int(total_secs // interval_secs) + 1 intervals = [0] * npoints for rec in q: idx = int((rec.datetime - since).total_seconds()) // interval_secs if idx >= npoints: break intervals[idx] += rec.count ret = [] for i, x in enumerate(intervals): ret.append((since + i * dt, x)) return ret def log(*log): log = " ".join([str(x) for x in log]) SystemLog.objects.create(log=log) class Job(models.Model): admin = models.ForeignKey(AdminInfo, null=True, related_name="jobs", on_delete=models.CASCADE) tenant = models.ForeignKey(Tenant, null=True, related_name="jobs", on_delete=models.CASCADE) name = models.CharField(max_length=128, null=True) status = models.CharField(max_length=64, default='created') progress = models.FloatField(default=0.0) message = models.TextField(default='') create_time = models.DateTimeField(auto_now_add=True) update_time = models.DateTimeField(auto_now=True) output = models.TextField(null=True) def update(self, status, progress=None, message=None): self.status = status if progress: self.progress = progress if message: self.message = message self.save() def __str__(self): return self.name class MiniProgram(models.Model): name = models.CharField(max_length=128, verbose_name='名称') app_id = models.CharField(max_length=128, unique=True, db_index=True, verbose_name='App ID') app_secret = models.CharField(max_length=128, verbose_name='App Secret') class MiniProgramIntegration(models.Model): tenant = models.ForeignKey(Tenant, related_name="integrations", on_delete=models.CASCADE) token = models.CharField(max_length=128, unique=True, db_index=True) product_scope = models.ForeignKey(Product, related_name="integrations", on_delete=models.CASCADE, null=True) batch_scope = models.ForeignKey(CodeBatch, related_name="integrations", on_delete=models.CASCADE, null=True) webhook_url = models.TextField(null=True) class SmsVerifiedAction(models.Model): admin = models.ForeignKey(AdminInfo, related_name="sms_verified_actions", on_delete=models.CASCADE) action = models.TextField(verbose_name="操作") request_time = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") verify_code = models.TextField(verbose_name="验证码") committed = models.BooleanField(default=False, verbose_name="已提交") commit_time = models.DateTimeField(null=True, blank=True, verbose_name="提交日期") def __str__(self): return f"{self.admin.user.username} {self.action}" class EstorArchiveRecord(models.Model): filename_sha1 = models.CharField(primary_key=True, max_length=128) filename = models.TextField(max_length=128) data_sha1 = models.TextField(null=True) create_time = models.DateTimeField(auto_now_add=True) class CameraRule(models.Model): name = models.CharField(max_length=128, verbose_name="规则名称") model_text = models.TextField(verbose_name="机型字符串") zoom = models.IntegerField(default=4, verbose_name="放大倍数") use_web_view = models.BooleanField(default=False, verbose_name="使用web-view(iPhone微距)") disabled = models.BooleanField(default=False, verbose_name="禁用") notes = models.TextField(null=True, verbose_name="备注信息") def __str__(self): return self.name class FrameLabel(models.Model): oss_path = models.TextField(verbose_name="OSS路径", unique=True, db_index=True) labels = models.TextField(verbose_name="标签") create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") update_time = models.DateTimeField(auto_now=True, verbose_name="更新日期") def __str__(self): return self.oss_path class ABTest(models.Model): name = models.CharField(max_length=128, verbose_name="名称", db_index=True, unique=True) description = models.TextField(verbose_name="描述") test_object = models.CharField(max_length=128, verbose_name="实验对象") create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") update_time = models.DateTimeField(auto_now=True, verbose_name="更新日期") enabled = models.BooleanField(default=True, verbose_name="是否启用") spec = models.TextField(verbose_name="实验配置") def __str__(self): return f"{self.name} {self.category}" class ABTestSample(models.Model): abtest = models.ForeignKey(ABTest, related_name="samples", on_delete=models.CASCADE) data = models.TextField(verbose_name="实验数据") create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") update_time = models.DateTimeField(auto_now=True, verbose_name="更新日期") def __str__(self): return f"{self.pk} {self.abtest.name} {self.create_time}"