【1009 | Day 40】仿优酷

仿优酷项目

1. read.me

管理员
    1 注册
    2 登录
    3 上传视频
    4 删除视频
    5 发布公告

用户
    1 注册
    2 登录
    3 冲会员
    4 查看视频
    5 下载免费视频
    6 下载收费视频
    7 查看观影记录
    8 查看公告

2. 项目框架

层级结构:
    客户端
    服务端
    数据库

客户端:
    -tcp_client: 基于tcp连接的套接字程序
    -admin: 管理员视图
        注册
        登录
        上传视频
        删除视频
        发布公告
    -user: 用户视图
        注册
        登录
        购买vip
        查看视频
        下载免费视频
        下载收费视频
        查看下载记录
        查看公告

服务端:
    tcpserver:基于多线程实现并发的套接字通信  解决粘包问题
    interface:admin_interface、user_interface、common_interface
    models类和ORM框架:models类中的四张表继承ORM框架中的基类model

数据库:
    创建四张表:user、movie、notice、download_record

3. orm框架

优点:
    能够简单快速操作数据库实现相应功能

缺点:
    sql封装固定,不利于sql查询优化

对象关系映射
    类                    >>>                数据库的表
    对象                   >>>                表的一条条的记录
    对象获取属性或方法       >>>                记录的字段对应的值

一张表有字段,字段又有字段名,字段类型,字段是否是主键,字段的默认值

class Field:
    pass

# 为了在定义的时候更加方便 通过继承Field定义具体的字段类型
class IntegerField(Field):
    pass

class StringField(Field):
    pass

class Models(dict, metaclass=OrmMetaClass):
    pass
    
    def __init__(self, **kwargs):
    	pass

    def __getattr__(self,item):
        return self.get(item)

    def __setattr__(self,key,value)
        self[key] = value

    # 查询
    def select(self,**kwargs):
        # select * from userinfo
        # select * from userinfo where id = 1

    # 新增
    def save(self):
        # insert into userinfo(name,password) values('jason','123')


    # 修改:是基于已经存在了的数据进行修改操作
    def sql_update(self):
        # update userinfo set name='jason',password='234' where id = 1


"""

(******)
hasattr
getattr
setattr

"""

# 元类拦截类的创建过程  使它具备表的特性
class OrmMetaClass(type):
    def __new__(cls, class_name, class_base, class_attr):
        # 只拦截模型表的创建表
        if class_name == 'Models':
            return type.__new__(cls, class_name, calss_base, class_attr)
        
        table_name = class_attr.get('table_name', class_name)
        primary_key = None
        mappings = {}
        for k,v in class_attr.items():
            if isinstance(v, Field):
                mappings[k] = v
                if v.primary:
                    if primary_key:
                        raise TypeError('主键重复')
                    primary_key = v.name
        for k in mappings.keys():
            class_attrs.pop(k)
        if not primary_key:
            raise TypeError('必须要有一个主键')
        class_attr['table_name'] = table_name
        class_attr['primary_key'] = primary_key
        class_attr['mappings'] = mappings
        return type.__new__(cls, class_name, calss_base, class_attr)

4. 数据库

#Models

- 用户表: User
        - id
        - name
        - pwd
        - register_time 注册时间
        - is_vip 是否是VIP  0/1
        - is_locked 是否被锁定 0/1
        - user_type 管理员用户/普通用户

- 电影表  Movie
        - id
        - m_name
        - is_free  免费/收费 0/1
        - is_delete  电影是否被删除
        - file_md5  校验电影文件的唯一性
        - path  电影的存放目录
        - upload_time  电影上传时间
        - user_id

- 公告表  Notice
        - id
        - title
        - content
        - create_time
        - user_id

- 下载记录表 DownloadRecord
        - id
        - user_id
        - movie_id
        - download_time


#利用orm将数据库操作转为代码操作
from orm_demo.orm_part import Models, IntegerField, StringField

