python教程:python中对于redis应用的封装

redis结构

(以下仅仅是我的个人经验,如果有更好的想法建议或者对内容有疑问的,可以留言)
附上 GITHUB地址

假定场景 老师,班级,学生,数学成绩,语文成绩 
老师:A老师,B老师
班级:1班,2班
学生:a学生,b学生,c学生。
成绩:英语,语文,数学...
在mysql中,理清楚这样的关系是非常容易的,那么,如果是在redis内,应该怎么做呢?
首先,我们需要有几个基本单元:
老师,班级,学生。
他们分别在redis可以这样子保存:
老师A => teacher:A
班级1 => class:1
学生a => student:a
那么如果实际情况里,老师A执教于班级1,班级2,老师B执教于班级2,学生a就读于班级1,学生b,c就读于班级2。那么参考mysql的关系表达式,在redis里可以同样使用关联表。
- 老师和班级的关系,由上可得是多对多:
  那么就需要两个表(列表形式存储):(如下可以很快的互相获取数据)
     "teacher:A:class" = ["class:1","class:2"](通过老师推导班级)
     "class:1:teacher" = ["teacher:A"](通过班级推导老师)
- 学生和班级的关系:由上可知是一对多:
   假定学生是一个字典(hash)存储的对象
    "student:a" = {
       ...
       "class":"class:1",
    },(直接通过键值获取,就可以得到对应的班级)
    "class:1:teacher" = ["student:a",...](因为一个班级有多个学生,所以只能通过第三张表来保存他们之间的关系)
- 学生和成绩的关系:由上可知是一对多,可以这么制作模型:
  同样假定学生是一个字典(hash)存储的对象
  "student:a" = {
       ...
       "chinese":100,
       "english":100,
       "math":50,
    },(直接通过键值获取,就可以得到对应的成绩)
  反之若是也无需求需要把成绩优秀的学生做成热门值,那么就需要做如下存储:
  performance:good = ["student:a",....]
  由上可知,在redis内,数据存储都是由 title:id:subtitle:id 的模式进行存储的,基于这个原理,衍生出来三种类型:

-1.admin对象:在上面就有3个admin对象,学生,老师,班级。他们三个分别有自己的内容属性,例如name,sex,student_count之类的。
-2.relation对象:
   如同上面的 "class:1:teacher","teacher:A:class",就是典型的关系对象,用来保存相关的admin对象。
-3.sub对象:
    sub对象是隶属于admin的对象,sub对象共有两种:
    1. 一种就是上面提到的成绩,可以直接保存于amdin对象的键值中。
    2. 那么问题来了,如果你的sub对象是一个list(数组)或者dict(hash)怎么办?因为redis的hash值只能是字符串。那么就需要单独创建一个sub对象来另外存储。上面的relation对象其实也可以当做是一种特殊的sub对象,只是他表达的是关系对象的名称,而不是直接表达admin对象的一个属性。
""

分析完了大体思路,接下来就直接上源码:

MemObject

首先是所有sub对象的子类,这里封装了基本用法, 用户的sub对象模型可以继承此类

