class ProxyChannelConfig(ABSChannelConfig): """ 代付通道配置 """ _banks = db.Column('banks', db.String(length=128), comment="支持的银行", nullable=False, default="") __table_args__ = ( # 联合唯一索引 db.UniqueConstraint('channel', 'version', name='uix_proxy_channel_config_channel_version'), ) @property def banks(self): bank_list = list() for value in StringParser(to_type=int).split(self._banks): bank_list.append(PaymentBankEnum(value)) return bank_list @banks.setter def banks(self, bank_list: List[PaymentBankEnum]): self._banks = StringParser().join([x.value for x in bank_list])
class ChannelConfig(ABSChannelConfig): """ 通道配置 """ _settlement_type = db.Column('st_type', db.Integer, comment="结算方式", nullable=False, default=0) priority = db.Column(db.Integer, comment="优先级(用于路由)", nullable=False, default=0) __table_args__ = ( # 联合唯一索引 db.UniqueConstraint('channel', 'version', name='uix_channel_config_channel_version'), ) @property def settlement_type(self) -> SettleTypeEnum: return SettleTypeEnum(self._settlement_type) @settlement_type.setter def settlement_type(self, value: SettleTypeEnum): self._settlement_type = value.value
def new_cls(cls, abs_cls, table_name, cls_name, bind_key=None): """ 生成模型类 :param abs_cls: :param table_name: :param cls_name: :param bind_key: :return: """ # 使用抽象类来生成子类 indexes = list() for v in abs_cls.UNION_UNIQUE_INDEX: indexes.append( db.UniqueConstraint(*v, name='uix_' + table_name + '_' + '_'.join(v))) for v in abs_cls.UNION_INDEX: indexes.append(db.Index('ix_' + table_name + '_' + '_'.join(v), *v)) return type( cls_name, (abs_cls, ), dict( __module__=abs_cls.__module__, __name__=cls_name, __tablename__=table_name, __bind_key__=bind_key, __table_args__=tuple(indexes), ))
class Target(db.Model): id = db.Column(db.Integer(), primary_key=True) target_from = db.Column(db.Float(), nullable=True) target_to = db.Column(db.Float(), nullable=True) indicator_id = db.Column(db.Integer(), db.ForeignKey('indicator.id'), nullable=False) objective_id = db.Column(db.Integer(), db.ForeignKey('objective.id', ondelete='CASCADE'), nullable=False) username = db.Column(db.String(120), default='') created = db.Column(db.DateTime(), default=datetime.utcnow) updated = db.Column(db.DateTime(), onupdate=datetime.utcnow, default=datetime.utcnow) __table_args__ = (db.UniqueConstraint( 'indicator_id', 'objective_id', name='target_indicator_id_objective_id_key'), ) def get_owner(self): return self.objective.product.product_group.name def __repr__(self): return '<Target {} | {} - {}>'.format(self.objective.product.name, self.target_from, self.target_to)
class AliasMailbox(db.Model, ModelMixin): __table_args__ = ( db.UniqueConstraint("alias_id", "mailbox_id", name="uq_alias_mailbox"), ) alias_id = db.Column(db.ForeignKey(Alias.id, ondelete="cascade"), nullable=False) mailbox_id = db.Column( db.ForeignKey(Mailbox.id, ondelete="cascade"), nullable=False )
class SocialAuth(db.Model, ModelMixin): """Store how user authenticates with social login""" user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False) # name of the social login used, could be facebook, google or github social = db.Column(db.String(128), nullable=False) __table_args__ = (db.UniqueConstraint("user_id", "social", name="uq_social_auth"),)
class AliasUsedOn(db.Model, ModelMixin): """Used to know where an alias is created""" __table_args__ = (db.UniqueConstraint("gen_email_id", "hostname", name="uq_alias_used"), ) gen_email_id = db.Column(db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=False) hostname = db.Column(db.String(1024), nullable=False)
class AliasUsedOn(db.Model, ModelMixin): """Used to know where an alias is created""" __table_args__ = ( db.UniqueConstraint("alias_id", "hostname", name="uq_alias_used"), ) alias_id = db.Column(db.ForeignKey(Alias.id, ondelete="cascade"), nullable=False) user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False) alias = db.relationship(Alias) hostname = db.Column(db.String(1024), nullable=False)
class SetEntry(Base): __table__name = 'set_entry' __table_args__ = (db.UniqueConstraint('set_num', 'entry_id', name='_set_num_entry_uc'), ) entry_id = db.Column(db.Integer, db.ForeignKey('exercise_entry.id'), nullable=False) set_num = db.Column(db.Integer, nullable=False) reps = db.Column(db.Integer, nullable=False) weight = db.Column(db.Integer, nullable=False) comment = db.Column(db.Text) bodyweight = db.Column(db.Boolean, default=False)
class ExerciseEntry(Base): __table__name = 'exercise_entry' __table_args__ = (db.UniqueConstraint('exercise_id', 'workout_id', name='_exercise_workout_uc'),) workout_id = db.Column(db.Integer, db.ForeignKey('workout.id'), nullable=False) ex_num = db.Column(db.Integer, nullable=False) exercise = db.relationship('Exercise') exercise_id = db.Column(db.Integer, db.ForeignKey('exercise.id'), nullable=False) sets = db.relationship('SetEntry', backref="exercise_entry", cascade="all, delete-orphan", order_by=('(SetEntry.set_num)'))
class ForwardEmail(db.Model, ModelMixin): """ Store configuration of sender (website-email) and alias. """ __table_args__ = ( db.UniqueConstraint("gen_email_id", "website_email", name="uq_forward_email"), ) gen_email_id = db.Column( db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=False ) # used to be envelope header, should be mail header from instead website_email = db.Column(db.String(512), nullable=False) # the email from header, e.g. AB CD <*****@*****.**> # nullable as this field is added after website_email website_from = db.Column(db.String(1024), nullable=True) # when user clicks on "reply", they will reply to this address. # This address allows to hide user personal email # this reply email is created every time a website sends an email to user # it has the prefix "reply+" to distinguish with other email reply_email = db.Column(db.String(512), nullable=False) gen_email = db.relationship(GenEmail, backref="forward_emails") def website_send_to(self): """return the email address with name. to use when user wants to send an email from the alias""" from app.email_utils import get_email_name if self.website_from: name = get_email_name(self.website_from) if name: return name + " " + self.website_email + f" <{self.reply_email}>" return self.website_email.replace("@", " at ") + f" <{self.reply_email}>" def last_reply(self) -> "ForwardEmailLog": """return the most recent reply""" return ( ForwardEmailLog.query.filter_by(forward_id=self.id, is_reply=True) .order_by(desc(ForwardEmailLog.created_at)) .first() )
class Message(db.Model): __tablename__ = "messages" __table_args__ = (db.UniqueConstraint("message_id", "chat_id"),) id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.String(), nullable=False) chat_id = db.Column(db.String(), nullable=False) message_id = db.Column(db.String(), nullable=False) created_date = db.Column(db.DateTime, default=datetime.now) def __init__(self, user_id, chat_id, message_id): self.user_id = user_id self.chat_id = chat_id self.message_id = message_id def __repr__(self): return "<Message {}>".format(self.id)
class Indicator(db.Model): id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(120), nullable=False, index=True) source = db.Column(db.JSON(), nullable=False) unit = db.Column(db.String(20), nullable=False, default='') aggregation = db.Column(db.String(80), default='average') is_deleted = db.Column(db.Boolean(), default=False, index=True, server_default=false()) product_id = db.Column(db.Integer(), db.ForeignKey('product.id'), nullable=False, index=True) slug = db.Column(db.String(120), nullable=False, index=True) targets = db.relationship('Target', backref=db.backref('indicator', lazy='joined'), lazy='dynamic') values = db.relationship('IndicatorValue', backref='indicator', lazy='dynamic', passive_deletes=True) username = db.Column(db.String(120), default='') created = db.Column(db.DateTime(), default=datetime.utcnow) updated = db.Column(db.DateTime(), onupdate=datetime.utcnow, default=datetime.utcnow) __table_args__ = (db.UniqueConstraint( 'name', 'product_id', 'is_deleted', name='indicator_name_product_id_key'), ) def get_owner(self): return self.product.product_group.name def __repr__(self): return '<SLI {} | {} | {}>'.format(self.product.name, self.name, self.source)
class Mailbox(db.Model, ModelMixin): user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False) email = db.Column(db.String(256), nullable=False) verified = db.Column(db.Boolean, default=False, nullable=False) force_spf = db.Column(db.Boolean, default=True, server_default="1", nullable=False) # used when user wants to update mailbox email new_email = db.Column(db.String(256), unique=True) pgp_public_key = db.Column(db.Text, nullable=True) pgp_finger_print = db.Column(db.String(512), nullable=True) __table_args__ = (db.UniqueConstraint("user_id", "email", name="uq_mailbox_user"),) def nb_alias(self): return Alias.filter_by(mailbox_id=self.id).count() @classmethod def delete(cls, obj_id): # Put all aliases belonging to this mailbox to global trash try: for alias in Alias.query.filter_by(mailbox_id=obj_id): # special handling for alias that has several mailboxes and has mailbox_id=obj_id if len(alias.mailboxes) > 1: # use the first mailbox found in alias._mailboxes first_mb = alias._mailboxes[0] alias.mailbox_id = first_mb.id alias._mailboxes.remove(first_mb) else: # only put aliases that have mailbox as a single mailbox into trash DeletedAlias.create(email=alias.email) db.session.commit() # this can happen when a previously deleted alias is re-created via catch-all or directory feature except IntegrityError: LOG.error("Some aliases have been added before to DeletedAlias") db.session.rollback() cls.query.filter(cls.id == obj_id).delete() db.session.commit() def __repr__(self): return f"<Mailbox {self.email}>"
class Membership(db.Model): """ 整个表可以认为zone_id 就是主键,因为逻辑是先保存zone表数据,才有zone_id, 所有数据zone_id绝对不会有重复值, 所以在删除的时候,需要以zone_id 查询,先删除该表数据,再删除zone表数据。 """ game_id = db.Column(db.Integer, db.ForeignKey('t_games.id'), primary_key=True) channel_id = db.Column(db.Integer, db.ForeignKey('t_channels.id'), primary_key=True) zone_id = db.Column(db.Integer, db.ForeignKey("t_zones.id"), primary_key=True) db.UniqueConstraint('game_id', 'channel_id', 'zone_id') db.relationship('Games', uselist=False, backref='memberships', lazy='dynamic') db.relationship('Channels', uselist=False, backref='memberships', lazy='dynamic') db.relationship('Zones', uselist=False, backref='memberships', lazy='dynamic') def __init__(self, game, channel, zone): self.game_id = game.id self.channel_id = channel.id self.zone_id = zone.id def __repr__(self): return "<Membership %s, %s, %s>" % (self.game_id, self.channel_id, self.zone_id)
class TeamMember(db.Model): """ Team-member database model. """ # pylint: disable=no-member __tablename__ = 'team_member' team_id = db.Column(db.Integer, db.ForeignKey('team.id'), primary_key=True) team = db.relationship('Team') user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) user = db.relationship('User', backref='teams_membership') is_leader = db.Column(db.Boolean, default=False, nullable=False) __table_args__ = ( db.UniqueConstraint('team_id', 'user_id', name='_team_user_uc'), ) def check_owner(self, user): return self.user == user def check_supervisor(self, user): return self.team.check_owner(user)
class TeamMember(db.Model): """ Team-member database model. """ __tablename__ = 'team_member' team_id = db.Column(db.Integer, db.ForeignKey('team.id'), primary_key=True) team = db.relationship('Team') user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) user = db.relationship( 'User', backref=db.backref('teams_membership', cascade='delete, delete-orphan') ) is_leader = db.Column(db.Boolean, default=False, nullable=False) __table_args__ = ( db.UniqueConstraint('team_id', 'user_id', name='_team_user_uc'), ) def __repr__(self): return ( "<{class_name}(" "team_id={self.team_id}, " "user_id=\"{self.user_id}\", " "is_leader=\"{self.is_leader}\"" ")>".format( class_name=self.__class__.__name__, self=self ) ) def check_owner(self, user): return self.user == user def check_supervisor(self, user): return self.team.check_owner(user)
class ClientUser(db.Model, ModelMixin): __table_args__ = ( db.UniqueConstraint("user_id", "client_id", name="uq_client_user"), ) user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False) client_id = db.Column(db.ForeignKey(Client.id, ondelete="cascade"), nullable=False) # Null means client has access to user original email gen_email_id = db.Column( db.ForeignKey(GenEmail.id, ondelete="cascade"), nullable=True ) # user can decide to send to client another name name = db.Column( db.String(128), nullable=True, default=None, server_default=text("NULL") ) # user can decide to send to client a default avatar default_avatar = db.Column( db.Boolean, nullable=False, default=False, server_default="0" ) gen_email = db.relationship(GenEmail, backref="client_users") user = db.relationship(User) client = db.relationship(Client) def get_email(self): return self.gen_email.email if self.gen_email_id else self.user.email def get_user_name(self): if self.name: return self.name else: return self.user.name def get_user_info(self) -> dict: """return user info according to client scope Return dict with key being scope name. For now all the fields are the same for all clients: { "client": "Demo", "email": "*****@*****.**", "email_verified": true, "id": 1, "name": "Son GM", "avatar_url": "http://s3..." } """ res = { "id": self.id, "client": self.client.name, "email_verified": True, "sub": str(self.id), } for scope in self.client.get_scopes(): if scope == Scope.NAME: if self.name: res[Scope.NAME.value] = self.name else: res[Scope.NAME.value] = self.user.name elif scope == Scope.AVATAR_URL: if self.user.profile_picture_id: if self.default_avatar: res[Scope.AVATAR_URL.value] = URL + "/static/default-avatar.png" else: res[Scope.AVATAR_URL.value] = self.user.profile_picture.get_url( AVATAR_URL_EXPIRATION ) else: res[Scope.AVATAR_URL.value] = None elif scope == Scope.EMAIL: # Use generated email if self.gen_email_id: LOG.debug( "Use gen email for user %s, client %s", self.user, self.client ) res[Scope.EMAIL.value] = self.gen_email.email # Use user original email else: res[Scope.EMAIL.value] = self.user.email return res
class BankCard(MerchantBase): """ 用户银行卡表,按商户分库 """ # __abstract__ = True valid = db.Column(db.SmallInteger, comment='是否删除', nullable=False) uid = db.Column(db.Integer, comment='用户ID', nullable=False) bank_name = db.Column(db.String(128), comment="银行名称例如中国工商银行", nullable=False) bank_code = db.Column(db.String(32), comment="银行简称例如ICBC", nullable=False) card_no = db.Column(db.String(32), comment="银行卡卡号例如:6212260405014627955", nullable=False) account_name = db.Column(db.String(20), comment="开户人姓名例如:张三", nullable=False) branch = db.Column(db.String(100), comment="支行名称例如:广东东莞东莞市长安镇支行", nullable=True) province = db.Column(db.String(32), comment="省份 例如:湖北省", nullable=False) city = db.Column(db.String(32), comment="市 例如:深圳市", nullable=False) __table_args__ = ( # 联合唯一索引 db.UniqueConstraint('card_no', 'merchant', name='uix_bank_card_no_merchant'), # 联合索引 # db.Index('ix_user_account_mch_name', 'account', 'merchant'), ) _merchant = db.Column('merchant', db.Integer, comment="商户ID", nullable=False) @property def merchant(self) -> MerchantEnum: return MerchantEnum(self._merchant) @merchant.setter def merchant(self, merchant: MerchantEnum): self._merchant = merchant.value @property def bank_enum(self): return PaymentBankEnum.get_bank_by_code(self.bank_code) @property def card_id(self): return self.id @property def short_description(self): return self.bank_name + '(' + self.card_no[-4:] + ')' + ' *' + self.account_name[1:] @property def card_no_short_description(self): card_hide = "**** **** **** {}".format(self.card_no[-4:]) # card_list = [self.card_no[index:index + 4] for index in range(0, len(self.card_no), 4)] # card_hide = "" # for index, num in enumerate(card_list): # if index != len(card_list)-1: # card_hide += "**** " # else: # card_hide += "{}".format(num) return card_hide @property def bank_address(self): return (self.province or '') + (self.city or '') + (self.branch or '') @classmethod def generate_model(cls, **kwargs): bank_card = cls.get_model_obj() bank_card.set_attr(kwargs) return bank_card @property def bank_info_dict(self): return dict( bank_name=self.bank_name, bank_code=self.bank_code, card_no=self.card_no, account_name=self.account_name, branch=self.branch, province=self.province, city=self.city, ) @classmethod def add_bank_card(cls, merchant: MerchantEnum, uid: int, bank_name: str, bank_code: str, card_no: str, account_name: str, branch: str, province: str, city: str): """ 添加用户银行卡 :param merchant: :param uid: :param bank_name: :param bank_code: :param card_no: :param account_name: :param branch: :param province: :param city: :return: """ with db.auto_commit(): bank_card = cls.query_one(query_fields=dict(card_no=card_no, valid=cls.INVALID, _merchant=merchant.value)) if not bank_card: bank_card = cls.get_model_obj(merchant=merchant) bank_card.valid = cls.VALID bank_card.uid = uid bank_card.bank_name = bank_name bank_card.bank_code = bank_code bank_card.card_no = card_no bank_card.account_name = account_name bank_card.branch = branch bank_card.province = province bank_card.city = city bank_card.merchant = merchant db.session.add(bank_card) return bank_card @classmethod def delete_bankcard_by_card_no(cls, merchant, card_no): """ 根据卡号删除用户银行卡 :param merchant: :param card_no: :return: """ with db.auto_commit(): bank_card = cls.query_bankcard_by_card_no(merchant, card_no=card_no) bank_card.valid = cls.INVALID db.session.add(bank_card) @classmethod def delete_bankcard_by_card_id(cls, card_id): """ 根据卡号删除用户银行卡 :param card_id: :return: """ with db.auto_commit(): bank_card = cls.query_bankcard_by_id(card_id=card_id) bank_card.valid = cls.INVALID db.session.add(bank_card) @classmethod def query_bankcard_by_card_no(cls, merchant, card_no): """ 根据卡号查询用户银行卡记录 :param merchant: :param card_no: :return: """ kwargs = dict(card_no=str(card_no), valid=cls.VALID, _merchant=merchant.value) return cls.query_one(query_fields=kwargs) @classmethod def query_bankcard_by_id(cls, card_id, valid_check=True): """ 根据卡id查询用户银行卡记录 :param card_id: :param valid_check: :return: """ if valid_check: kwargs = dict(id=str(card_id), valid=cls.VALID) else: kwargs = dict(id=str(card_id)) return cls.query_one(query_fields=kwargs) @classmethod def query_bankcards_by_uid(cls, merchant, uid): """ 根据卡号查询用户银行卡记录 :param merchant: :param uid: :return: """ kwargs = dict(uid=int(uid), valid=cls.VALID) return cls.query_model(query_fields=kwargs, merchant=merchant).order_by(cls._create_time.desc()).all() @classmethod def query_bankcards_by_bank_ids(cls, merchant, bank_ids): """ 查询一批银行卡的信息 :param merchant: :param bank_ids: :return: """ model_cls = cls.get_model_cls(merchant=merchant) return model_cls.query.filter(model_cls.id.in_(bank_ids))
class MerchantFeeConfig(AdminLogMix, ModelBase): """ 商户费率控制 """ admin_log_model = AdminLog _merchant = db.Column('merchant', db.Integer, comment="商户", nullable=False) version = db.Column(db.Integer, comment="版本", nullable=False, default=1) valid = db.Column(db.SmallInteger, comment="是否被删除的标记", nullable=False) _p_way = db.Column('p_way', db.SmallInteger, comment="支付形式", nullable=False) _p_method = db.Column('p_method', db.SmallInteger, comment="支付方法", nullable=False, default=0) _fee_type = db.Column('fee_type', db.SmallInteger, comment="费用类型", nullable=False, default=PaymentFeeTypeEnum.PERCENT_PER_ORDER.value) _value = db.Column('value', db.Integer, comment="值", nullable=False, default=0) _cost_type = db.Column('cost_type', db.Integer, comment="扣费类型", nullable=True) __table_args__ = ( # 联合唯一索引 db.UniqueConstraint( 'merchant', 'version', 'valid', 'p_way', 'p_method', name='uix_merchant_fee_version_valid_way_method'), ) @property def short_description(self): s = 'version: ' + str(self.version) + ', ' + ( self.payment_method.desc if self.payment_method else '') + \ self.value_description + "(" + self.cost_type.desc + ")" return s @property def config_id(self): return self.id @property def merchant(self) -> MerchantEnum: return MerchantEnum(self._merchant) @merchant.setter def merchant(self, value: MerchantEnum): self._merchant = value.value @property def cost_type(self) -> CostTypeEnum: if not self._cost_type: return CostTypeEnum.MERCHANT return CostTypeEnum(self._cost_type) @cost_type.setter def cost_type(self, value: CostTypeEnum): self._cost_type = value.value @property def payment_way(self) -> PayTypeEnum: return PayTypeEnum(self._p_way) @payment_way.setter def payment_way(self, value: PayTypeEnum): self._p_way = value.value @property def payment_method(self) -> PayMethodEnum: if self._p_method: return PayMethodEnum(self._p_method) @payment_method.setter def payment_method(self, value: PayMethodEnum): if value: self._p_method = value.value @property def fee_type(self) -> PaymentFeeTypeEnum: return PaymentFeeTypeEnum(self._fee_type) @fee_type.setter def fee_type(self, value: PaymentFeeTypeEnum): self._fee_type = value.value @property def value(self): return BalanceKit.divide_hundred(self._value) @value.setter def value(self, value): self._value = BalanceKit.multiple_hundred(value) @property def value_description(self): return "{}{}".format(self.value, self.fee_type.desc) @classmethod def convert_query_fields(cls, query_fields): params = dict(_merchant=query_fields['merchant'].value) if query_fields.get('payment_way'): params['_p_way'] = query_fields['payment_way'].value if query_fields.get('payment_method'): params['_p_method'] = query_fields['payment_method'].value return params @classmethod def query_latest_one(cls, query_fields): """ 以version倒序,查询最新的一个配置 :param query_fields: :return: """ params = cls.convert_query_fields(query_fields) return cls.query_one_order_by(query_fields=params, order_fields=[cls.version.desc()]) @classmethod def query_by_config_id(cls, config_id): """ 根据主键查询配置 :param config_id: :return: """ return cls.query_by_id(config_id) @classmethod def query_active_configs(cls, query_fields): """ 查询有效的配置 :param query_fields: :return: """ params = cls.convert_query_fields(query_fields) params['valid'] = cls.VALID return cls.query_model(params) @classmethod def update_fee_config(cls, merchant, params: list, models=None): """ 修改费率,批量操作 :param merchant: :param params: [ dict(merchant, payment_way, fee_type, value, payment_method), dict(merchant, payment_way, fee_type, value, payment_method), ] :param models: 商户和日志模型 :return: """ models = models or list() params_dict = cls.get_update_dict(params) all_configs = cls.query_active_configs(dict(merchant=merchant)) for item in all_configs.all(): if item.item_key not in params_dict: # 不在更新列表里面的,全部标记为删除 item.valid = cls.INVALID models.append(item) for fields in params: fields = copy.deepcopy(fields) fields['valid'] = cls.VALID model = cls.query_latest_one( dict( merchant=fields.get('merchant'), payment_way=fields.get('payment_way'), payment_method=fields.get('payment_method', None), )) if model: fields['version'] = model.version + 1 else: fields['version'] = 1 rst = cls.add_model(fields=fields) models.append(rst['model']) log_model = cls.add_admin_log(rst['model']) log_model and models.append(log_model) try: cls.commit_models(models=models) except IntegrityError as e: print(e) current_app.config['SENTRY_DSN'] and current_app.logger.fatal( traceback.format_exc()) return False, SqlIntegrityError() return True, None @classmethod def filter_latest_items(cls, items): """ 过滤出最新的配置,同一个渠道下的旧的配置会被排除掉 :param items: :return: """ # 按version值倒序,version值越大,记录越新 items = sorted(items, key=attrgetter('version'), reverse=True) latest = dict() for item in items: if not item.valid: # 已经删除的过滤掉 continue key = item.item_key if key in latest: # 已经存在的配置直接跳过 continue latest[key] = item return latest.values() @property def item_key(self): """ 唯一键 :return: """ return self._merchant, self._p_way, self._p_method @classmethod def get_update_dict(cls, update_list: list): tmp = dict() for fields in update_list: params = cls.convert_query_fields(fields) tmp[(params['_merchant'], params['_p_way'], params.get('_p_method'))] = fields return tmp @classmethod def get_latest_active_configs(cls, merchant, payment_way): """ 获取商户最新的有效的费率配置 :param merchant: :param payment_way: :return: """ merchant_fees = MerchantFeeConfig.query_active_configs( query_fields=dict( merchant=merchant, payment_way=payment_way, )).all() return MerchantFeeConfig.filter_latest_items(merchant_fees)
class OrderConstraint(ModelBase): """ 订单状态唯一约束 """ order_id = db.Column(db.Integer, comment='订单ID', nullable=False) _state = db.Column('state', db.SmallInteger, comment="订单状态", nullable=False) __table_args__ = ( # 联合唯一索引 db.UniqueConstraint('order_id', 'state', name='uix_order_constraint_order_id_state'), ) @property def state(self) -> OrderStateEnum: return OrderStateEnum(self._state) @state.setter def state(self, value: OrderStateEnum): self._state = value.value @classmethod def query_by_order_id(cls, order_id): return cls.query_one(dict(order_id=order_id)) @classmethod def revoke_order_state(cls, order_id, state: OrderStateEnum): """ 回滚订单状态 :param order_id: :param state: :return: """ if not state: return cls.get_model_cls().query.filter_by(order_id=order_id).update( dict(_state=state.value)) @classmethod def apply_ref_id(cls, order_id, order_type: PayTypeEnum, state: OrderStateEnum): """ 申请修改订单的票据ID :param order_id: :param order_type: :param state: :return: """ if not state: return OrderUtils.gen_unique_ref_id() params = copy.deepcopy(locals()) params.pop('cls') if state == OrderStateEnum.INIT: # 初始化状态无需前置状态约束 model = cls.get_model_obj() model.order_id = order_id model.state = state model.commit_models(model) return OrderUtils.gen_unique_ref_id() query_params = dict(order_id=order_id, ) if order_type == PayTypeEnum.WITHDRAW: if state == OrderStateEnum.ALLOC: query_params.update(_state=OrderStateEnum.INIT.value) elif state == OrderStateEnum.DEALING: query_params.update(_state=OrderStateEnum.ALLOC.value) elif state == OrderStateEnum.SUCCESS: query_params.update(_state=OrderStateEnum.DEALING.value) elif state == OrderStateEnum.FAIL: # 状态变更为失败,无需前置状态约束 pass else: raise ValueError('invalid state, params: %s' % params) elif order_type == PayTypeEnum.DEPOSIT: if state == OrderStateEnum.SUCCESS: query_params.update(_state=OrderStateEnum.INIT.value) elif state == OrderStateEnum.FAIL: # 状态变更为失败,无需前置状态约束 pass else: raise ValueError('invalid state, params: %s' % params) elif order_type == PayTypeEnum.DEPOSIT: if state == OrderStateEnum.FAIL: # 只有状态成功后才有退款 query_params.update(_state=OrderStateEnum.SUCCESS.value) else: raise ValueError('invalid state, params: %s' % params) # 让数据库来确保事务 with db.auto_commit(): effect = cls.get_model_cls().query.filter_by( **query_params).update(dict(_state=state.value)) if effect != 1: msg = '修改订单状态的票据ID申请失败,query_params: %s, params: %s' % ( query_params, params) current_app.config['SENTRY_DSN'] and current_app.logger.error( msg) return None return OrderUtils.gen_unique_ref_id()
db.Integer, db.ForeignKey('document.id'), nullable=False), db.PrimaryKeyConstraint('rule_id', 'document_id')) term_to_term_relationship = db.Table('term_to_term_relationship', db.Column('term_id', db.Integer, db.ForeignKey('term.id'), primary_key=True), db.Column('related_term_id', db.Integer, db.ForeignKey('term.id'), primary_key=True), db.UniqueConstraint('term_id', 'related_term_id', name='unique_related_terms')) class Term(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100)) short_description = db.Column(db.String(200)) long_description = db.Column(db.Text) abbreviation = db.Column(db.String(10)) categories = db.relationship('Category', secondary=term_category_relationship, backref='terms') columns = db.relationship('Column', secondary=term_column_relationship, backref='terms') documents = db.relationship('Document', secondary=term_document_relationship, backref='terms')
class Contact(db.Model, ModelMixin): """ Store configuration of sender (website-email) and alias. """ __table_args__ = ( db.UniqueConstraint("alias_id", "website_email", name="uq_contact"), ) user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False) alias_id = db.Column(db.ForeignKey(Alias.id, ondelete="cascade"), nullable=False) name = db.Column( db.String(512), nullable=True, default=None, server_default=text("NULL") ) website_email = db.Column(db.String(512), nullable=False) # the email from header, e.g. AB CD <*****@*****.**> # nullable as this field is added after website_email website_from = db.Column(db.String(1024), nullable=True) # when user clicks on "reply", they will reply to this address. # This address allows to hide user personal email # this reply email is created every time a website sends an email to user # it has the prefix "reply+" to distinguish with other email reply_email = db.Column(db.String(512), nullable=False) # whether a contact is created via CC is_cc = db.Column(db.Boolean, nullable=False, default=False, server_default="0") alias = db.relationship(Alias, backref="contacts") user = db.relationship(User) def website_send_to(self): """return the email address with name. to use when user wants to send an email from the alias Return "First Last | email at example.com" <ra+random_string@SL> """ # Prefer using contact name if possible name = self.name # if no name, try to parse it from website_from if not name and self.website_from: try: from app.email_utils import parseaddr_unicode name, _ = parseaddr_unicode(self.website_from) except Exception: # Skip if website_from is wrongly formatted LOG.warning( "Cannot parse contact %s website_from %s", self, self.website_from ) name = "" # remove all double quote if name: name = name.replace('"', "") if name: name = name + " | " + self.website_email.replace("@", " at ") else: name = self.website_email.replace("@", " at ") # cannot use formataddr here as this field is for email client, not for MTA return f'"{name}" <{self.reply_email}>' def new_addr(self): """ Replace original email by reply_email. Possible formats: - [email protected] via SimpleLogin <reply_email> OR - First Last - first at example.com <reply_email> OR - First Last - first(a)example.com <reply_email> OR - First Last - [email protected] <reply_email> OR And return new address with RFC 2047 format `new_email` is a special reply address """ user = self.user if ( not user or not SenderFormatEnum.has_value(user.sender_format) or user.sender_format == SenderFormatEnum.VIA.value ): new_name = f"{self.website_email} via SimpleLogin" elif user.sender_format == SenderFormatEnum.AT.value: name = self.name or "" new_name = ( name + (" - " if name else "") + self.website_email.replace("@", " at ") ).strip() elif user.sender_format == SenderFormatEnum.A.value: name = self.name or "" new_name = ( name + (" - " if name else "") + self.website_email.replace("@", "(a)") ).strip() elif user.sender_format == SenderFormatEnum.FULL.value: name = self.name or "" new_name = (name + (" - " if name else "") + self.website_email).strip() new_addr = formataddr((new_name, self.reply_email)).strip() return new_addr.strip() def last_reply(self) -> "EmailLog": """return the most recent reply""" return ( EmailLog.query.filter_by(contact_id=self.id, is_reply=True) .order_by(desc(EmailLog.created_at)) .first() ) def __repr__(self): return f"<Contact {self.id} {self.website_email} {self.alias_id}>"
class User(MerchantBase): """ 用户,按商户分库 """ id = db.Column(db.Integer, primary_key=True, autoincrement=False, comment='用户ID,主键但不自增,由全局表生成id') _create_time = db.Column('create_time', db.Integer, nullable=False, comment="创建时间", index=True) account = db.Column(db.String(32), comment="账号,手机号码/邮箱", nullable=False) _ac_type = db.Column('ac_type', db.SmallInteger, default=AccountTypeEnum.NONE.value, comment="账号类型,手机号码/邮箱") _state = db.Column('state', db.SmallInteger, default=AccountStateEnum.ACTIVE.value, comment="账号状态") _login_pwd = db.Column('login_pwd', db.String(100), comment="登录密码,已加密存储") _trade_pwd = db.Column('trade_pwd', db.String(100), comment="支付密码,已加密存储") _flag = db.Column('flag', db.SmallInteger, comment="账号状态", nullable=True) _permissions = db.Column('permissions', db.SmallInteger, comment="账号权限", nullable=True) __table_args__ = ( # 联合唯一索引 db.UniqueConstraint('account', 'merchant', name='uix_user_account_mch_name'), # 联合索引 # db.Index('ix_user_account_mch_name', 'account', 'merchant'), ) _merchant = db.Column('merchant', db.Integer, comment="商户ID", nullable=False) @property def merchant(self) -> MerchantEnum: return MerchantEnum(self._merchant) @merchant.setter def merchant(self, merchant: MerchantEnum): self._merchant = merchant.value @property def permissions(self) -> List[UserPermissionEnum]: if not self._permissions: return UserPermissionEnum.get_all_enums() return UserPermissionEnum.parse_permissions(self._permissions) @permissions.setter def permissions(self, values: List[UserPermissionEnum]): self._permissions = UserPermissionEnum.join_permissions(values) @property def permission_names(self): return [x.name for x in self.permissions] def has_permission(self, perm: UserPermissionEnum): """ 判断用户是否有权限 :param perm: :return: """ if not self._permissions: # 未设置权限,拥有所有权限 return True return perm.has_permission(self._permissions) @property def flag(self) -> AccountFlagEnum: if not self._flag: return AccountFlagEnum.NORMAL return AccountFlagEnum(self._flag) @flag.setter def flag(self, value: AccountFlagEnum): if value: self._flag = value.value @property def is_official_auth(self): return self.flag == AccountFlagEnum.VIP @property def is_test_user(self): """ 是否是测试用户 :return: """ return self.merchant.is_test @property def uid(self): return self.id @property def is_active(self): return self._state == AccountStateEnum.ACTIVE.value @property def state(self) -> AccountStateEnum: return AccountStateEnum(self._state) @state.setter def state(self, value: AccountStateEnum): self._state = value.value @property def ac_type(self): """ 返回账户枚举类型 :return: """ return AccountTypeEnum(self._ac_type) @ac_type.setter def ac_type(self, e_value): """ 传入账户的类型 :param e_value: :return: """ if e_value: self._ac_type = e_value.value @property def login_pwd(self): """ 登录密码 :return: """ return self._login_pwd @login_pwd.setter def login_pwd(self, raw_pwd): """ 设置密码时要进行加密 :param raw_pwd: :return: """ if raw_pwd: self._login_pwd = generate_password_hash(raw_pwd) @property def trade_pwd(self): """ 交易密码 :return: """ return self._trade_pwd @trade_pwd.setter def trade_pwd(self, raw_pwd): """ 设置交易密码 :param raw_pwd: :return: """ if raw_pwd: self._trade_pwd = generate_password_hash(raw_pwd) def has_trade_pwd(self): return bool(self.trade_pwd) @classmethod def generate_model(cls, merchant, account, ac_type: AccountTypeEnum = None, login_pwd=None): """ 生成用户模型 :param merchant: :param account: :param ac_type: :param login_pwd: :return: """ user = cls.get_model_obj(merchant=merchant) user.account = account user.ac_type = ac_type user.login_pwd = login_pwd user.merchant = merchant user.id = 0 return user @classmethod def register_account(cls, merchant, account, ac_type: AccountTypeEnum = None, login_pwd=None): """ 注册账号 :param merchant: :param account: :param ac_type: :param login_pwd: :return: """ uid = GlobalUid.make_uid(merchant) with db.auto_commit(): user = cls.generate_model(merchant, account, ac_type, login_pwd) user.id = uid db.session.add(user) balance = UserBalance.generate_model(user.uid, merchant) db.session.add(balance) return user @classmethod def delete_account(cls, merchant, uid=None, account=None): """ 删除账号 :param uid: :param account: :param merchant: :return: """ with db.auto_commit(): user = cls.query_user(merchant, uid=uid, account=account) db.session.delete(user) @classmethod def update_user_state(cls, merchant, account=None, uid=None, state=None): """ 修改用户状态 :param merchant: :param account: :param uid: :param state: :return: """ with db.auto_commit(): if not account: user = cls.query_user(merchant=merchant, uid=uid) else: user = cls.query_user(merchant=merchant, account=account) if user is not None: user.state = state db.session.add(user) return True else: return False @classmethod def update_user_flag(cls, merchant, flag: AccountFlagEnum, account=None, uid=None): """ 修改用户标签 """ if account: user = cls.query_user(merchant=merchant, account=account) else: user = cls.query_user(merchant=merchant, uid=uid) if user is not None: user.flag = flag cls.commit_models(user) UserFlagCache(user.uid).set_flag(flag) return True return False @classmethod def update_user_permission(cls, merchant, permissions: List[UserPermissionEnum], account=None, uid=None): """ 修改用户权限 """ if account: user = cls.query_user(merchant=merchant, account=account) else: user = cls.query_user(merchant=merchant, uid=uid) if user is not None: user.permissions = permissions cls.commit_models(user) return True return False @classmethod def reset_password(cls, merchant, account=None, uid=None, login_pwd=None): """ 修改密码 :param merchant: :param account: :param uid: :param login_pwd: :return: """ with db.auto_commit(): if not account: user = cls.query_user(merchant=merchant, uid=uid) else: user = cls.query_user(merchant=merchant, account=account) if user is not None: user.login_pwd = login_pwd db.session.add(user) return True else: return False @classmethod def verify_login(cls, merchant, account, password): """ 账号密码鉴权 :param merchant: :param account: :param password: :return: """ user = cls.query_user(merchant, account=account) if not user: return False return user.check_login_pwd(password) @classmethod def verify_password(cls, merchant, uid, password): """ 账号密码鉴权 :param merchant: :param uid: :param password: :return: """ user = cls.query_user(merchant, uid=uid) if not user: return False return user.check_login_pwd(password) def check_login_pwd(self, raw_pwd): return check_password_hash(self._login_pwd, raw_pwd) @classmethod def query_user(cls, merchant, uid=None, account=None): """ 查询用户 :param merchant: :param uid: :param account: :return: """ if uid: kwargs = dict(id=int(uid)) elif account: kwargs = dict(account=account, ) else: raise ValueError('parameter error') kwargs['_merchant'] = merchant.value return cls.query_one(query_fields=kwargs) @classmethod def set_payment_password(cls, merchant, account=None, uid=None, trade_pwd=None): """ 修改密码 :param merchant: :param account: :param uid: :param trade_pwd: :return: """ with db.auto_commit(): if not account: user = cls.query_user(merchant=merchant, uid=uid) else: user = cls.query_user(merchant=merchant, account=account) if user is not None: user.trade_pwd = trade_pwd db.session.add(user) return True else: return False @classmethod def verify_payment_password(cls, merchant, uid, password): """ 支付密码鉴权 :param merchant: :param uid: :param password: :return: """ user = cls.query_user(merchant, uid=uid) if not user: return False return user.check_trade_pwd(password) def check_trade_pwd(self, raw_pwd): """ 验证交易密码 :param raw_pwd: :return: """ if not self._trade_pwd: return False return check_password_hash(self._trade_pwd, raw_pwd)
class UserBindInfo(ModelBase): id = db.Column(db.Integer, primary_key=True, autoincrement=False, comment='用户ID', nullable=False) name = db.Column(db.String(64), comment="名称", nullable=False) account = db.Column(db.String(32), comment="账号", nullable=False, index=True) _bind_type = db.Column('bind_type', db.SmallInteger, comment="绑定类型", nullable=False) _merchant = db.Column('merchant', db.Integer, comment="商户ID", nullable=False) __table_args__ = ( # 联合唯一索引 db.UniqueConstraint('name', 'bind_type', 'merchant', name='uix_user_bind_name_bind_type_merchant'), ) @property def uid(self): return self.id @uid.setter def uid(self, value): self.id = value @property def merchant(self) -> MerchantEnum: return MerchantEnum(self._merchant) @merchant.setter def merchant(self, merchant: MerchantEnum): self._merchant = merchant.value @property def bind_type(self): """ 返回账户枚举类型 :return: """ return AccountTypeEnum(self._bind_type) @bind_type.setter def bind_type(self, e_value): """ 传入账户的类型 :param e_value: :return: """ self._bind_type = e_value.value @classmethod def query_bind_by_uid(cls, uid): """ 根据用户ID查询绑定信息 :param uid: :return: """ return cls.query_one(query_fields=dict(id=uid)) @classmethod def query_bind(cls, merchant: MerchantEnum, name, bind_type: AccountTypeEnum = AccountTypeEnum.ACCOUNT): return cls.query_one(query_fields=dict( _merchant=merchant.value, name=name, _bind_type=bind_type.value, )) @classmethod def bind_account(cls, uid, merchant: MerchantEnum, account, name, bind_type: AccountTypeEnum = AccountTypeEnum.ACCOUNT): obj = cls() obj.uid = uid obj.merchant = merchant obj.name = name obj.account = account obj.bind_type = bind_type cls.commit_models(obj) return obj @classmethod def unbind_account(cls, uid): obj = cls.query_bind_by_uid(uid) if not obj: return False cls.commit_models(obj, delete=True) return True