class User(Models):
    # 自定义表名
    table_name = 'user_info'  # {'table_name': 'user_info'}
    user_id = IntegerField(name='user_id', primary_key=True)
    user_name = StringField(name='user_name')
    pwd = StringField(name='pwd')
    is_vip = IntegerField(name='is_vip')
    is_locked = IntegerField(name='is_locked')
    user_type = StringField(name='user_type')
    register_time = StringField(name='register_time')


# 电影表
class Movie(Models):
    table_name = 'movie'
    movie_id = IntegerField(name='movie_id', primary_key=True)
    movie_name = StringField(name='movie_name')
    is_free = IntegerField(name='is_free')
    is_delete = IntegerField(name='is_delete')
    file_md5 = StringField(name='file_md5')
    path = StringField(name='path')
    upload_time = StringField(name='upload_time')
    user_id = IntegerField(name='user_id')


# 公告表
class Notice(Models):
    table_name = 'notice'
    n_id = IntegerField(name='n_id', primary_key=True)
    title = StringField(name='title')
    content = StringField(name='content')
    create_time = StringField(name='create_time')
    user_id = IntegerField(name='user_id')


# 下载记录表
class DownloadRecord(Models):
    table_name = 'download_record'
    download_id = IntegerField(name='download_id', primary_key=True)
    user_id = IntegerField(name='user_id')
    movie_id = IntegerField(name='movie_id')
    download_time = StringField(name='download_time')

5. 各个功能模块

管理员:


>>>注册功能

    客户端
        1-1、选择每个功能之前都需要都需要需要连接上服务器,即需要一个socket对象,每个函数传一个client
        1-2、密码在传递过程中不能是明文吧,需要加密,可选择hashlib中md5加密,定义一个公共方法咯
        1-3、定义一个发送和接收的公共方法、这里要注意的是在这个方法有一个关键字形参、用于传输文件,默认为None
        1-4、考虑一个问题,发送的字典中要包含哪些数据、对于注册这个问题,包含服务器端用于标识的type的功能类型、
             用户名、密码(要加密)、还有用户类型"user_type"(admin或者user)这里是admin类型
        1-5、接收得到的字典back_dic又包含那些数据,常见的就flag和msg,后续的功能中有返回列表类型的
        
        
    服务端
        1-6、首先就是基于多线程实现并发的套接字程序,子线程working函数中会先接收到客户端发来的字典(用到json、struct模块)
        
        1-7、有个问题是有什么便利的方法将接收到的字典recv_dic 和与客户端建立连接的socket对象conn 交给接口层中相应的功能进
             行操作数据库,那就定义一个分发函数dispatch(recv_dic,conn),然后判断recv_dic[’type‘]类型和全局func_dic字典中进行
             比对,去执行与之对应的函数,如果传过来的类型不存在func_dic字典中,那就自定义一个字典back_dic(包含flag和msg数据)
             调用服务端公共发送数据方法返回给客户端
             
        1-8、咱们不知不觉就来到了服务端注册接口了,意味着可以操作数据库啦,就需要用到ORM框架和db目录中models模块中与表一一对应
             的类、这四个类都是根据事先在数据库中定义好的字段进行创建的,不要写错了,字段和类型。这四个类都继承了ORM框架的基类
             modle,所以可是直接点就可以调用ORM框架中基类中方法,select方法是类方法,得到的是一个列表套对象,还有save方法,用于保存
             ,还有一个update方法用于更新,那咱们回过头来
             
        1-9、注册功能拿到的recv_dic中可以拿到注册的用户名,得到用户名后使用user_data = models.User.select(name=name)进行判断要注册的用户是否存在,若存在老规矩back_dic(flag为False,msg为注册失败)返回去。
        - 不存在那咋整?
        	保存到数据库user表中呗。
        - 那怎么保存?
        	name,password,user_type,is_locked和is_vip都有默认值,register_time注册时间的话写个方法 time.strftime("%Y-%m-%d %X")这样不就全搞定了,什么数据都拿到了,那就用models.User()把这些数据搞进去创建得到一个对象,对象调用save方法进行方法就ojbk了,不急还有要记得通知客户端,老规矩back_dic字典,调用公共发送方法,注册大功告成。
        	
