示例#1
0
class Tunnel(models.Model):
    """
    SSH隧道配置
    """
    tunnel_name = models.CharField('隧道名称', max_length=50, unique=True)
    host = models.CharField('隧道连接', max_length=200)
    port = models.IntegerField('端口', default=0)
    user = fields.EncryptedCharField(verbose_name='用户名', max_length=200, default='', blank=True, null=True)
    password = fields.EncryptedCharField(verbose_name='密码', max_length=300, default='', blank=True, null=True)
    pkey = fields.EncryptedTextField(verbose_name="密钥", blank=True, null=True)
    pkey_path = models.FileField(verbose_name="密钥地址", blank=True, null=True, upload_to='keys/')
    pkey_password = fields.EncryptedCharField(verbose_name='密钥密码', max_length=300, default='', blank=True, null=True)
    create_time = models.DateTimeField('创建时间', auto_now_add=True)
    update_time = models.DateTimeField('更新时间', auto_now=True)

    def __str__(self):
        return self.tunnel_name

    def short_pkey(self):
        if len(str(self.pkey)) > 20:
            return '{}...'.format(str(self.pkey)[0:19])
        else:
            return str(self.pkey)

    class Meta:
        managed = True
        db_table = 'ssh_tunnel'
        verbose_name = u'隧道配置'
        verbose_name_plural = u'隧道配置'
示例#2
0
class ProcLogin(models.Model):
    def __str__(self):
        return self.name + ' - ' + self.sz_id

    class Meta:
        # Add verbose name
        verbose_name_plural = '나무 로그인 정보'
        verbose_name = '나무 로그인 정보'

    name = models.CharField(max_length=15, verbose_name='별칭')  # 별칭
    sz_id = models.CharField(max_length=15, verbose_name='로그인 아이디')  # 로그인 ID
    sz_pw = fields.EncryptedCharField(
        verbose_name='로그인 비밀번호', null=False
    )  # models.CharField(max_length=15, verbose_name='로그인 PW')  # 로그인 PW
    sz_cert_pw = fields.EncryptedCharField(
        verbose_name='인증서 비밀번호', null=False
    )  # models.CharField(max_length=15, verbose_name='인증서 비밀번호')  # 인증서 비밀번호
    account_pw = models.CharField(max_length=44,
                                  verbose_name='계좌 비밀번호',
                                  null=False)  # 계좌 비밀번호
    trade_pw = models.CharField(max_length=44,
                                verbose_name='거래 비밀번호',
                                null=False)  # 거래 비밀번호
    is_hts = models.BooleanField(default=True,
                                 verbose_name='모의투자 여부',
                                 null=False)  # 완료 유무

    def save(self,
             force_insert=False,
             force_update=False,
             using=None,
             update_fields=None):
        # if self. # TODO 거래비밀번호, 게좌비밀번호 hash 암호화 처리 필요(문의한 내용 답변 받아야 처리 가능)
        super().save(force_insert, force_update, using, update_fields)
示例#3
0
class Instance(models.Model):
    """
    各个线上实例配置
    """
    instance_name = models.CharField('实例名称', max_length=50, unique=True)
    type = models.CharField('实例类型', max_length=6, choices=(('master', '主库'), ('slave', '从库')))
    db_type = models.CharField('数据库类型', max_length=20, choices=DB_TYPE_CHOICES)
    host = models.CharField('实例连接', max_length=200)
    port = models.IntegerField('端口', default=0)
    user = fields.EncryptedCharField(verbose_name='用户名', max_length=200, default='', blank=True)
    password = fields.EncryptedCharField(verbose_name='密码', max_length=300, default='', blank=True)
    db_name = models.CharField('数据库', max_length=64, default='', blank=True)
    charset = models.CharField('字符集', max_length=20, default='', blank=True)
    service_name = models.CharField('Oracle service name', max_length=50, null=True, blank=True)
    sid = models.CharField('Oracle sid', max_length=50, null=True, blank=True)
    resource_group = models.ManyToManyField(ResourceGroup, verbose_name='资源组', blank=True)
    instance_tag = models.ManyToManyField(InstanceTag, verbose_name='实例标签', blank=True)
    tunnel = models.ForeignKey(Tunnel, blank=True, null=True, on_delete=models.CASCADE, default=None)
    create_time = models.DateTimeField('创建时间', auto_now_add=True)
    update_time = models.DateTimeField('更新时间', auto_now=True)

    def __str__(self):
        return self.instance_name

    class Meta:
        managed = True
        db_table = 'sql_instance'
        verbose_name = u'实例配置'
        verbose_name_plural = u'实例配置'
示例#4
0
文件: models.py 项目: xxlrr/Archery
class Host(models.Model):
    """"Host info"""
    host_id = models.AutoField('host id', primary_key=True)
    host_name = models.CharField('host name', max_length=32)
    host_addr = models.CharField('host address', max_length=64)
    ssh_port = models.IntegerField('ssh port', blank=True, default=22)
    ssh_user = models.CharField('ssh user',
                                max_length=32,
                                blank=True,
                                default='')
    ssh_password = fields.EncryptedCharField(verbose_name='ssh password',
                                             max_length=128,
                                             default='',
                                             blank=True)
    create_time = models.DateTimeField('create time', auto_now_add=True)
    update_time = models.DateTimeField('update time', auto_now=True)

    class Meta:
        managed = True
        db_table = 'sql_host'
        verbose_name = u'主机列表'
        verbose_name_plural = u'主机列表'

    def __str__(self):
        return self.host_name
示例#5
0
class TestModel(models.Model):
    char = fields.EncryptedCharField(blank=True, null=True)
    text = fields.EncryptedTextField(blank=True, null=True)
    textraw = models.TextField(blank=True, null=True)
    integer = fields.EncryptedIntegerField(blank=True, null=True)
    email = fields.EncryptedEmailField(blank=True, null=True)
    url = fields.EncryptedURLField(blank=True, null=True)
示例#6
0
class sys_user(models.Model):
    """
    username唯一索引
    status,del_flag Btree索引

    """
    GENDER_CHOICES = (
        (0, 'Male'),
        (1, 'Female'),
    )
    LOCK_CHOICES = (
        (0, 'normal'),
        (1, 'locked'),
    )

    username = models.CharField(unique=True,null=True,max_length=100, verbose_name='用户名')
    nickname = models.CharField(null=True,max_length=100, verbose_name='显示名称')
    password = fields.EncryptedCharField(null=True,max_length=500, verbose_name='显示名称')
    type = models.IntegerField(default=5,verbose_name='0-超级管理员,1-管理员,2-项目管理员,3-开发人员,4-测试人员,5-访客')
    salt = models.CharField(null=True,max_length=100, verbose_name='MD5密码盐')
    avatar = models.CharField(null=True,max_length=100, verbose_name='头像')
    gender = models.IntegerField(choices=GENDER_CHOICES, verbose_name='性别')
    email = models.CharField(null=True,max_length=100, verbose_name='电子邮箱')
    phone = models.CharField(null=True,max_length=100, verbose_name='电话')
    status = models.IntegerField(db_index=True, choices=LOCK_CHOICES, verbose_name='用户状态,锁定 正常')
    deleted = models.CharField(max_length=200, default=0,db_index=True,verbose_name='删除状态,锁定 正常')
    createTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    updateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')

    class Meta:
        unique_together = ('username', 'deleted',)