class MemObject:
    '''
     内存数据模型(hash)
    '''
    _tablename_ = "MemObject"
    
    def __init__(self,pk):
        '''
        :param pk:主键,一般为数据库内的 id,或者设备名称,编号等唯一值
        '''
        self._connection = sync_redis
        self._fk= 0
        self._admin = ""
        self._pk = str(pk)
        self._lock = 0
        self.sync_count = 10

        self._key = None

    def __str__(self):
        # return "{} 数据为:\n {}".format(self.key,dict(self))
        return "{}:\n  key:{}\n----------------------------".format(self.__class__.__name__,self.key)

    def keys(self):
        '''
        返回一个tuple,该tuple决定了dict(self)所用到的键值
        
        :return:
        '''

    def setPK(self, pk):
        '''
        设置ID
        :param pk:
        :return:
        '''
        self._pk = pk

    def setFK(self,admin,fk):
        '''
        设置外键,若是关联数据,需要设置管理对象的id
        :param admin: 关联的admin对象的key
        :param fk :关联admin对象的pk
        :return:
        '''
        self._fk = str(fk)
        self._admin = str(admin) + ':'
        self.produceKey()
        return self

    def produceKey(self):
        '''
        生成redis保存的key
        规则为:外键key + : + _tablename_ + : + 主键
        '''
        self._key = ''.join([self._admin, self._tablename_, ':', self._pk])

    @property
    def key(self):
        '''
        保证每次需要使用 _key 时,同一个对象只会生成一次,减少cpu消耗
        :return:
        '''
        # Log.debug("使用_key")
        if not self._key:
            # Log.debug("载入/重载_key")
            self.produceKey()
        return self._key

    def locked(self):
        '''
        检测对象是否被锁定
        '''
        # return self._connection.get(self.key + "_lock")
        return False

    def lock(self):
        '''
        锁定对象,暂定最长锁定时间为2S(按业务需求自己添加锁)
        '''
        # return self._connection.set(self.key + "_lock", 1, 2)

    def release(self):
        '''
        释放锁(按业务需求自己释放)
        '''
        # return self._connection.delete(self.key + "_lock")
    
    def is_exist(self):
        return self._connection.exists(self.key)
    
    def get(self, key, default=None):
        '''
        本对象映射的哈希对象内的某个key值
        @:param key:需要获取的键值
        @:param default:若该键不存在,则返回 default
        @:return str
        '''
        ret = self._connection.hget(self.key, key)
        if ret is not None:
            return ret
        else:
            return default

    def get_multi(self, *keys):
        '''
        一次获取本对象映射的哈希对象内的多个key的值
        @param keys: list(str) key的列表
        :return: dict
        '''
        ret = self._connection.hmget2dic(self.key, *keys)
        return ret

    def get_all(self):
        '''
        获取本对象映射的哈希对象内的所有值(keys里面定义的所有值,而非getall)
        :return: self
        '''
        ret = self._connection.hmget(self.key, self.keys())
        return self.get_from_list(ret)

    def get_all2dic(self):
        '''
        获取本对象映射的哈希对象内的所有值(keys里面定义的所有值,而非getall)
        :return: 字典
        '''
        ret = self._connection.hmget2dic(self.key, self.keys())
        return ret

    def get_one(self):
        '''
        针对单键值
        :return:
        '''
        ret = self._connection.get(self.key)
        return ret
    
    def get_pattern(self, pattern):
        '''
        模糊查询(keys)谨慎使用
        :param pattern:
        :return: 字典
        '''
        ret = self._connection.keys(pattern)
        return ret

    def dbsize(self):
        '''

        :return:
        '''
        ret = self._connection.dbsize()
        return ret

    def update(self,key,value):
        '''
        修改对象的值
        '''
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            # Log.debug("拼接字段名称:{}".format(name))
            ret = self._connection.hset(self.key, key, value)
            # Log.debug("设置字段值{}:{}".format(key,value))
            self.release()
            # Log.debug("解锁字段")
            return ret
        else:
            # Log.err("update:该字段被锁定")
            return 1

    def update_multi(self,mapping):
        '''
        同时修改name下多个key的值
        '''
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            # Log.debug("拼接字段名称:{}".format(name))
            ret = self._connection.hmset(self.key,mapping)
            # Log.debug("设置字段值{}:{}".format(name,mapping))
            self.release()
            # Log.debug("解锁字段")
            return ret
        else:
            # Log.err("update_multi:该字段被锁定")
            # defer.returnValue(False)
            return None

    def update_one(self, value):
        '''
        修改本单个键值对象
        '''
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            # Log.debug("拼接字段名称:{}".format(name))
            ret = self._connection.set(self.key, value)
            # Log.debug("设置字段值{}:{}".format(key,value))
            self.release()
            # Log.debug("解锁字段")
            return ret
        else:
            # Log.err("update:该字段被锁定")
            return 1
    
    def delete(self):
        '''
        删除指定name的redis数据
        '''
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            # Log.debug("拼接字段名称:{}".format(self.key))
            # print("拼接字段名称:{}".format(self.key))
            ret = self._connection.delete(self.key)
            # Log.debug("设置字段值{}:{}".format(key,value))
            self.release()
            # Log.debug("解锁字段")
            return ret
        else:
            # Log.err("mdelete:该字段被锁定")
            return False

    def incr(self, key, delta):
        '''
        自增
        '''
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()

            # Log.debug("拼接字段名称:{}".format(name))
            ret = self._connection.hincrby(self.key, key, delta)
            # Log.debug("设置字段值{}:{}".format(key,value))
            self.release()
            # Log.debug("解锁字段")
            return ret
        else:
            # Log.err("incr:该字段被锁定")
            return None

    def expire(self,ex=0):
        self._connection.expire(self.key,ex)

    def insert(self):
        '''
        插入对象记录
        '''
        # return self._insert().addCallback(self.syncDB).addErrback(DefferedErrorHandle)
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            nowdict = dict(self)
            # Log.debug("拼接字段名称:{}".format(name))
            self._connection.hmset(self.key, nowdict)
            # Log.debug("设置字段值{}:{}".format(key,value))

            count = self._connection.hincrby(self.key, "_count", 1)

            self.release()

            self.syncDB(count)

            return True
        else:
            return False

    def insert_without_sync(self):
        '''
        插入本对象映射的哈希对象
        '''
        # return self._insert().addCallback(self.syncDB).addErrback(DefferedErrorHandle)

        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            nowdict = dict(self)
            # Log.debug("拼接字段名称:{}".format(name))
            self._connection.hmset(self.key, nowdict)
            # Log.debug("设置字段值{}:{}".format(key,value))

            self.release()

            return True
        else:
            return False

    def syncDB(self,count):
        '''

        :param count:
        :return:
        '''

    def saveDB(self):
        '''
        同步数据库
        :return:
        '''

    def get_from_dict(self, dic):
        '''
        将字典内的值依次赋予自身
        :param dic:
        :return: self
        '''
        for (k, v) in dic.items():
            if v != None:
                setattr(self, k, v)
        return self

    def get_from_list(self, list):
        '''
        将列表内的值,赋予自身(顺序按照keys排列)
        :param list:
        :return:
        '''
        for index, key in enumerate(self.keys()):
            if list[index] != None:
                setattr(self, key, list[index])
        return self

    def __getitem__(self, item):
        return getattr(self, item)

    def name2object(self,name):
        '''
        调用该函数可以通过redis中的key值
        转换成对应的mem对象
        子类需要重写才能实现该功能
        :param name:
        :return:
        '''
        name_ = name.split(self._tablename_+":")
        self._pk = name_[1]
        self._admin = name_[0]
        self._key = name
        return self

    @classmethod
    def scan(cls, admin=None,start=0, count=1000):
        '''
        通过搜索手段返回内存内所有该对象
        :param start:
        :param count:
        :return:
        '''
        admins = []
        rets = []
        if admin == None:
            pattern = cls._tablename_ + "*"
            # pattern = cls._tablename_ +"[1,2,3,4,5,6,7,8,9,0]"
            while True:
                start, t_ = sync_redis.scan(start, pattern, count)
                if t_:
                    admins += t_
                if not start:
                    break
            ret = list(set(admins))
            for item in ret:
                split_item = item.split(":")
                if len(split_item) == 2:
                    name_, id_ = split_item
                    ret_ = cls(id_)
                    rets.append(ret_)
            return rets
        else:
            pattern = admin._tablename_ +"*"+ cls._tablename_ + "*"
            while True:
                start, t_ = sync_redis.scan(start, pattern, count)
                if t_:
                    admins += t_
                if not start:
                    break
            return list(set(admins))