>>>登录

    客户端
        2-1、在注册功能该项目的总体框架都已经打通了任督二脉,我的乖乖,那登录功能需要考虑一个问题,客户端如果登陆成功,是不是需要标记一下登陆状态,老规矩在全局定义一个字典,把返回的字典中一个session存到全局字典cookie中,解决了
        
        2-2、发送字典send_dic中type类型修改为login,密码的话照样发送密文,然后over了
    
    服务器
        2-3、还记得tcpserver模块中的全局func_dic字典吗?强大的地方来了,刚刚只是写了一个注册的映射接口,现在来了一个login类型,那咋整,就往里加一个login的映射方法,还可以直接拿到recv_dic和conn,任督二脉打通了就是强,哦还有注册和登录都是管理员和普通用户的公共方法,所以放到common_interface中,其实放哪都一样只要能找到就行啦 哈哈
        
        2-4、你要登陆,逻辑点在哪里,首先我要判断你这货存不存在呀,不存在登陆个屁呀,淡定淡定,哈哈,上面说过select方法得到的是列表,别给老子忘了,列表里面放的是一个个对象,models中User类调用select方法根据name=recv_dic["name"]得到user_list,如果user_list存在,那就取零号位就拿到user_obj用户对象
        
        2-5、拿到user_obj对象点表中的字段属性判断其类型和接收的recv_dic字典中类型和密码是否一致,一致的话便可以得到一个back_dic字典了,老规矩包含flag和msg
        
        2-6、重点来了,这里可能有带你绕,请无关人员速速离开,要返回的back_dic字典中需要添加一个session添加到字典中,这个session是用户登陆成功之后生成的一个随机字符串,咱这里也是用hashlib,这里要保证生成的字符串是唯一的,这里需要加盐,加一个当前cpu执行代码的时间 time.clock()
        
        2-7、服务端怎么校验用户的登陆问题,考虑两个问题,第一个问题服务端需要保存session,第二个问题当用户退出之后将该用户对应的键值删除?
那我们如何判断用户走了,运行到哪一段代码就标记用户走了呢,我们可不可以通过addr就可以定位哪一个用户断开了,找到当前用对应的数据删除,数据保存形式{‘addr’:[session,user_id]}  将这个东西存在哪里呢,可以放在全局,但我们这里把他存到Tcpsever目录下user_data模块live_user['addr’']=[session,user_id]。
            那问题又来了,怎么拿到add?第一种思路给每一个函数都添加addr参数,但是这个addr参数只是login函数用到,其他函数都没用到,这样第一种思路很不合理,第二种思路可以通过working中接收到的recv_dic字典添加recv_dic["addr"] = str(addr) 再传给每一个函数,在login函数中user_data.live_user[recv_dic["addr"]] = [session,user_obj.id]。
            考虑一个问题,因为多线程要操作公共数据user_data中的live_user字典,就会出现数据错乱,所以要加锁,那这个锁在那里产生呢?我们要在tcpsever全局中产生mutex = Lock()。在这里产生,但是不能在这里用,因为会出现循环导入问题,tcpserver导入common_interface,在common_interface中又用到tcpserver中的锁,相互导入就出现循环导入,解决办法,将锁保存到user_data中  user_data.mutex = mutex,在login中给user_data.live_user[recv_dic["addr"]] = [session,user_obj.id]加锁,直接导入user_data就可以使用到锁啦!还没完在tcpserver中 用户退出(try...except.(下面的执行的代码就表示其中一个线程断开)..)就要删除user_data.live_user.pop(str(addr))  ,这里也是公共方法需要