示例#7
0
class MyUser(AbstractBaseUser, PermissionsMixin):
    email = fields.EncryptedEmailField(blank=False,
                                       null=True,
                                       verbose_name='邮件')
    nickname = fields.EncryptedCharField(max_length=30, verbose_name='名字')
    username = models.CharField(unique=True, max_length=30, verbose_name='用户名')
    position = models.CharField(max_length=50, default="学生", verbose_name="职位")
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    join_time = models.DateTimeField(auto_now_add=True)
    desc = models.TextField(blank=True, null=True)
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ["email"]
    EMAIL_FIELD = "email"
    objects = UserManager()

    def get_full_username(self):
        return self.username

    def get_short_username(self):
        return self.username

    def __str__(self):
        return self.nickname
示例#8
0
class InstanceAccount(models.Model):
    """
    实例账号列表
    """
    instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
    user = fields.EncryptedCharField(verbose_name='账号', max_length=128)
    host = models.CharField(verbose_name='主机', max_length=64)
    password = fields.EncryptedCharField(verbose_name='密码', max_length=128, default='', blank=True)
    remark = models.CharField('备注', max_length=255)
    sys_time = models.DateTimeField('系统修改时间', auto_now=True)

    class Meta:
        managed = True
        db_table = 'instance_account'
        unique_together = ('instance', 'user', 'host')
        verbose_name = '实例账号列表'
        verbose_name_plural = '实例账号列表'
示例#9
0
class ServerVPN(models.Model):
    name = models.CharField(max_length=50, unique=True, null=False, blank=False, verbose_name='Server name')
    address = models.GenericIPAddressField(null=False, blank=False, verbose_name='IP address')
    port = models.IntegerField(null=False, blank=False, default=22, verbose_name='Port number')
    user_name = models.CharField(max_length=150, null=False, blank=False, verbose_name='User name')
    password = fields.EncryptedCharField(max_length=100, null=False, blank=False, verbose_name='Password')
    is_active = models.BooleanField(null=False, blank=False, default=True, verbose_name='Active')

    def __str__(self):
        return self.name
示例#10
0
class ossconf(models.Model):
    """
    oss配置
    """
    name = models.CharField(max_length=500, verbose_name='名称')
    description = models.CharField(max_length=500, verbose_name='描述信息')
    accessKey = models.CharField(max_length=500, verbose_name='oss访问key')
    accessSecret = fields.EncryptedCharField(max_length=500, verbose_name='oss访问secret')
    endPoint = models.CharField(max_length=500, verbose_name='oss endpoint')
    bucketName = models.CharField(max_length=500, verbose_name='bucketname')
示例#11
0
class sys_mailserver(models.Model):
    """
    邮件服务配置
    """
    name = models.CharField(max_length=500,verbose_name='邮件服务器')
    description = models.CharField(max_length=500, verbose_name='描述信息')
    server_type = models.CharField(max_length=32, verbose_name='邮件服务器类型')
    mail_server = models.CharField(max_length=100, verbose_name='邮件服务器地址')
    smtp_port = models.IntegerField(verbose_name='smtp端口')
    protocol = models.CharField(max_length=32, verbose_name='smtp协议')
    mailusername = models.CharField(max_length=300, verbose_name='邮件发送者')
    mailpasswd = fields.EncryptedCharField(max_length=300, verbose_name='邮件密码')
示例#12
0
class Tunnel(models.Model):
    """
    SSH隧道配置
    """
    tunnel_name = models.CharField('隧道名称', max_length=50, unique=True)
    host = models.CharField('隧道连接', max_length=200)
    port = models.IntegerField('端口', default=0)
    user = fields.EncryptedCharField(verbose_name='用户名', max_length=200, default='', blank=True, null=True)
    password = fields.EncryptedCharField(verbose_name='密码', max_length=300, default='', blank=True, null=True)
    pkey_path = fields.EncryptedCharField(verbose_name='密钥地址', max_length=300, default='', blank=True, null=True)
    pkey_password = fields.EncryptedCharField(verbose_name='密钥密码', max_length=300, default='', blank=True, null=True)
    create_time = models.DateTimeField('创建时间', auto_now_add=True)
    update_time = models.DateTimeField('更新时间', auto_now=True)

    def __str__(self):
        return self.tunnel_name

    class Meta:
        managed = True
        db_table = 'ssh_tunnel'
        verbose_name = u'隧道配置'
        verbose_name_plural = u'隧道配置'
示例#13
0
class Config(models.Model):
    """
    配置信息表
    """
    item = models.CharField('配置项', max_length=200, primary_key=True)
    value = fields.EncryptedCharField(verbose_name='配置项值', max_length=500)
    description = models.CharField('描述', max_length=200, default='', blank=True)

    class Meta:
        managed = True
        db_table = 'sql_config'
        verbose_name = u'系统配置'
        verbose_name_plural = u'系统配置'
示例#14
0
class CustomUser(AbstractBaseUser, PermissionsMixin):
    username_validator = UnicodeUsernameValidator()
    email = fields.EncryptedEmailField(blank=True, unique=True)
    username = fields.EncryptedCharField(
        max_length=150,
        unique=True,
        help_text=
        ('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'
         ),
        validators=[username_validator],
        error_messages={
            'unique': ("Пользователь с таким логином уже существует."),
        },
    )
    is_active = models.BooleanField(default=True)
    first_name = models.CharField(('first name'), max_length=150, blank=True)
    last_name = models.CharField(('last name'), max_length=150, blank=True)
    date_joined = models.DateTimeField(('date joined'), default=timezone.now)

    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = []
    is_staff = models.BooleanField(default=False, )

    objects = UserManager()

    class Meta:
        verbose_name = ('user')
        verbose_name_plural = ('Пользователи')

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)
class User(models.Model):
    surname = models.CharField(max_length=100, verbose_name='Фамилия')
    name = models.CharField(max_length=100, verbose_name='Имя')
    lastName = models.CharField(max_length=100, verbose_name='Отчество')
    phone = models.CharField(max_length=15, null=True, blank=True, verbose_name='Телефон')
    address = models.CharField(max_length=200, verbose_name='Адрес')
    inn = fields.EncryptedCharField(max_length=50, verbose_name='ИНН') # Данные ИНН будут зашифрованы в базе

    def __str__(self):
        if self.lastName:
            return "{} {} {}".format(self.surname, self.name, self.lastName)
        else:
            return '{} {}'.format(self.name, self.surname)

    class Meta:
        verbose_name = 'Пользователь'
        verbose_name_plural = 'Пользователи'
示例#16
0
class InstanceDatabase(models.Model):
    """
    实例数据库列表
    """
    instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
    db_name = fields.EncryptedCharField(verbose_name='数据库名', max_length=128)
    owner = models.CharField('负责人', max_length=50, default='', blank=True)
    owner_display = models.CharField('负责人中文名',
                                     max_length=50,
                                     default='',
                                     blank=True)
    remark = models.CharField('备注', max_length=255, default='', blank=True)
    sys_time = models.DateTimeField('系统修改时间', auto_now=True)

    class Meta:
        managed = True
        db_table = 'instance_database'
        unique_together = ('instance', 'db_name')
        verbose_name = '实例数据库'
        verbose_name_plural = '实例数据库列表'