MemAdmin

第二个登场的是admin对象的子类,他继承自sub对象,拥有sub对象所有的属性和方法外,还有admin独有的一些属性方法

class MemAdmin(MemObject):
    '''
        内存数据模型(hash),一般用于admin模型
    '''
    _tablename_ = "MemAdmin"

    def __init__(self, pk):
        '''
        root节点
        :param pk :Mode主键,以此区分不同对象
        '''
        MemObject.__init__(self, pk)

    def build_leaf(self, leaf, pk, dict):
        '''
        创建 子节点 对象,且读取内存数据
        :param object: 外键连接对象(MemObject 及其子类)
        :param pk:     MemObject 及其子类 的 主键
        :param dict:   字典数据
        :return: MemObject 及其子类
        '''
        ret = leaf(pk).setFK(self.key, self._pk).get_from_dict(dict)
        return ret

    def build_empty_leaf(self, leaf, fk=""):
        '''
        创建 子节点 对象,但不读取内存数据
        :param leaf:
        :param fk:
        :return:
        '''
        return leaf(fk).setFK(self.key, self._pk)
    
    def build_empty_relation(self, relation,fk=""):
        '''
        创建 relation 对象,但不读取内存数据
        :return:
        '''
        return relation(fk).setFK(self.key, self._pk)
    
    def update_leaf(self, leaf, fk, dict):
        '''
        插入/更新 子节点对象,且更新内存数据
        :param leaf: 外键连接对象
        :param dict:   字典数据
        :return:
        '''
        return leaf(fk).setFK(self.key, self._pk).get_from_dict(dict).insert()

    def is_leaf_exits(self, leaf, fk):
        '''
        判断子节点对象是否存在内存中
        :return:
        '''
        return leaf(fk).setFK(self.key, self._pk).is_exist()

    def get_leaf(self,leaf,fk="",*keys):
        '''
        从内存中获取单个子节点对象
        :param object:  外键连接对象
        :return:
        '''
        if not keys:
            ret = leaf(fk).setFK(self.key,self._pk).get_all()
        else:
            ret_ = leaf(fk).setFK(self.key,self._pk).get_multi(*keys)
            ret = leaf.get_from_dict(ret_)
        return ret
    
    def get_leaf2dic(self,leaf,fk="",*keys):
        '''
        从内存中获取单个子节点数据(dict)
        :param object:  外键连接对象
        :return:
        '''
        if not keys:
            return leaf(fk).setFK(self.key, self._pk).get_all2dic()
        else:
            return leaf(fk).setFK(self.key, self._pk).get_multi(*keys)
    
    def get_leafs_by_relation(self,relation,fk="",start=0,count=1000):
        '''
        通过 relation 表 取出所有相关子节点key名称
        :param relation_name:
        :return:
        '''
        relation = relation(fk).setFK(self.key, self._pk)
        leafsname = relation.get_leafs_by_relation(start, count)
        ret = relation.names2leafs(leafsname)
        return ret

    def get_leafnames_by_relation(self,relation,fk="",start=0,count=1000):
        '''
        通过 relation 表 取出所有相关子节点key名称
        :param relation_name:
        :return:
        '''
        relation = relation(fk).setFK(self.key, self._pk)
        leafsname = relation.get_leafs_by_relation(start, count)
        return leafsname

    def add_leafs_on_relation(self,relation,leafs,fk=""):
        '''
        往 relation 表下 添加子节点元素
        :param relation:
        :param fk:
        :param leafs:
        :return:
        '''
        relation = relation(fk).setFK(self.key, self._pk)
        ret = relation.add_leafs(leafs)
        return ret
    
    def scan_leafnames(self,leaf,start=0,count=1000):
        '''
        搜索所有和该对象相关的 leaf 对象,并返回key值
        :param start:
        :param pattern:
        :param count:
        :return:
        '''
        keys = []
        leaf = leaf("").setFK(self.key,self._pk)
        pattern = leaf.key+":"+"*"
        while True:
            start,t_ = sync_redis.scan(start,pattern,count)
            if t_:
                keys += t_
            if not start:
                break
        return list(set(keys))

    def scan_leafs(self, leaf, start=0, count=1000):
        '''
        搜索所有和该对象相关的 leaf 对象,并返回对象值(并未从内存中获取填充数据)
        :param start:
        :param pattern:
        :param count:
        :return:
        '''
        leafs = []
        keys = self.scan_leafnames(leaf, start, count)
        for key in keys:
            l_ = leaf().name2object(key)
            if l_:
                leafs.append(l_)
        return leafs

    @classmethod
    def scan(cls, start=0, count=1000):
        '''
        通过搜索手段返回内存内所有该对象
        :param start:
        :param count:
        :return:
        '''
        admins = []
        rets = []
        pattern = cls._tablename_ + "*"
        # pattern = cls._tablename_ +"[1,2,3,4,5,6,7,8,9,0]"
        while True:
            start, t_ = sync_redis.scan(start, pattern, count)
            if t_:
                admins += t_
            if not start:
                break
        ret = list(set(admins))
        for item in ret:
            split_item = item.split(":")
            if len(split_item) == 2:
                name_,id_ = split_item
                ret_ = cls(id_)
                rets.append(ret_)
        return rets

