class LikeItem(ActionMixin, db.Model): __tablename__ = 'like_items' user_id = db.Column(db.Integer) target_id = db.Column(db.Integer) target_kind = db.Column(db.Integer) action_type = 'like' __table_args__ = ( db.Index('idx_ti_tk_ui', target_id, target_kind, user_id), )
class CollectItem(ActionMixin, db.Model): __tablename__ = 'collect_items' user_id = db.Column(db.Integer) target_id = db.Column(db.Integer) target_kind = db.Column(db.Integer) # 根据action_type判断相似功能的类, 如CommentItem, LikeItem action_type = 'collect' __table_args__ = (db.Index('idx_ti_tk_ui', target_id, target_kind, user_id), )
class CommentItem(ActionMixin, LikeMixin, db.Model): __tablename__ = 'comment_items' user_id = db.Column(db.Integer) target_id = db.Column(db.Integer) target_kind = db.Column(db.Integer) ref_id = db.Column(db.Integer, default=0) # 评论分文章评论,还有评论的评论,文章评论设置为0 content = PropsItem() kind = K_COMMENT action_type = 'comment' __table_args__ = (db.Index('idx_ti_tk_ui', target_id, target_kind, user_id), ) @cached_property def html_content(self): return self.content @cached_property def user(self): return User.get(self.user_id)
class Tag(db.Model): """ 原则上Tag一旦创建,则不能修改或删除 """ __tablename__ = 'tags' name = db.Column(db.String(128), default='', unique=True) __table_args__ = (db.Index('idx_name', name), ) @classmethod @cache(MC_KEY_GET_BY_NAME % ('{name}')) def get_by_name(cls, name): return cls.query.filter_by(name=name).first() def delete(self): raise NotAllowedException def update(self, **kwargs): raise NotAllowedException @classmethod def create(cls, **kwargs): name = kwargs.pop('name') kwargs['name'] = name.lower() return super().create(**kwargs)
class Contact(db.Model): """ 关注关系 """ __tablename__ = 'contacts' from_id = db.Column(db.Integer) # 关注者 follower to_id = db.Column(db.Integer) # 被关注者 followed __table_args__ = ( db.UniqueConstraint('from_id', 'to_id', name='uk_from_to'), db.Index('idx_to_time_from', to_id, 'created_at', from_id), db.Index('idx_time_to_from', 'created_at', to_id, from_id), ) def update(self, **kwargs): raise NotAllowedException @classmethod def create(cls, **kwargs): ok, obj = super().create(**kwargs) cls.clear_mc(obj, 1) if ok: from handler.tasks import feed_followed_posts_to_follower feed_followed_posts_to_follower.delay(obj.from_id, obj.to_id) return ok, obj def delete(self): super().delete() self.clear_mc(self, -1) from handler.tasks import remove_user_posts_from_feed remove_user_posts_from_feed.delay(self.from_id, self.to_id) @classmethod @cache(MC_KEY_GET_FOLLOWERS_PAGINATE % ('{to_id}', '{page}')) def get_followers_paginate(cls, to_id, page=1): """ 获取`followers`列表分页 """ query = cls.query.with_entities(cls.from_id).filter_by( to_id=to_id) followers = query.paginate(page, PER_PAGE) followers.items = [id for id, in followers.items] del followers.query return followers @classmethod @cache(MC_KEY_GET_FOLLOWING_PAGINATE % ('{from_id}', '{page}')) def get_following_paginate(cls, from_id, page=1): """ 获取`following`正在关注列表分页 """ query = cls.query.with_entities(cls.to_id).filter_by( from_id=from_id) following = query.paginate(page, PER_PAGE) following.items = [id for id, in following.items] del following.query return following @classmethod @cache(MC_KEY_GET_FOLLOW_ITEM % ('{from_id}', '{to_id}')) def get_follow_item(cls, from_id, to_id): """ 获取两用户是否关注 """ return cls.query.filter_by(from_id=from_id, to_id=to_id).first() @classmethod def clear_mc(cls, target, amount): """ 关注和取消都要清理缓存及更新相关对象 """ to_id = target.to_id from_id = target.from_id st = userFollowStats.get_or_create(to_id) follower_count = st.follower_count or 0 st.follower_count = follower_count + amount st.save() # 注意,放在sqlalchemy的清理钩子里,会报错 st = userFollowStats.get_or_create(from_id) following_count = st.following_count or 0 st.following_count = following_count + amount st.save() rdb.delete(MC_KEY_GET_FOLLOW_ITEM % (from_id, to_id)) for user_id, total, mc_key in ( (to_id, follower_count, MC_KEY_GET_FOLLOWERS_PAGINATE), (from_id, following_count, MC_KEY_GET_FOLLOWING_PAGINATE)): pages = math.ceil((max(total, 0) or 1) / PER_PAGE) for p in range(1, pages + 1): rdb.delete(mc_key % (user_id, p))
class Post(CommentMixin, LikeMixin, CollectMixin, db.Model): __tablename__ = 'posts' author_id = db.Column(db.Integer) title = db.Column(db.String(128), default='') orig_url = db.Column(db.String(255), default='') can_comment = db.Column(db.Boolean, default=True) content = PropsItem() kind = K_POST __table_args__ = ( db.Index('idx_title', title), # noqa db.Index('idx_authorId', author_id)) def url(self): return '/{}/{}/'.format(self.__class__.__name__.lower(), self.id) @classmethod @cache(MC_KEY_GET_BY_TITLE % ('{title}')) def get_by_title(cls, title): return cls.query.filter_by(title=title).first() @classmethod def get(cls, identifier): return super().get(identifier) if is_numeric(identifier) \ else cls.get_by_title(identifier) @property @cache(MC_KEY_TAGS % ('{self.id}')) def tags(self): tag_ids = PostTag.query.with_entities(PostTag.tag_id).\ filter(PostTag.post_id == self.id).all() tags = Tag.query.filter(Tag.id.in_((id for id, in tag_ids))).all() return tags @cached_property def abstract_content(self): return trunc_utf8(self.content, 100) @cached_property def author(self): return User.get(self.author_id) @classmethod def create_or_update(cls, **kwargs): """ 给爬虫使用,创建post """ tags = kwargs.pop('tags', []) created, obj = super().create_or_update(**kwargs) if tags: PostTag.update_multi(obj.id, tags, []) if created: from handler.tasks import feed_post_to_followers feed_post_to_followers.delay(obj.id) return created, obj def update(self, **kwargs): rdb.delete(MC_KEY_GET_BY_TITLE % self.title) # 注意,在更新前清除缓存 super().update(**kwargs) def delete(self): super().delete() for pt in PostTag.query.filter_by(post_id=self.id): pt.delete() from handler.tasks import remove_post_from_feed remove_post_from_feed.delay(self.id, self.author_id) @cached_property def netloc(self): return urlparse(self.orig_url).netloc @classmethod def clear_mc(cls, target): rdb.delete(MC_KEY_GET_BY_TITLE % target.title) rdb.delete(MC_KEY_TAGS % target.id) @classmethod def __flush_delete_event__(cls, target): super().__flush_delete_event__(target) cls.clear_mc(target)
class PostTag(db.Model): __tablename__ = 'post_tags' post_id = db.Column(db.Integer) tag_id = db.Column(db.Integer) __table_args__ = ( db.Index('idx_post_id', post_id, 'updated_at'), db.Index('idx_tag_id', tag_id, 'updated_at'), ) @classmethod def _get_posts_by_tag(cls, identifier): if not identifier: return if not is_numeric(identifier): tag = Tag.get_by_name(identifier) if not tag: return identifier = tag.id post_ids = cls.query.with_entities( cls.post_id).filter(cls.tag_id == identifier).all() query = Post.query.filter(Post.id.in_( id for id, in post_ids)).order_by(Post.id.desc()) return query @classmethod @cache(MC_KEY_GET_POSTS_BY_TAG % ('{identifier}', '{page}')) def get_posts_by_tag(cls, identifier, page=1): """ `identifier`: tag_id or tag_name """ query = cls._get_posts_by_tag(identifier) if not query: return [] posts = query.paginate(page, PER_PAGE) del posts.query # Fix `TypeError: can't pickle _thread.lock objects` return posts @classmethod @cache(MC_KEY_GET_COUNT_BY_TAG % ('{identifier}')) def get_post_count_by_tag(cls, identifier): """ identifier`: tag_id or tag_name """ query = cls._get_posts_by_tag(identifier) return query.count() if query else 0 @classmethod def update_multi(cls, post_id, tags, origin_tags=None): if origin_tags is None: origin_tags = Post.get(post_id).tags need_add = set(tags) - set(origin_tags) need_del = set(origin_tags) - set(tags) for tag_name in need_add: _, tag = Tag.create(name=tag_name) cls.create(post_id=post_id, tag_id=tag.id) for tag_name in need_del: _, tag = Tag.create(name=tag_name) obj = cls.query.filter_by(post_id=post_id, tag_id=tag.id).first() obj.delete() @classmethod def __flush_insert_event__(cls, target): super().__flush_insert_event__(target) cls.clear_mc(target, 1) @classmethod def __flush_delete_event__(cls, target): super().__flush_delete_event__(target) cls.clear_mc(target, -1) @classmethod def clear_mc(cls, target, amount): tag_id = target.tag_id tag_name = Tag.get(tag_id).name for ident in (tag_id, tag_name): total = incr_key(MC_KEY_GET_COUNT_BY_TAG % ident, amount) pages = math.ceil(max(total, 1) / PER_PAGE) for p in range(1, pages + 1): rdb.delete(MC_KEY_GET_POSTS_BY_TAG % (ident, p))