示例#17
0
class Profile(models.Model):
    user = models.OneToOneField("auth_.CustomUser",
                                null=False,
                                on_delete=models.CASCADE,
                                verbose_name='Пользователь')
    first_name = fields.EncryptedCharField(default='', verbose_name='Имя')
    surname = fields.EncryptedCharField(default='', verbose_name='Фамилия')
    patronymic = fields.EncryptedCharField(default='', verbose_name='Отчество')
    partner = models.OneToOneField("utils.Partner",
                                   null=True,
                                   on_delete=models.CASCADE,
                                   related_name='+')
    avatar = models.ImageField(upload_to='avatars',
                               blank=True,
                               default='avatars/default.jpg')
    activated = models.BooleanField(default=False)
    whatsapp = fields.EncryptedCharField(default=None, null=True)
    instagram = fields.EncryptedCharField(default=None, null=True)
    telegram = fields.EncryptedCharField(default=None, null=True)

    def __str__(self):
        return self.user.username

    class Meta:
        verbose_name_plural = ('Профили пользователей')

    def get_partner_list(self):
        return Profile.objects.filter(inviter=self).all()

    def upload_file(self, image_pillow):
        path = os.path.join(settings.MEDIA_ROOT, 'avatars',
                            self.user.username + '.png')
        image_pillow.save(path)
        self.avatar = os.path.join('avatars', self.user.username + '.png')
        self.save()
        return get_avatar_profile_link(self)
示例#18
0
class VehicleLicense(BasicModel, BIDModel):
    """ 车辆行驶证信息 """
    class Meta:
        verbose_name = 'VehicleLicense'
        verbose_name_plural = verbose_name
        index_together = [
            'vehicle_type', 'use_character', 'model', 'is_verified'
        ]
        db_table = 'k_ls_vehicle_license'
        ordering = ('-pk', )

    oss_key = models.URLField('图片路径', db_index=True, default='')
    usrid = models.BigIntegerField('用户', db_index=True, default=0)
    plate_num = models.CharField('车牌号',
                                 db_index=True,
                                 max_length=20,
                                 default='')
    vehicle_type = models.CharField('车辆类型', max_length=100, default='')
    use_character = models.CharField('车辆使用性质', max_length=100, default='')
    owner = mg_fields.EncryptedCharField(verbose_name='所有者名字',
                                         max_length=200,
                                         default='')
    address = mg_fields.EncryptedCharField(verbose_name='住址',
                                           max_length=255,
                                           default='')
    vin = mg_fields.EncryptedCharField(verbose_name='车辆识别代号',
                                       max_length=100,
                                       default='')
    engine_num = mg_fields.EncryptedCharField(verbose_name='发动机号码',
                                              max_length=100,
                                              default='')
    model = models.CharField('车辆品牌', max_length=200, default='')
    register_date = models.DateField('注册日期', null=True, default=None)
    issue_date = models.DateField('发证日期', null=True, default=None)
    reason = models.CharField('原因', max_length=200, default='')
    verified_at = models.DateTimeField('验证时间', null=True, default=None)
    is_verified = models.BooleanField('已验证', default=False)

    objects = VehicleLicenseManager()

    @cached_property
    def model_info(self):
        inst = VehicleModel.objects.get_vehicle_model(
            self.model, vehicle_type=self.vehicle_type)
        return inst

    @property
    def oss_url(self):
        """ OSS URL,无水印,60秒内有效 """
        url = oss_sign_url(self.oss_key)
        return url

    @property
    def url_watermark(self):
        """ OSS URL,带水印,99秒内有效 """
        url = oss_sign_url(self.oss_key, expires=99, watermark=True)
        return url

    @property
    def is_success(self):
        is_ok = self.vehicle_type and self.plate_num and self.owner
        return is_ok

    @property
    def is_support(self):
        """ 是否支持,仅支持 非营运的小型汽车或新能源车 """
        is_type_ok = self.get_vehicle_type() in mc.VehicleTypeSupport
        is_character_ok = self.use_character == '非营运'
        is_ok = is_type_ok and is_character_ok
        return is_ok

    def get_vehicle_type(self):
        """ 车辆类型转车牌类型 """
        if self.vehicle_type in vehicle_small_types:
            if len(self.plate_num) == 7:
                return mc.VehicleType.Small  # 小型汽车
            elif len(self.plate_num) == 8:
                return mc.VehicleType.Energy  # 新能源车
        return None

    def get_ocr_result(self):
        """ OCR查询 """
        if self.is_success:
            logger.info(
                f'vehicle_license_get_ocr_result__is_success {self.pk}')
            return
        from server.applibs.outside.models import ImageOcr
        inst = ImageOcr.objects.image_ocr_create(
            self.oss_key, mc.OCRType.VehicleLicenseFront, usrid=self.usrid)
        if not inst.is_victor:
            logger.warning(
                f'vehicle_license_get_ocr_result__not_victor {self.pk}')
            return
        data = inst.result_dic
        try:
            self.vin = data['vin']
            self.model = data['model']
            self.owner = data['owner']
            self.address = data['address']
            self.plate_num = data['plateNum']
            self.engine_num = data['engineNum']
            self.vehicle_type = data['vehicleType']
            self.use_character = data['useCharacter']
            self.issue_date = pendulum.parse(data['issueDate'], exact=True)
            self.register_date = pendulum.parse(data['registerDate'],
                                                exact=True)
            up_fields = [
                'vin',
                'model',
                'owner',
                'address',
                'plate_num',
                'engine_num',
                'vehicle_type',
                'use_character',
                'issue_date',
                'register_date',
                'updated_at',
            ]
            self.save(update_fields=up_fields)
        except Exception as exc:
            self.reason = '行驶证信息识别不完整'
            self.save(update_fields=['reason', 'updated_at'])
            logger.exception(
                f'vehicle_license_ocr_result__save_error {str(exc)}')

    def check_verifid(self):
        """ 验证信息 """
        if not (self.is_success and self.usrid > 0):
            self.is_verified = False
            self.reason = self.reason or '行驶证认证失败'
        elif not self.is_support:
            self.is_verified = False
            self.reason = '目前仅支持非营运的小型汽车或新能源车'
        else:
            self.is_verified = True
        self.save(update_fields=['is_verified', 'reason', 'updated_at'])
        return self.is_verified