加锁user_data.mutex.acquire()和user_data.mutex.release()
        
        2-8、下面的功能都需要先登录才能操作,这里来个装饰器功能:校验客户端发过来的随机字符串,如果有这个随机字符串那就正常执行函数,如果没有返回请先登录的提示,意味着客户端发送的字典要带着session过来,装饰器inner(*args,**kwargs)中args=(recv_dic,conn) kwargs={}  拿到客户端发过来的随机字符串与服务器的数据进行比对  values=[session,user_id]  
        for vlues in user_data.live_user.vlues(): 
        if args[0].get("session") == v[0]:
             将对应的user_id放入recv_dic中,以便后续使用
             args[0]["user_id"]=vlues[1] 
             break
            以上for循环不一定能找到,for循环只是单单的判断session,然后将user_id放到接收字典recv_dic中,那被装饰的函数到底执不执行,
            if args[0].get("user_id"): func(*args,**kwargs)
            else: back_dic ={"flag"False,"msg":"请先登录"} 然后调用返回函数send_back(back_dic,args[1])
            
            
>>>上传视频

    客户端
        3-1、查看有哪些影片需要上传的,即获取所有视频
        3-2、判断影片是否存在才能上传,那应该怎么判断是个问题,我们能不能对上传的视频文件进行hashlib,自定义被hash的数据可以在文件开头,1/3,2/3,末尾-10然后得到md5值
            发送字典类型"check_movie",包含"session","file_md5",得到字典back_dic,如果视频不存在那要输入is_free,是否免费,然后在发字典send_dic,该字典类型为"upload_movie",还包含
            "session"、"file_name"、 "file_size"、"file_md5",这里调用公共收发方法是要给文件file传参了,把上传文件路径传过去
     服务端
        3-3、还记得tcpserver模块中的全局func_dic字典吗?加上"check_movie"和"upload_movie"映射,映射函数全都加上装饰器
        3-4、"check_movie"比较简单,只是查看要上传视频的file_md5是否在数据库,注意数据库中存的只是文件地址而已,不是真实的视频文件
        3-5、这里为了避免上传的视频名字是一样的但是内容不一样,所以文件名应该尽量取的唯一,所以给传来的file_name加上一个随机字符串,就直接调用之前定义的 get_session方法即可
        3-6、这里要拼接文件的存放路径了,根据file_size循环写入文件
        3-7、生成一个 movie_obj 电影对象,调用save方法保存,然后返回back_dic说明上传成功


>>>删除视频

    客户端
        4-1、先查询出所有没有被删除的电影列表,即send_dic字典中"type"为'get_movie_list' 和'movie_type'为"all",返回的电影列表可以全部是收费,全部是免费,收费免费都有,这里需要注意的是获取所有视频列表考
             虑的不周全,如果单从管理员角度要获得所有视频不考虑用户获取收费或者免费的视频,会出现一些代码冗余,所以在获取所有视频这个功能要判断传过来的的movie_type是all、free、charge
        4-2、拿到所有视频列表movie_list,该列表的格式[电影名称,是否免费收费,电影id]发送字典send_dic中"type"为"delete_movie"和'delete_movie_id'为movie_list[choice-1][2]
        
    服务端
        4-3、还记得tcpserver模块中的全局func_dic字典吗?加上'get_movie_list'和"delete_movie"映射,映射函数全都加上装饰器
        4-4、删除电影不是真的删除,只是找到每一个电影对象,然后点is_delete属性改为1即可,所以get_movie_list方法会先获得所有对象列表,遍历列表得到每一个对象,对每一个对象的is_delete属性进行判断,注意还要判断
            ecv_dic['movie_type'],这里是“all”类型,满足的全部添加到一个返回的列表中back_movie_list,然后返回给客户端
        4-5、delete_movie方法的话 movie_list = models.Movie.select(id=recv_dic.get("delete_movie_id"))然后对列表去索引得到一个电影对象,然后修改movie_obj.is_delete,然后调用update()方法更新,然后返回back_cic


>>>发布公告
    
    客户端
        5-1 公告包含title和content  发送的字典send_dic包含"type"为"release_notice"、"session"、"title"、"content"
    
    服务端、
        5-2、这里需要知道接受的字典recv_dic是包含user_id字段的,要写入表notice时用到
        5-3、也是创建表notice对象,然后调用save方法保存
               