MemRelation


class MemRelation(MemObject):
    '''
    内存模型间的中间关系表
    1 * ADMIN 《-》N * OBJECT
    '''
    _tablename_ = "MemRelation"

    def __init__(self, pk=''):
        '''
        :param pk :Mode主键,以此区分不同对象
        '''
        MemObject.__init__(self, pk)

    def produceKey(self):
        '''
        生成redis保存的key
        规则为:外键key + : + _tablename_ + : + 主键
        '''
        self._key =  ''.join([self._admin,self._tablename_])

    def count(self):
        '''
        获取与左外建相关的所有
        :return:
        '''
        ret = self._connection.llen(self.key)
        return ret

    def append(self,objects):
        '''
        往关系表内添加数据
        :param value:
        :return:
        '''
        if isinstance(objects,list):
            if objects and isinstance(objects[0],str):
                values = objects
            elif objects:
                values = [item.key for item in objects]
            else:
                return False
        else:
            values = objects.key

        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()

            self._connection.lpush(self.key, values)

            self.release()

            return True
        else:
            return False
    
    def remove_one(self,leaf,count=0):
        '''
        移除列表中下标从start开始的
        第一个值为value的数据
        :param count:
            count > 0 : 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT 。
            count < 0 : 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 COUNT 的绝对值。
            count = 0 : 移除表中所有与 VALUE 相等的值。
        :param value:
        :return:
        '''
        if isinstance(leaf, str):
            value = leaf
        else:
            value = leaf.key
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
            # try:
            self._connection.lrem(key=self.key,value=value,count = count)
            # except Exception as e:
            #     print(e)
            self.release()

            return True
        else:
            return False

    def get_by_index(self,index):
        ret  = self._connection.lindex(self.key,index)
        return ret

    def add_leafs_on_relation(self, leafs):
        '''
        同 append 方法,往 relation 表内插入 对象列表
        :param leafs:
        :return:
        '''
        if isinstance(leafs, list):
            values = [item.key for item in leafs]
        else:
            values = leafs.key
    
        locked = self.locked()
        # Log.debug("检查字段是否被锁定")
        if not locked:
            # Log.debug("字段检查通过")
            self.lock()
        
            self._connection.lpush(self.key, values)
        
            self.release()
        
            return True
        else:
            return False

    def get_leafs_by_relation(self, start, end):
        '''

        :param start:
        :param end:
        :return:
        '''
        ret = self._connection.lrange(self.key, start, end)
        return ret

    def get_all(self):
        '''

        :return:
        '''
        return self.get_leafs_by_relation(start=0,end=1000)

    def get_all2dic(self):
        '''

        :return:
        '''
        return self.get_leafs_by_relation(start=0,end=1000)

    def names2leafs(self, leafnames):
        '''

        :return:
        '''
        sensors = []
        for leafname in leafnames:
            sensor_ = self.name2object(leafname)
            if sensor_:
                sensors += sensor_.get_data()
        return sensors

    def name2object(self, name):
        '''
        调用该函数可以通过内存中的key值
        转换成对应的mem对象
        若是relation对象,则需要重写该方法
        因为该方法只能返回 Object 对象
        :return:
        '''

实例demo:

有空补上
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容