示例#19
0
class ExportDestination(TitleDescriptionModel):
    """
    Some SSH reachable destination for file export.
    """

    #: SSH destination host IP.
    ip = models.GenericIPAddressField(
        blank=False,
        null=False,
        verbose_name="IP",
        help_text=help_text.EXPORT_DESTINATION_IP,
    )

    #: Authentication username.
    username = models.CharField(
        max_length=128,
        blank=False,
        null=False,
        help_text=help_text.EXPORT_DESTINATION_USERNAME,
    )

    #: Authentication password.
    password = fields.EncryptedCharField(
        max_length=128,
        blank=False,
        null=False,
        help_text=help_text.EXPORT_DESTINATION_PASSWORD,
    )

    #: Destination in the host filesystem.
    destination = models.CharField(
        max_length=512,
        blank=False,
        null=False,
        help_text=help_text.EXPORT_DESTINATION_PATH,
    )

    #: Connection port.
    port = models.PositiveIntegerField(default=DEFAULT_PORT,
                                       null=False,
                                       help_text=help_text.SSH_PORT)

    #: SSH banner timeout in seconds.
    banner_timeout = models.PositiveIntegerField(
        default=DEFAULT_BANNER_TIMEOUT,
        null=False,
        help_text=help_text.SSH_BANNER_TIMEOUT,
    )

    #: Socket connection timeout in seconds.
    socket_timeout = models.PositiveIntegerField(
        default=DEFAULT_SOCKET_TIMEOUT,
        null=False,
        help_text=help_text.SSH_SOCKET_TIMEOUT,
    )

    #: Transport session negotiation timeout in seconds.
    negotiation_timeout = models.PositiveIntegerField(
        default=DEFAULT_NEGOTIATION_TIMEOUT,
        null=False,
        help_text=help_text.SSH_NEGOTIATION_TIMEOUT,
    )

    #: Users that may export to this destination.
    users = models.ManyToManyField("accounts.User")

    # Host key cache.
    _key = None
    # Transport instance cache.
    _transport = None
    # SFTPClient cache.
    _sftp_client = None

    _logger = logging.getLogger("accounts.export_destination")

    #: String representation template.
    STRING_TEMPLATE: str = "{username}@{ip}"

    def __str__(self) -> str:
        """
        Returns the string representation of this instance.

        Returns
        -------
        str
            Export destination string representation
        """
        return self.STRING_TEMPLATE.format(username=self.username, ip=self.ip)

    def get_key(self) -> paramiko.ecdsakey.ECDSAKey:
        """
        Returns the public key of the host if it exists within the
        *known_hosts* file.

        See Also
        --------
        * :meth:`key`

        Returns
        -------
        paramiko.ecdsakey.ECDSAKey
            Host key
        """
        # Log public key query start.
        start_log = logs.SSH_KEY_QUERY_START.format(ip=self.ip)
        self._logger.debug(start_log)
        try:
            key_dict = get_known_hosts()[self.ip]
        except KeyError:
            # IP address not found in the known hosts file.
            unknown_host_log = logs.SSH_KEY_QUERY_FAILURE.format(ip=self.ip)
            self._logger.info(unknown_host_log)
            pass
        else:
            # Log public key query success and return.
            key_name = key_dict.keys()[0]
            success_log = logs.SSH_KEY_QUERY_SUCCESS.format(key_name=key_name,
                                                            ip=self.ip)
            self._logger.info(success_log)
            return key_dict[key_name]

    def create_transport(self,
                         banner_timeout: int = None,
                         socket_timeout: int = None) -> paramiko.Transport:
        """
        Returns the transport instance which will be used to negotiate the
        connection.

        Parameters
        ----------
        banner_timeout : int
            Timeout in seconds protocol banner read

        See Also
        --------
        :meth:`transport`

        Returns
        -------
        paramiko.Transport
            SSH transport thread
        """
        # Log start
        start_log = logs.SSH_TRANSPORT_INIT_START.format(
            export_destination=self)
        self._logger.debug(start_log)
        # Create Transport instance.
        try:
            transport = paramiko.Transport((self.ip, self.port),
                                           socket_timeout=self.socket_timeout)
        except Exception as e:
            # Log exception and re-raise,
            exception_log = logs.SSH_TRANSPORT_INIT_FAILURE.format(exception=e)
            self._logger.warn(exception_log)
            raise
        else:
            # Log success.
            success_log = logs.SSH_TRANSPORT_INIT_SUCCESS.format(
                export_destination=self)
            self._logger.info(success_log)
        # Increase banner timeout to prevent exception raised due to lack of
        # resources. See: https://stackoverflow.com/a/59453832/4416932.
        transport.banner_timeout = self.banner_timeout
        # Set encryption algorithm type.
        if self.key is not None:
            expected_name = self.key.get_name()
            transport._preferred_keys = [expected_name]
            # Log transport encryption key name.
            transport_key_log = logs.SSH_TRANSPORT_KEY.format(
                ip=self.ip, key_name=expected_name)
            self._logger.debug(transport_key_log)
        return transport

    def query_public_key(self) -> Tuple[str, bytes]:
        """
        Queries the remote host for a public key.

        Returns
        -------
        Tuple[str, bytes]
            Key type name, Value as bytes
        """
        # Log start.
        start_log = logs.SSH_HOST_KEY_QUERY_START.format(ip=self.ip)
        self._logger.debug(start_log)
        # Query host for public key.
        try:
            remote_key = self.transport.get_remote_server_key()
        except SSHException as e:
            # Log exception and re-raise.
            failure_log = logs.SSH_HOST_KEY_QUERY_FAILURE.format(ip=self.ip,
                                                                 exception=e)
            self._logger.info(failure_log)
            raise
        else:
            # Read key encryption name and value.
            name = remote_key.get_name()
            value = remote_key.asbytes()
            # Log success.
            success_log = logs.SSH_HOST_KEY_QUERY_SUCCESS.format(key_name=name,
                                                                 ip=self.ip)
            self._logger.info(success_log)
            return name, value

    def validate_public_key(self):
        """
        Validates the host's public key against the known hosts file.
        """
        if self.key:
            # Log validation start.
            start_log = logs.SSH_KEY_VALIDATION_START.format(ip=self.ip)
            self._logger.debug(start_log)
            # Read existing key information.
            expected_name = self.key.get_name()
            expected_value = self.key.asbytes()
            # Query key information from the host.
            name, value = self.query_public_key()
            # Check key validity.
            valid_key = name == expected_name and value == expected_value
            if not valid_key:
                # Log and raise exception for an invalid key.
                failure_log = logs.SSH_KEY_VALIDATION_FAILURE.format(
                    ip=self.ip, expected_name=expected_name, name=name)
                self._logger.warn(failure_log)
                raise SSHException(failure_log)
            # Log public key validation success.
            sucess_log = logs.SSH_KEY_VALIDATION_SUCCESS.format(ip=self.ip)
            self._logger.info(sucess_log)
        else:
            skip_log = logs.SSH_KEY_VALIDATION_SKIP.format(ip=self.ip)
            self._logger.info(skip_log)

    def authenticate(self):
        """
        Authenticate the transport session to the host using
        :attr:`username` and :attr:`password`.
        """
        # Log password authentication start.
        start_log = logs.SSH_PASSWORD_AUTH_START.format(
            export_destination=self)
        self._logger.debug(start_log)
        try:
            self.transport.auth_password(self.username, self.password)
        except Exception as e:
            failure_log = logs.SSH_TRANSPORT_INIT_FAILURE.format(
                export_destination=self, exception=e)
            self._logger.warn(failure_log)
        else:
            success_log = logs.SSH_PASSWORD_AUTH_SUCCESS.format(
                export_destination=self)
            self._logger.info(success_log)

    def connect(self, timeout: int = None) -> None:
        """
        Negotiates a connection with the host.

        Parameters
        ----------
        timeout : int
            SSH session negotiation timeout value (seconds)
        """
        if not self.transport.active:
            # Log SSH client session initialization start.
            start_log = logs.SSH_CONNECTION_START.format(
                export_destination=self)
            self._logger.debug(start_log)
            # Initialize SSH client session.
            try:
                self.transport.start_client(timeout=self.negotiation_timeout)
            except SSHException as e:
                # Log raised exception and re-raise.
                failure_log = logs.SSH_CONNECTION_FAILURE.format(
                    export_destination=self, exception=e)
                self._logger.info(failure_log)
                raise
            else:
                # Log success.
                success_log = logs.SSH_CONNECTION_SUCCESS.format(
                    export_destination=self)
                self._logger.info(success_log)
            self.validate_public_key()
            self.authenticate()
        else:
            # Log existing active connection found.
            skip_log = logs.SSH_TRANSPORT_ACTIVE.format(
                export_destination=self)
            self._logger.debug(skip_log)

    def start_sftp_client(self) -> paramiko.sftp_client.SFTPClient:
        """
        Returns an SFTP client, enabling secure interaction with the host
        filesystem.

        See Also
        --------
        * :meth:`sftp_client`

        Returns
        -------
        paramiko.sftp_client.SFTPClient
            SFTP Client connected to the host filesystem
        """
        # Log SFTP connection initialization.
        start_log = logs.SFTP_CLIENT_START.format(export_destination=self)
        self._logger.debug(start_log)
        # Establish SSH connection if it isn't already active.
        if not self.transport.active:
            self.connect()
        # Start SFTP client.
        try:
            sftp_client = paramiko.SFTPClient.from_transport(self.transport)
        except Exception as e:
            # Log exception and re-raise.
            failure_log = logs.SFTP_CLIENT_FAILURE.format(
                export_destination=self, exception=e)
            self._logger.warn(failure_log)
            raise
        else:
            # Log success and return SFTPClient instance.
            success_log = logs.SFTP_CLIENT_SUCCESS.format(
                export_destination=self)
            self._logger.info(success_log)
            # Disable relative path emulation, see:
            # https://docs.paramiko.org/en/stable/api/sftp.html#paramiko.sftp_client.SFTPClient.chdir
            sftp_client.chdir(path=None)
            return sftp_client

    def mkdir(
        self,
        path: Union[str, Path],
        parents: bool = True,
        exist_ok: bool = True,
    ):
        """
        Create directory within the host.

        Parameters
        ----------
        path : Union[str, Path]
            Directory path
        parents : bool
            Whether to create destination parents if they don't already exist
        exist_ok : bool
            Whether to raise an exception if the destination directory already
            exists
        """
        # Log directory creation start.
        start_log = logs.SFTP_MKDIR_START.format(path=path,
                                                 export_destination=self)
        self._logger.debug(start_log)
        # Iterate given directory destination parts and try to create.
        directory_names = []
        for part in path.parts:
            if part == "/":
                continue
            # Create path instance for current iteration.
            directory_names.append(part)
            current_path = Path("/" + "/".join(directory_names))

            # If not *parents* and the current path is not the destination
            # path, skip to last iteration.
            if not (parents or part == path.name):
                continue
            # If *parents* and the current path is not the destination path,
            # try to create the parent.
            elif parents and part != path.name:
                self.mkdir(current_path, parents=False, exist_ok=exist_ok)
                continue
            # Handle destination directory creation.
            else:
                try:
                    # Check if the directory already exists.
                    self.sftp_client.stat(str(current_path))
                except FileNotFoundError:
                    # Directory does not exist, create it.
                    try:
                        self.sftp_client.mkdir(str(current_path))
                    except OSError as e:
                        # Log failure and re-raise.
                        failure_log = logs.SFTP_MKDIR_FAILURE.format(
                            export_destination=self,
                            path=current_path,
                            exception=e,
                        )
                        self._logger.warn(failure_log)
                        raise
                    else:
                        # Log success.
                        success_log = logs.SFTP_MKDIR_SUCCESS.format(
                            export_destination=self, path=current_path)
                        self._logger.debug(success_log)
                else:
                    # Log existing directory found and raise or return.
                    log_exists = logs.SFTP_MKDIR_EXISTS.format(
                        path=current_path, export_destination=self)
                    self._logger.debug(log_exists)
                    if not exist_ok:
                        raise OSError(log_exists)

    def _put(self, source: Path, destination: Path):
        """
        Utility method to reduce clutter due to logging and surrounding logic.

        Parameters
        ----------
        source : Path
            Local file to copy
        destination : Path
            Absolute destination in the host file system
        """
        # Create parent directory if needed.
        self.mkdir(destination.parent, parents=True, exist_ok=True)
        # Transfer file.
        try:
            self.sftp_client.put(str(source), str(destination), confirm=True)
        except OSError as e:
            # Log file transfer failure and re-raise.
            failure_log = logs.SFTP_PUT_FAILURE.format(
                source=source,
                export_destination=self,
                destination=destination,
                exception=e,
            )
            self._logger.warning(failure_log)
            raise
        else:
            # Log success and return.
            success_log = logs.SFTP_PUT_SUCCESS.format(
                source=source,
                export_destination=self,
                destination=destination,
            )
            self._logger.debug(success_log)

    def put(
        self,
        source: Union[Path, str, Iterable[Union[Path, str]]],
        destination: Union[Path, str] = None,
        exist_ok: bool = True,
        force: bool = False,
        progressbar: bool = False,
    ) -> None:
        """
        Copies *source* (a filesystem accessible file path) to *destination* in
        the host using SFTP.

        Parameters
        ----------
        source : Union[Path, str]
            Local file to copy
        destination : Union[Path, str]
            Destination in the host file system. if None, tries to use the
            *source* path relative to the application's MEDIA_ROOT
        exist_ok : bool, optional
            Whether to forgive trying to put an existing file (rather than
            raising an exception), default is True
        force : bool, optional
            Whether to override the file if it already exists in the host,
            default is False (if set to True, *exist_ok* is meaningless)
        progressbar : bool, optional
            Whether to display a progressbar or not, applicable only if
            *source* is an interable of paths, default is False
        """
        # Handle iterable of paths.
        if not isinstance(source, (Path, str)):
            try:
                # Create progressbar if *progressbar* is True.
                iterable = (tqdm(
                    source, unit="file", desc=f"Copying to {self}")
                            if progressbar else source)
                # Iterate *source* and transfer files.
                for i, path in enumerate(iterable):
                    dest = destination[i] if destination else None
                    self.put(path, dest, exist_ok=exist_ok, force=force)
            except TypeError:
                # If iteration failed, log and re-raise.
                bad_input_log = logs.SFTP_PUT_BAD_INPUT.format(
                    bad_type=type(source))
                self._logger.warn(bad_input_log)
                raise
            else:
                return
        # Handle single file path.
        # Infer absolute destination path.
        destination = (Path(source).relative_to(settings.MEDIA_ROOT)
                       if destination is None else destination)
        destination = (Path(destination) if Path(destination).is_absolute()
                       else Path(self.destination) / destination)
        # Log file transfer start.
        start_log = logs.SFTP_PUT_START.format(source=source,
                                               export_destination=self,
                                               destination=destination)
        self._logger.debug(start_log)
        # Look for an existing file at the destination.
        try:
            self.sftp_client.stat(str(destination))
        except FileNotFoundError:
            # No existing file found, continue to file transfer.
            pass
        else:
            # Log existing file found.
            exists_log = logs.SFTP_PUT_EXISTS.format(export_destination=self,
                                                     destination=destination)
            self._logger.debug(exists_log)
            # Handle existing file found and *force* is False.
            if not force:
                # Log transfer termination and return.
                abort_log = logs.SFTP_PUT_ABORT.format(
                    source=source,
                    export_destination=self,
                    destination=destination,
                )
                self._logger.debug(abort_log)
                return
            # Handle existing file found and *exist_ok* is False.
            elif not exist_ok:
                raise OSError(exists_log)
        # Create parent directory if needed.
        self.mkdir(destination.parent, parents=True, exist_ok=True)
        # Transfer file.
        self._put(source, destination)

    @property
    def key(self):
        """
        Returns the key of the host if it exists within the *known_hosts* file.

        See Also
        --------
        * :meth:`get_key`

        Returns
        -------
        paramiko.ecdsakey.ECDSAKey
            Host key
        """
        if self._key is None:
            self._key = self.get_key()
        return self._key

    @property
    def transport(self):
        """
        Returns the transport instance which will be used to negotiate the
        connection.

        See Also
        --------
        :meth:`create_transport`

        Returns
        -------
        paramiko.Transport
            SSH transport thread
        """
        if self._transport is None:
            self._transport = self.create_transport()
        return self._transport

    @property
    def sftp_client(self) -> paramiko.sftp_client.SFTPClient:
        """
        Returns an SFTP client, enabling secure interaction with the host
        filesystem.

        See Also
        --------
        * :meth:`start_sftp_client`

        Returns
        -------
        paramiko.sftp_client.SFTPClient
            SFTP Client connected to the host filesystem
        """
        if self._sftp_client is None:
            self._sftp_client = self.start_sftp_client()
        return self._sftp_client