普通用户:

>>>注册

    直接调用公共注册方法
    
    
>>>登录

    直接调用公共登录,在全局添加user_dic中保存session和is_vip


>>>购买会员
    
    客户端
        3-1、判断全局user_dic['is_vip']可知道是否是会员
        3-2、如果不是的话,让用户选择是否购买会员,购买的话最后要修改全局
    
    服务端
        3-3、根据recv_dic["user_id"]判断是哪一个用户要购买会员,得到的对象点is_vip属性修改为1,调用update(0方法保存
                                                                           
                                                                           
>>>查看电影
                                                                           
    客户端
        4-1、发送字典send_dic里面的type为'get_movie_list','movie_type'为'all'
                                                                           
    服务器
        4-2、直接调用之前写好的get_movie_list方法即可  这和管理员中删除视频就先获取所有视频
                                                                           
>>>下载免费电影
                                                                           
    客户端
        5-1、先列出所有免费电影,和上个功能差不多,只是'movie_type'改为'free'
        5-2、再发送字典send_dic中'type'为'download_movie' 'movie_id'为movie_list[choice-1][2]
        5-3、接受得到的字典back_dic中有一个wait_time 打印可能是0或者30秒  拼接下载的路径,循环写入文件
                                                                           
    服务端
        5-4、id=recv_dic.get('movie_id')来得到电影列表movie_list,然后索引取值得到电影对象
        5-5  id=recv_dic['user_id']来得到用户列表索引取得用户对象user_obj
        5-6、下载电影的话先判断使用是否是vip,vip的话不需要等待30秒 不是的话需要等待30秒
        5-7、更新下载记录到down_record表中
        5-8、循环发送文件
        5-9、发送字典back_dic
                                                                           
                                                                           
>>>下载收费电影
                                                                           
    客户端
        6-1、针对普通用户和vip用户下载收费视频收费标准不一样(5元 10元)
        6-2、发送字典send_dic 中还是'get_movie_list'但是电影类型为收费'movie_type':'charge'
        6-3、剩下功能和下载免费电影差不多
                                                                           
    服务器
        同上
                                                                           
                                                                           
>>>查看电影下载记录
                                                                           
    客户端
        7-1、发送字典send_dic 中的类型'check_download_record'
        7-2、接受字典back_dic进行判断即可
                                                                           
    服务端
        7-3、还记得tcpserver模块中的全局func_dic字典吗?加上'check_download_record'的映射方法
        7-4、要查看下载记录 先根据用户id得到一个记录列表,循环该列表得到的是每一个记录对象
        7-5、根据每一个对象点movie_id 和电影id判断得到电影列表,索引取值得到各个对象
        7-6、把每一个对象的名字添加到一个自定义的列表中,用于返回给客户端
                                                                           
>>>查看公告
                                                                           
    客户端
        8-1、发送字典send_dic 中的类型'check_notice'
        8-2、接受字典back_dic进行判断即可
                                                                           
    服务端
        8-3、还记得tcpserver模块中的全局func_dic字典吗?加上'check_notice'的映射方法
        8-4、Notice类调用select方法得到公告列表
        8-5、列表存在的话 遍历该列表得到每一个对象,返回字典中保存对象点title,点content进行返回            

6. 项目中遇到的问题及怎么解决的

1、校验登陆问题(服务端必须校验,客户端无所谓)
2、获取所有视频列表考虑的不周全,如果单从管理员角度要获得所有视频不考虑用户获取收费或者免费
    的视频,会出现一些代码冗余,所以在获取所有视频这个功能要判断传过来的的movie_type是all、
    free、charge
3、服务端怎样标识客户端问题:cookie保存到客户端、session保存到服务器user_data文件中
4、从客户端到数据库一顿操作打通以后遇到最多的问题 有字段打错了、
原文地址:https://www.cnblogs.com/fxyadela/p/11644592.html