示例#20
0
class CallRecord(BasicModel, BIDModel):
    """ 点击拨号 """
    class Meta:
        verbose_name = 'CallRecord'
        verbose_name_plural = verbose_name
        index_together = ['provider', 'status', 'call_state']
        db_table = 'k_os_call_record'
        ordering = ('-created_at',)

    free_limit = 2  # 每天免费通话次数

    msgid = models.BigIntegerField('消息', unique=True)
    usrid = models.BigIntegerField('主叫用户', db_index=True, default=0)
    touchid = models.BigIntegerField('被叫用户', db_index=True, default=0)
    req_id = models.CharField('呼叫ID(供应商)', unique=True, max_length=50, null=True, default=None)
    status = models.SmallIntegerField('呼叫状态', choices=mc.CallStatus.choices, default=0)
    caller = mg_fields.EncryptedCharField(verbose_name='主叫号码', max_length=50, default='')
    called = mg_fields.EncryptedCharField(verbose_name='被叫号码', max_length=50, default='')
    callers_at = models.DateTimeField('主叫接听时间', db_index=True, null=True, default=None)
    callere_at = models.DateTimeField('主叫挂机时间', db_index=True, null=True, default=None)
    calleds_at = models.DateTimeField('被叫接听时间', db_index=True, null=True, default=None)
    callede_at = models.DateTimeField('被叫挂机时间', db_index=True, null=True, default=None)
    status_at = models.DateTimeField('状态同步时间戳', null=True, default=None)
    duration = models.PositiveSmallIntegerField('通话时长(秒)', default=0)  # 32767
    call_state = models.CharField('结果状态', max_length=20, default='')
    provider = models.CharField('服务提供商', max_length=20, default='')
    cost = models.PositiveSmallIntegerField('成本', default=0)
    fee = models.PositiveSmallIntegerField('计费', default=0)
    is_record = models.BooleanField('是否录音', default=False)
    record_file = models.URLField('录音文件', default='')

    objects = CallRecordManager()

    @property
    def callid(self):
        """ 外部ID """
        return self.hid

    @property
    def is_end(self):
        """ 是否终态 """
        is_yes = self.status in [
            mc.CallStatus.ENDCaller,
            mc.CallStatus.ENDCalled,
            mc.CallStatus.ENDOKCall,
        ]
        return is_yes

    @cached_property
    def msg_info(self):
        """ 会话消息 """
        from server.applibs.convert.models import Message
        inst = Message.objects.get(pk=self.msgid, sender=self.usrid)
        return inst

    @property
    def caller_seconds(self):
        """ 主叫接听时长 """
        if not (self.callers_at and self.callere_at):
            return 0
        assert self.callere_at > self.callers_at, self.pk
        seconds = (self.callere_at - self.callers_at).seconds
        return seconds

    @property
    def called_seconds(self):
        """ 被叫接听时长 """
        if not (self.calleds_at and self.callede_at):
            return 0
        assert self.callede_at > self.calleds_at, self.pk
        seconds = (self.callede_at - self.calleds_at).seconds
        return seconds

    @property
    def day_index(self):
        """ 当天第几次有效通话,以通话结束时间过滤 """
        if not self.call_ts:
            return 0
        end_at = deal_time.time_tzcn(self.callede_at)
        start_at = deal_time.time_floor_day(end_at)
        count = self.__class__.objects.filter(
            usrid=self.usrid,
            status=mc.CallStatus.ENDOKCall,
            callede_at__gte=start_at,
            callede_at__lt=end_at,
        ).count()
        return count + 1

    @property
    def call_ts(self):
        """ 有效通话时长 """
        if not (self.status == mc.CallStatus.ENDOKCall):
            return 0
        return self.called_seconds

    @property
    def summary(self):
        """ 呼叫摘要 """
        per = 60  # 分钟秒数
        m, s = self.call_ts // per, self.call_ts % per
        status = self.get_status_display()
        state_desc = str(self.call_state).split('|').pop()
        state_desc = state_desc.replace('正常', '')
        state_desc = state_desc.replace('应答', '')
        state_desc = state_desc.replace('未知', '')
        state_desc = state_desc or '请稍后重试'
        if self.status == mc.CallStatus.ENDOKCall:
            desc = f'{status},时长:{m}′ {s}″'
        elif self.status == mc.CallStatus.ENDCalled:
            desc = f'{status}:{state_desc}'
        elif self.status == mc.CallStatus.ENDCaller:
            desc = f'{status}:{state_desc}'
        else:
            desc = f'{status}...'
        return desc

    def call_yxt(self):
        """ 云讯,双向呼叫 """
        if self.req_id:
            warn_msg = f'call_yxt__done {self.pk} {self.req_id}'
            capture_message(warn_msg)
            logger.warning(warn_msg)
            return
        src = phonenumbers.parse(self.caller, None).national_number
        dst = phonenumbers.parse(self.called, None).national_number
        try:
            result = ytx_apis.YTXDailBackCallApi(src, dst, self.callid).fetch_result()
            self.req_id, self.provider = result['requestId'], mc.ThirdProvider.YTX
            self.save(update_fields=['req_id', 'provider', 'updated_at'])
        except Exception as exc:
            # {'statusCode': '-104', 'statusMsg': '请求频率过高'}
            self.call_state = str(exc)
            self.status = mc.CallStatus.ENDCaller
            self.save(update_fields=['status', 'call_state', 'updated_at'])
            self.msg_info.up_call_reach(self.status, self.summary)  # 更新触达状态,发起失败

    def query_call_ytx(self):
        """ 云讯,话单获取 """
        if not (self.req_id and (self.provider == mc.ThirdProvider.YTX)):
            warn_msg = f'query_ytx__info_error {self.pk} {self.provider}'
            capture_message(warn_msg)
            logger.warning(warn_msg)
            return
        now = deal_time.get_now()
        created_at = deal_time.time_floor_ts(self.created_at)
        cut_seconds = (now - created_at).seconds
        if cut_seconds < 20:
            logger.warning(f'query_ytx__too_early {self.pk} {cut_seconds}')
            return  # 查询太早没有结果
        result = ytx_apis.YTXCallCdrByResIdOneApi(
            lastresid=self.req_id
        ).fetch_result()
        if not isinstance(result, dict):
            return  # 无查询结果
        self.call_result_ytx_up(result, action='query')

    def callback_call_ytx(self, result):
        """ 云讯,话单回调 """
        if self.provider != mc.ThirdProvider.YTX:
            logger.warning(f'callback_ytx__provider_error {self.pk} {result}')
            return
        if self.req_id != result['requestid']:
            logger.warning(f'callback_ytx__req_id_error {self.pk} {result}')
            return
        self.call_result_ytx_up(result, action='callback')

    def call_result_ytx_up(self, result, action='query'):
        """ 云讯,话单更新 """
        key = f'{self.pk} {action} {self.req_id}'
        assert self.provider == mc.ThirdProvider.YTX, f'{key}: {result}'
        assert self.req_id == result['requestid'], f'{key}: {result}'
        self.duration = result['duration']
        self.call_state = result['stateDesc']
        self.cost = int(100 * result['oriamount'])
        self.callers_at = deal_time.get_tzcn_parse(result['callerstime'])
        self.callere_at = deal_time.get_tzcn_parse(result['calleretime'])
        self.calleds_at = deal_time.get_tzcn_parse(result['calledstime'])
        self.callede_at = deal_time.get_tzcn_parse(result['calledetime'])
        self.save(update_fields=[
            'callers_at', 'callere_at', 'calleds_at', 'callede_at',
            'call_state', 'duration', 'cost', 'updated_at',
        ])
        self.extra_log(f'result-{action}', result=result)
        self.final_status_check()
        self.checkout()

    def final_status_check(self):
        """ 话单更新后,确认通话最终状态 """
        old_status = self.status
        if self.caller_seconds and self.called_seconds:
            new_status = mc.CallStatus.ENDOKCall
        elif self.caller_seconds:
            new_status = mc.CallStatus.ENDCalled
        else:
            new_status = mc.CallStatus.ENDCaller
        self.status = new_status
        self.save(update_fields=['status', 'updated_at'])
        self.extra_log('status-check', status=self.status)
        if self.status == mc.CallStatus.ENDCalled:  # 暂未接通
            task = send_wxsm_for_msg_one.delay(self.msgid)
            logger.info(f'send_wxsm_for_msg_one__task {self.pk} {self.msgid} {task}')
        elif self.status == mc.CallStatus.ENDOKCall:  # 通话结束
            self.mark_msg_read()  # 消息更新已读
        self.msg_info.up_call_reach(self.status, self.summary)  # 更新触达状态,终态
        logger.warning(f'final_status_check__up {self.pk} {old_status} > {self.status}')

    def callback_status_ytx_up(self, result):
        """ 云讯回调,呼叫状态同步 """
        if self.is_end:
            self.extra_log('status-callback-end', result=result)
            logger.warning(f'cb_status_ytx__end {self.pk} {self.status} {result}')
            return
        state_desc = result['stateDesc']
        phone, state = result['dsc'], result['state']
        status_at = deal_time.get_tzcn_parse(result['timestamp'])
        if self.status_at and status_at and (self.status_at > status_at):
            later_info = f'{self.pk} {self.status} {self.status_at} {result}'
            logger.warning(f'callback_status_ytx_up__time_later {later_info}')
        elif status_at:
            self.status_at = status_at
        if str(self.caller).endswith(phone):  # 主叫
            if state in [YTXCallState.Callout, YTXCallState.Alerting]:  # 呼叫主叫...
                self.status = mc.CallStatus.OUTCaller
            elif state == YTXCallState.Answer:  # 主叫接听
                self.status = mc.CallStatus.OUTCalled
            elif state == YTXCallState.Disconnect:
                if self.status == mc.CallStatus.OUTCaller:  # 主叫未接听挂断
                    self.status = mc.CallStatus.ENDCaller
        elif str(self.called).endswith(phone):  # 被叫
            if state in [YTXCallState.Callout, YTXCallState.Alerting]:  # 呼叫被叫...
                if self.status != mc.CallStatus.OUTCalled:
                    self.status = mc.CallStatus.OUTCalled
            elif state == YTXCallState.Answer:  # 被叫接听
                self.status = mc.CallStatus.ONCalling
            elif state == YTXCallState.Disconnect:
                if self.status == mc.CallStatus.OUTCalled:  # 被叫未接听挂断
                    self.status = mc.CallStatus.ENDCalled
        else:
            logger.warning(f'callback_status_ytx_up__phone_error {self.pk} {result}')
        if self.is_end and not self.call_state:
            self.call_state = state_desc  # 话单消息可能先到达
        self.save(update_fields=['status', 'status_at', 'call_state', 'updated_at'])
        self.extra_log('status-callback', status=self.status, result=result)
        self.msg_info.up_call_reach(self.status, self.summary)  # 更新触达状态

    def checkout(self):
        """ 用户费用计算,每天两次免费通话,超次0.2元/分钟 """
        from server.applibs.billing.models import BillDetail
        if not (self.call_ts > 0):
            return
        self.fee = int(20 * math.ceil(self.call_ts / 60))
        self.save(update_fields=['fee', 'updated_at'])
        bill = BillDetail.objects.call_record_add(self)
        if not bill:
            return
        if bill.is_free or bill.is_paid:
            return
        # RTM推送通话账单 # 因小程序审核,计费功能2020-1105下线
        # self.msg_info.rtm_event_bill_reach(bill.hid)
        logger.info(f'rtm_event_bill_reach__offline {bill.pk}')

    def mark_msg_read(self):
        if self.status != mc.CallStatus.ENDOKCall:
            return
        if not self.callede_at:
            return
        self.msg_info.mark_read(self.touchid, self.callede_at)
        self.msg_info.conv_info.check_unread()
示例#21
0
class IDCard(BasicModel, BIDModel):
    """ 身份证 """
    class Meta:
        verbose_name = 'IDCard'
        verbose_name_plural = verbose_name
        index_together = ['sex', 'birth', 'nationality', 'is_valid']
        db_table = 'k_ac_idcard'
        ordering = ('-pk', )

    oss_keys = models.JSONField('身份证照片', default=dict)  # front、back
    shahash = models.CharField('SHA1签名', max_length=50, unique=True)
    usrid = models.BigIntegerField('用户', unique=True, null=True, default=None)
    number = mg_fields.EncryptedCharField(verbose_name='身份证号',
                                          max_length=100,
                                          unique=True)
    name = mg_fields.EncryptedCharField(verbose_name='姓名',
                                        max_length=200,
                                        default='')
    sex = models.CharField('性别', max_length=20, default='')
    nationality = models.CharField('民族', max_length=50, default='')
    birth = models.DateField('出生年月', null=True, default=None)
    address = mg_fields.EncryptedCharField(verbose_name='住址',
                                           max_length=255,
                                           default='')
    authority = mg_fields.EncryptedCharField(verbose_name='签发机构',
                                             max_length=255,
                                             default='')
    start_date = models.DateField('有效期开始日期', null=True, default=None)
    end_date = models.DateField('有效期结束日期', null=True,
                                default=None)  # 可能为None: 长期
    is_valid = models.BooleanField('是否有效', default=False)
    # 身份证照片是否是复印件、身份证照片是否是翻拍

    objects = IDCardManager()

    @property
    def img_front(self):
        """ 身份证正面图片,带水印,60秒内有效 """
        oss_front = self.oss_keys['front']
        url = oss.oss_sign_url(oss_front, expires=60, watermark=True)
        return url

    @property
    def img_back(self):
        """ 身份证背面图片,带水印,60秒内有效 """
        oss_back = self.oss_keys['back']
        url = oss.oss_sign_url(oss_back, expires=60, watermark=True)
        return url

    def img_hold_on(self):
        """ 认证通过后图片迁移目录 """
        oss_keys = self.oss_keys
        oss_front, oss_back = oss_keys['front'], oss_keys['back']
        is_ok_front, new_front_key = oss.oss_idcard_hold_on(oss_front)
        oss_keys['front'] = new_front_key if is_ok_front else oss_front
        is_ok_back, new_back_key = oss.oss_idcard_hold_on(oss_back)
        oss_keys['back'] = new_back_key if is_ok_back else oss_back
        if not (is_ok_front and is_ok_back):
            return False, '图片拉取失败'
        self.is_valid = True
        self.oss_keys = oss_keys
        self.save(update_fields=['is_valid', 'oss_keys', 'updated_at'])
        return True, self
示例#22
0
class SmsRecord(BasicModel, BIDModel):
    """ 阿里短信发送记录,仅通知消息,需要关心发送结果 """
    class Meta:
        verbose_name = 'SmsRecord'
        verbose_name_plural = verbose_name
        index_together = ['scene', 'status', 'err_code']
        db_table = 'k_os_sms_record'
        ordering = ('-created_at', )

    scene = models.CharField('场景',
                             choices=mc.SMSNoticeScene.choices,
                             max_length=25)
    number = mg_fields.EncryptedCharField(verbose_name='手机号',
                                          max_length=50,
                                          db_index=True)  # E164,加密
    usrid = models.BigIntegerField('触发用户', db_index=True, default=0)
    touchid = models.BigIntegerField('触达用户', db_index=True, default=0)
    bizid = models.CharField('回执', db_index=True, max_length=50, default='')  #
    sign = models.CharField('签名', max_length=25, default='')
    template = models.CharField('模板', max_length=50, default='')
    params = models.JSONField('模板参数', default=dict)
    status = models.SmallIntegerField('发送状态',
                                      choices=mc.SMSStatus.choices,
                                      default=0)
    report_at = models.DateTimeField('收到运营商回执时间', null=True, default=None)
    send_at = models.DateTimeField('转发给运营商时间', null=True, default=None)
    instid = models.BigIntegerField('关联对象', db_index=True,
                                    default=0)  # Message、PNVerify
    err_msg = models.CharField('错误信息', max_length=200, default='')
    err_code = models.CharField('错误码', max_length=50, default='')

    objects = SmsRecordManager()

    @property
    def sms_outid(self):
        """ 短信发送外部ID """
        return f'notice-{self.hid}'

    @cached_property
    def parse_info(self):
        info = phonenumbers.parse(self.number, None)
        return info

    @property
    def is_status_final(self):
        """ 是否已终态 """
        is_yes = self.status in [
            mc.SMSStatus.Success,
            mc.SMSStatus.Failure,
        ]
        return is_yes

    @property
    def is_dev_fake(self):
        """ 开发测试,转钉钉通知 """
        is_yes = self.bizid.startswith('dd-mock-')
        return is_yes

    @property
    def national(self):
        """ 国内号码,不带+86 """
        number = str(self.parse_info.national_number)
        return number

    def send(self):
        """ 发送 """
        if self.is_status_final:
            logger.warning(f'sms_send__status_final {self.pk}')
            return self.extra['resp_send']
        resp_dic = sms_action.sms_send__notice(self)
        self.extra['resp_send'] = resp_dic
        self.bizid = resp_dic.get('BizId', '')
        self.status = mc.SMSStatus.Waiting if self.bizid else mc.SMSStatus.Init
        self.save(update_fields=['status', 'bizid', 'extra', 'updated_at'])
        logger.info(f'SmsRecord.send__done {self.pk} {resp_dic}')
        return resp_dic

    def query(self):
        """ 主动查询回执状态 """
        if self.is_status_final and self.report_at:
            logger.info(f'sms_notice_query__final {self.pk}')
            return
        result = sms_action.sms_query__notice(self)
        assert result['OutId'] == self.sms_outid, f'{self.sms_outid} {result}'
        up_fields = [
            'extra', 'report_at', 'send_at', 'err_code', 'status', 'updated_at'
        ]
        self.report_at = get_tzcn_parse(result['ReceiveDate'])  # 短信接收日期和时间 ?!
        self.send_at = get_tzcn_parse(result['SendDate'])  # 短信发送日期和时间 ?!
        self.status = result['SendStatus']
        self.err_code = result['ErrCode']
        self.extra['resp_query'] = result
        self.save(update_fields=up_fields)

    def report_receipt(self, result):
        """ 短信发送回执MNS订阅 """
        self.err_msg = result['err_msg']
        self.err_code = result['err_code']
        self.extra['size'] = result['sms_size']
        self.send_at = get_tzcn_parse(result['send_time'])
        self.report_at = get_tzcn_parse(result['report_time'])
        if self.status in [mc.SMSStatus.Init, mc.SMSStatus.Waiting]:  # 回调时序问题
            self.status = mc.SMSStatus.Success if result[
                'success'] else mc.SMSStatus.Failure
        up_fields = [
            'err_msg', 'err_code', 'status', 'send_at', 'report_at', 'extra',
            'updated_at'
        ]
        self.save(update_fields=up_fields)
        return True