本博文将一步步带领你实现抽屉官网的各种功能:包括登陆注册发送邮箱验证码登陆验证码页面登陆验证发布文章上传图片form验证点赞、评论、文章分页处理以及基于tornado的后端和ajax的前端数据处理。

转载请注明出处http://www.cnblogs.com/wanghzh/p/5806514.html

抽屉官网:http://dig.chouti.com/

一、配置(settings)

settings = {'template_path': 'views',    #模板文件路径'static_path': 'statics',        #静态文件路径'static_url_prefix': '/statics/',  #静态文件前缀'autoreload': True,'ui_methods': mt
}

二、路由配置

application = tornado.web.Application([(r"/index", home.IndexHandler),    #主页(r"/check_code", account.CheckCodeHandler),  #验证码(r"/send_msg", account.SendMsgHandler),  #邮箱验证码(r"/register", account.RegisterHandler),  #注册(r"/login", account.LoginHandler),    #登陆(r"/upload_image", home.UploadImageHandler),  #上传图片(r"/comment", home.CommentHandler),  #评论(r"/favor", home.FavorHandler),   #点赞
], **settings)

三、文件夹分类

下面我们将根据上图文件目录由上到下做一一分析:

四、准备工作

  本项目所有前端反馈均是通过BaseResponse类实现的:

class BaseResponse:def __init__(self):self.status = False    #状态信息,是否注册成功,是否登陆成功,是否点赞成功、是否评论成功等self.code = StatusCodeEnum.Successself.data = None        #前端需要展示的数据self.summary = None    #错误信息self.message = {}    #字典类型的错误信息

本文参考了大量前端和后端基础知识,从入门到精通的链接如下:

前端:

  • html
  • css基础(一)
  • css基础(二)
  • JavaScript基础
  • Dom基础和实例
  • jquary基础和实例大全
  • 偷偷发请求的ajax基础与实例大全

后端:

  • web框架本质
  • web框架之tronado

数据库:

  • mysql基础一
  • mysql基础二 
  • python操作mysql(pymysql 和ORM框架 SQLAchemy)

缓存:

  • RabbitMQ、Redis、Memcache、SQLAlchemy

      

五、core:业务处理类handler需要继承的父类

import tornado.web
from backend.session.session import SessionFactoryclass BaseRequestHandler(tornado.web.RequestHandler):def initialize(self):self.session = SessionFactory.get_session_obj(self)

六、form:用于form验证的文件,这是一个自定义的tornado form验证模块

  fields:包含字符串、邮箱、数字、checkbox、文件类型验证

  forms:核心验证处理,返回验证是否成功self._valid_status、成功后的数据提self._value_dict、错误信息self._error_dict

classField:def __init__(self):self.is_valid=Falseself.name=Noneself.value=Noneself.error=Nonedefmatch(self, name, value):self.name=nameif notself.required:self.is_valid=Trueself.value=valueelse:if notvalue:if self.custom_error_dict.get('required', None):self.error= self.custom_error_dict['required']else:self.error= "%s is required" %nameelse:ret=re.match(self.REGULAR, value)ifret:self.is_valid=Trueself.value=valueelse:if self.custom_error_dict.get('valid', None):self.error= self.custom_error_dict['valid']else:self.error= "%s is invalid" % name

初始化

classStringField(Field):REGULAR= "^.*$"def __init__(self, custom_error_dict=None, required=True):self.custom_error_dict= {}  #{'required': 'IP不能为空', 'valid': 'IP格式错误'}ifcustom_error_dict:self.custom_error_dict.update(custom_error_dict)self.required=requiredsuper(StringField, self).__init__()

字符串匹配

classIPField(Field):REGULAR= "^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$"def __init__(self, custom_error_dict=None, required=True):self.custom_error_dict= {}  #{'required': 'IP不能为空', 'valid': 'IP格式错误'}ifcustom_error_dict:self.custom_error_dict.update(custom_error_dict)self.required=requiredsuper(IPField, self).__init__()

ip匹配

classEmailField(Field):REGULAR= "^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$"def __init__(self, custom_error_dict=None, required=True):self.custom_error_dict= {}  #{'required': 'IP不能为空', 'valid': 'IP格式错误'}ifcustom_error_dict:self.custom_error_dict.update(custom_error_dict)self.required=requiredsuper(EmailField, self).__init__()

邮箱匹配

classIntegerField(Field):REGULAR= "^\d+$"def __init__(self, custom_error_dict=None, required=True):self.custom_error_dict= {}  #{'required': 'IP不能为空', 'valid': 'IP格式错误'}ifcustom_error_dict:self.custom_error_dict.update(custom_error_dict)self.required=requiredsuper(IntegerField, self).__init__()

数字匹配

classCheckBoxField(Field):REGULAR= "^\d+$"def __init__(self, custom_error_dict=None, required=True):self.custom_error_dict= {}  #{'required': 'IP不能为空', 'valid': 'IP格式错误'}ifcustom_error_dict:self.custom_error_dict.update(custom_error_dict)self.required=requiredsuper(CheckBoxField, self).__init__()defmatch(self, name, value):self.name=nameif notself.required:self.is_valid=Trueself.value=valueelse:if notvalue:if self.custom_error_dict.get('required', None):self.error= self.custom_error_dict['required']else:self.error= "%s is required" %nameelse:ifisinstance(name, list):self.is_valid=Trueself.value=valueelse:if self.custom_error_dict.get('valid', None):self.error= self.custom_error_dict['valid']else:self.error= "%s is invalid" % name

checkbox匹配

classFileField(Field):REGULAR= "^(\w+\.pdf)|(\w+\.mp3)|(\w+\.py)$"def __init__(self, custom_error_dict=None, required=True):self.custom_error_dict= {}  #{'required': 'IP不能为空', 'valid': 'IP格式错误'}ifcustom_error_dict:self.custom_error_dict.update(custom_error_dict)self.required=requiredsuper(FileField, self).__init__()defmatch(self, name, file_name_list):flag=Trueself.name=nameif notself.required:self.is_valid=Trueself.value=file_name_listelse:if notfile_name_list:if self.custom_error_dict.get('required', None):self.error= self.custom_error_dict['required']else:self.error= "%s is required" %nameflag=Falseelse:for file_name infile_name_list:if not file_name or notfile_name.strip():if self.custom_error_dict.get('required', None):self.error= self.custom_error_dict['required']else:self.error= "%s is required" %nameflag=Falsebreakelse:ret=re.match(self.REGULAR, file_name)if notret:if self.custom_error_dict.get('valid', None):self.error= self.custom_error_dict['valid']else:self.error= "%s is invalid" %nameflag=Falsebreakself.is_valid=flagdef save(self, request, upload_to=""):file_metas=request.files[self.name]for meta infile_metas:file_name= meta['filename']file_path_name=os.path.join(upload_to, file_name)with open(file_path_name,'wb') as up:up.write(meta['body'])upload_file_path_list= map(lambdapath: os.path.join(upload_to, path), self.value)self.value= list(upload_file_path_list)

文件匹配

核心验证处理:

from backend.form import fieldsclass BaseForm:def __init__(self):self._value_dict = {} #数据字典self._error_dict = {} #错误信息字典self._valid_status = True #是否验证成功def valid(self, handler):for field_name, field_obj in self.__dict__.items():if field_name.startswith('_'):  #过滤私有字段continueif type(field_obj) == fields.CheckBoxField:  #checkbox处理post_value = handler.get_arguments(field_name, None) elif type(field_obj) == fields.FileField:  #文件处理post_value = []file_list = handler.request.files.get(field_name, [])for file_item in file_list:post_value.append(file_item['filename'])else:post_value = handler.get_argument(field_name, None)field_obj.match(field_name, post_value)  #匹配if field_obj.is_valid:                    #如果验证成功self._value_dict[field_name] = field_obj.value  #提取数据else:self._error_dict[field_name] = field_obj.error  #错误信息self._valid_status = Falsereturn self._valid_status    #返回是否验证成功

七、如何应用上述form验证模块:

以注册为例:

前端:

<divclass="header"><span>注册</span><divclass="dialog-close"onclick="CloseDialog('#accountDialog');">X</div></div><divclass="content"><divstyle="padding: 0 70px"><divclass="tips"><span>输入注册信息</span></div><divid="register_error_summary"class="error-msg"></div><divclass="inp"><inputname="username"type="text"placeholder="请输入用户名" /></div><divclass="inp"><inputname="email"id="email"type="text"placeholder="请输入邮箱" /></div><divclass="inp"><inputname="email_code"class="email-code"type="text"placeholder="请输入邮箱验证码" /><aid="fetch_code"class="fetch-code"href="javascript:void(0);">获取验证码</a></div><divclass="inp"><inputname="password"type="password"placeholder="请输入密码" /></div><divclass="inp"><divclass="submit"onclick="SubmitRegister(this);"><span>注册</span><spanclass="hide"><imgsrc="/statics/images/loader.gif"style="height: 16px;width: 16px"><span>正在注册</span></span></div></div></div></div>

html

js:
/*点击注册按钮*/function SubmitRegister(ths){$('#register_error_summary').empty();$('#model_register .inp .error').remove();$(ths).children(':eq(0)').addClass('hide');$(ths).addClass('not-allow').children(':eq(1)').removeClass('hide');var post_dict = {};$('#model_register input').each(function(){post_dict[$(this).attr("name")] = $(this).val();  #将所有input标签内容提取出来,以对应name为key,值为value放入post_dict字典});$.ajax({url: '/register', #提交的urltype: 'POST',    #提交方式data: post_dict,  #提交数据dataType: 'json',  #数据格式success: function(arg){if(arg.status){window.location.href = '/index'; #验证成功跳转至主页}else{$.each(arg.message, function(k,v){//<span class="error">s</span>var tag = document.createElement('span'); #验证失败创建标签tag.className = 'error'; tag.innerText = v; #存入错误信息$('#model_register input[name="'+ k +'"]').after(tag); #将标签加入html中})}}});$(ths).removeClass('not-allow').children(':eq(1)').addClass('hide');$(ths).children(':eq(0)').removeClass('hide');}

 后台处理:

首先需要编写RegisterForm:

class RegisterForm(BaseForm):  #需要继承上面的form验证核心处理类def __init__(self): #初始化每一个input标签的nameself.username = StringField()  #input标签name=对应类型的类self.email = EmailField()self.password = StringField()self.email_code = StringField()super(RegisterForm, self).__init__()

后台RegisterHandler:

class RegisterHandler(BaseRequestHandler):def post(self, *args, **kwargs):rep = BaseResponse()   #总的返回前端的类,包含是否注册成功的状态、错误信息form = account.RegisterForm()  #实例化RegisterFormif form.valid(self):  #调用baseform核心验证处理函数valid,返回是否验证成功current_date = datetime.datetime.now()limit_day = current_date - datetime.timedelta(minutes=1)conn = ORM.session()  #获取数据库session对象        #查看验证码是否过期is_valid_code = conn.query(ORM.SendMsg).filter(ORM.SendMsg.email == form._value_dict['email'],ORM.SendMsg.code == form._value_dict['email_code'],ORM.SendMsg.ctime > limit_day).count()if not is_valid_code:rep.message['email_code'] = '邮箱验证码不正确或过期'self.write(json.dumps(rep.__dict__))returnhas_exists_email = conn.query(ORM.UserInfo).filter(ORM.UserInfo.email == form._value_dict['email']).count()#邮箱是否存在if has_exists_email:rep.message['email'] = '邮箱已经存在'self.write(json.dumps(rep.__dict__))returnhas_exists_username = conn.query(ORM.UserInfo).filter(ORM.UserInfo.username == form._value_dict['username']).count() #用户名是否存在if has_exists_username:rep.message['email'] = '用户名已经存在'self.write(json.dumps(rep.__dict__))return        #按数据库表的列订制form._value_dict
       form._value_dict['ctime'] = current_date form._value_dict.pop('email_code') obj = ORM.UserInfo(**form._value_dict) conn.add(obj)conn.flush()conn.refresh(obj) #将自增id也提取出来user_info_dict = {'nid': obj.nid, 'email': obj.email, 'username': obj.username}conn.query(ORM.SendMsg).filter_by(email=form._value_dict['email']).delete()#删除本次邮箱验证码conn.commit()conn.close()self.session['is_login'] = True  #注册成功后定义登陆成功self.session['user_info'] = user_info_dict 用户信息写入sessionrep.status = Trueelse:rep.message = form._error_dict #错误信息self.write(json.dumps(rep.__dict__))  #返回给前端,前端ajax success接收并处理,在前端页面展示

八、session ,本session是基于tornado的自定义session  

1.应用工厂方法模式定义session保存的位置,用户只需在配置文件修改即可

class SessionFactory:@staticmethoddef get_session_obj(handler):obj = Noneif config.SESSION_TYPE == "cache": #缓存obj = CacheSession(handler)elif config.SESSION_TYPE == "memcached": #memcached
      obj = MemcachedSession(handler)     elif config.SESSION_TYPE == "redis": #radis      obj = RedisSession(handler)     return obj

2.缓存session

classCacheSession:session_container={}session_id= "__sessionId__"def __init__(self, handler):self.handler=handlerclient_random_str=handler.get_cookie(CacheSession.session_id, None)if client_random_str and client_random_str inCacheSession.session_container:self.random_str=client_random_strelse:self.random_str=create_session_id()CacheSession.session_container[self.random_str]={}expires_time= time.time() +config.SESSION_EXPIREShandler.set_cookie(CacheSession.session_id, self.random_str, expires=expires_time)def __getitem__(self, key):ret=CacheSession.session_container[self.random_str].get(key, None)returnretdef __setitem__(self, key, value):CacheSession.session_container[self.random_str][key]=valuedef __delitem__(self, key):if key inCacheSession.session_container[self.random_str]:del CacheSession.session_container[self.random_str][key]

缓存session

3.memcache session

importmemcacheconn= memcache.Client(['192.168.11.119:12000'], debug=True, cache_cas=True)classMemcachedSession:session_id= "__sessionId__"def __init__(self, handler):self.handler=handler#从客户端获取随机字符串client_random_str =handler.get_cookie(CacheSession.session_id, None)#如果从客户端获取到了随机字符串#        if client_random_str andconn.get(client_random_str):self.random_str=client_random_strelse:self.random_str=create_session_id()conn.set(self.random_str, json.dumps({}), config.SESSION_EXPIRES)#CacheSession.session_container[self.random_str] = {}
conn.set(self.random_str, conn.get(self.random_str), config.SESSION_EXPIRES)expires_time= time.time() +config.SESSION_EXPIREShandler.set_cookie(MemcachedSession.session_id, self.random_str, expires=expires_time)def __getitem__(self, key):#ret = CacheSession.session_container[self.random_str].get(key, None)ret =conn.get(self.random_str)ret_dict=json.loads(ret)result=ret_dict.get(key,None)returnresultdef __setitem__(self, key, value):ret=conn.get(self.random_str)ret_dict=json.loads(ret)ret_dict[key]=valueconn.set(self.random_str, json.dumps(ret_dict), config.SESSION_EXPIRES)#CacheSession.session_container[self.random_str][key] = valuedef __delitem__(self, key):ret=conn.get(self.random_str)ret_dict=json.loads(ret)delret_dict[key]conn.set(self.random_str, json.dumps(ret_dict), config.SESSION_EXPIRES)

memcache session

4.radis session

importredispool= redis.ConnectionPool(host='192.168.11.119', port=6379)
r= redis.Redis(connection_pool=pool)classRedisSession:session_id= "__sessionId__"def __init__(self, handler):self.handler=handler#从客户端获取随机字符串client_random_str =handler.get_cookie(CacheSession.session_id, None)#如果从客户端获取到了随机字符串if client_random_str andr.exists(client_random_str):self.random_str=client_random_strelse:self.random_str=create_session_id()r.hset(self.random_str,None,None)#conn.set(self.random_str, json.dumps({}), config.SESSION_EXPIRES)#CacheSession.session_container[self.random_str] = {}
r.expire(self.random_str, config.SESSION_EXPIRES)#conn.set(self.random_str, conn.get(self.random_str), config.SESSION_EXPIRES)
expires_time= time.time() +config.SESSION_EXPIREShandler.set_cookie(RedisSession.session_id, self.random_str, expires=expires_time)def __getitem__(self, key):#ret = CacheSession.session_container[self.random_str].get(key, None)result =r.hget(self.random_str,key)ifresult:ret_str= str(result, encoding='utf-8')try:result=json.loads(ret_str)except:result=ret_strreturnresultelse:returnresultdef __setitem__(self, key, value):if type(value) ==dict:r.hset(self.random_str, key, json.dumps(value))else:r.hset(self.random_str, key, value)#CacheSession.session_container[self.random_str][key] = valuedef __delitem__(self, key):r.hdel(self.random_str,key)

radis session

九、验证码:

注:验证码需要依赖session。

#!/usr/bin/env python#coding:utf-8importrandomfrom PIL importImage, ImageDraw, ImageFont, ImageFilter_letter_cases= "abcdefghjkmnpqrstuvwxy"  #小写字母,去除可能干扰的i,l,o,z
_upper_cases = _letter_cases.upper()  #大写字母
_numbers = ''.join(map(str, range(3, 10)))  #数字
init_chars = ''.join((_letter_cases, _upper_cases, _numbers))def create_validate_code(size=(120, 30),chars=init_chars,img_type="GIF",mode="RGB",bg_color=(255, 255, 255),fg_color=(0, 0, 255),font_size=18,font_type="Monaco.ttf",length=4,draw_lines=True,n_line=(1, 2),draw_points=True,point_chance= 2):'''@todo: 生成验证码图片@param size: 图片的大小,格式(宽,高),默认为(120, 30)@param chars: 允许的字符集合,格式字符串@param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG@param mode: 图片模式,默认为RGB@param bg_color: 背景颜色,默认为白色@param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF@param font_size: 验证码字体大小@param font_type: 验证码字体,默认为 ae_AlArabiya.ttf@param length: 验证码字符个数@param draw_lines: 是否划干扰线@param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效@param draw_points: 是否画干扰点@param point_chance: 干扰点出现的概率,大小范围[0, 100]@return: [0]: PIL Image实例@return: [1]: 验证码图片中的字符串'''width, height= size #宽, 高img = Image.new(mode, size, bg_color) #创建图形draw = ImageDraw.Draw(img) #创建画笔defget_chars():'''生成给定长度的字符串,返回列表格式'''returnrandom.sample(chars, length)defcreate_lines():'''绘制干扰线'''line_num= random.randint(*n_line) #干扰线条数for i inrange(line_num):#起始点begin = (random.randint(0, size[0]), random.randint(0, size[1]))#结束点end = (random.randint(0, size[0]), random.randint(0, size[1]))draw.line([begin, end], fill=(0, 0, 0))defcreate_points():'''绘制干扰点'''chance= min(100, max(0, int(point_chance))) #大小限制在[0, 100]for w inrange(width):for h inrange(height):tmp= random.randint(0, 100)if tmp > 100 -chance:draw.point((w, h), fill=(0, 0, 0))defcreate_strs():'''绘制验证码字符'''c_chars=get_chars()strs= '%s' % ' '.join(c_chars) #每个字符前后以空格隔开
font=ImageFont.truetype(font_type, font_size)font_width, font_height=font.getsize(strs)draw.text(((width- font_width) / 3, (height - font_height) / 3),strs, font=font, fill=fg_color)return ''.join(c_chars)ifdraw_lines:create_lines()ifdraw_points:create_points()strs=create_strs()#图形扭曲参数params = [1 - float(random.randint(1, 2)) / 100,0,0,0,1 - float(random.randint(1, 10)) / 100,float(random.randint(1, 2)) / 500,0.001,float(random.randint(1, 2)) / 500]img= img.transform(size, Image.PERSPECTIVE, params) #创建扭曲
img= img.filter(ImageFilter.EDGE_ENHANCE_MORE) #滤镜,边界加强(阈值更大)return img, strs

check_code.py

class CheckCodeHandler(BaseRequestHandler):def get(self, *args, **kwargs):stream = io.BytesIO()img, code = check_code.create_validate_code()img.save(stream, "png")self.session["CheckCode"] = code  #利用session保存验证码self.write(stream.getvalue())

  路由配置:(r"/check_code", account.CheckCodeHandler),

  前端:

<img class="check-img" src="/check_code" alt="验证码" onclick="ChangeCode(this);">

  js:

<script>function ChangeCode(ths) {ths.src += '?';}</script>

十、发送邮箱验证码

前端:

<div class="inp"><input class="regiter-temp" name="code" class="email-code" type="text" placeholder="请输入邮箱验证码" /><a onclick="SendCode(this);" class="fetch-code" >获取验证码</a>
</div>

js:

function SendCode(ths) {
//            var email = $(ths).prev().val();var email = $('#email').val();$.ajax({url: '/send_code',type: 'POST',data: {em: email},success: function (arg) {console.log(arg);},error: function () {}});}

路由配置:

(r"/send_code", account.SendCodeHandler),

后台handler:

class SendCodeHandler(BaseRequestHandler):def post(self, *args, **kwargs):ret = {'status': True, "data": "", "error": ""}email = self.get_argument('em', None)if email:code = commons.random_code()  #获取随机验证码message.email([email,], code) #发送验证码到邮箱conn = chouti_orm.session()   #获取数据库session对象obj = chouti_orm.SendCode(email=email,code=code, stime=datetime.datetime.now()) #写入数据库conn.add(obj)conn.commit()else:ret['status'] = Falseret['error'] = "邮箱格式错误"self.write(json.dumps(ret))

 发送邮箱验证码函数:

import smtplib
from email.mime.text import MIMEText
from email.utils import formataddrdef email(email_list, content, subject="抽屉新热榜-用户注册"):  #email_list邮件列表,content邮件内容,subject:发送标题msg = MIMEText(content, 'plain', 'utf-8')msg['From'] = formataddr(["抽屉新热榜",'wptawy@126.com'])msg['Subject'] = subjectserver = smtplib.SMTP("smtp.126.com", 25) 邮箱引擎server.login("wptawy@126.com", "JUEmimima") #邮箱名,密码server.sendmail('wptawy@126.com', email_list, msg.as_string())server.quit()

十一、邮箱验证码之过期时间

案例:

html:

<a id="fetch_code" class="fetch-code" href="javascript:void(0);">获取验证码</a>

js:

function BindSendMsg(){$("#fetch_code").click(function(){$('#register_error_summary').empty();  #清空错误信息var email = $('#email').val(); #获取邮箱地址if(email.trim().length == 0){  #判断是否输入邮箱$('#register_error_summary').text('请输入注册邮箱');return;}if($(this).hasClass('sending')){  #判断是否已经发送return; }var ths = $(this);var time = 60; 设置倒计时时间为60s$.ajax({url: "/send_msg", type: 'POST',data: {email: email},dataType: 'json',success: function(arg){if(!arg.status){  #是否发送成功$('#register_error_summary').text(arg.summary); #不成功显示错误信息}else{ths.addClass('sending');      #成功后显示已发送状态var interval = setInterval(function(){ths.text("已发送(" + time + ")");time -= 1;    #定时器每运行一次,计数器减1if(time <= 0){clearInterval(interval); #一分钟过完,清除定时器ths.removeClass('sending');# 移除已发送状态ths.text("获取验证码");# 恢复未发送状态}}, 1000);#定时器每隔1s运行一次}}});});}

附:一些常见模块:1.随机验证码获取:

defrandom_code():code= ''for i in range(4):current= random.randrange(0,4)if current !=i:temp= chr(random.randint(65,90))else:temp= random.randint(0,9)code+=str(temp)return code

随机验证码

2.md5加密

defgenerate_md5(value):r=str(time.time())obj= hashlib.md5(r.encode('utf-8'))obj.update(value.encode('utf-8'))return obj.hexdigest()

md5加密

十二、分页功能,该功能是基于tornado的自定义分页功能

案例:

前端:

<div class="pagination">{% raw str_page%}  #展示原生html</div>

url配置:

(r"/index/(?P<page>\d*)", IndexHandler),

分页模块:

#!/usr/bin/env python#-*- coding:utf-8 -*-classPagination:def __init__(self, current_page, all_item):try:page=int(current_page)except:page= 1if page < 1:page= 1all_pager, c= divmod(all_item, 10)if c >0:all_pager+= 1self.current_page=pageself.all_pager=all_pager@propertydefstart(self):return (self.current_page - 1) * 10@propertydefend(self):return self.current_page * 10def string_pager(self, base_url="/index/"):list_page=[]if self.all_pager < 11:s= 1t= self.all_pager + 1else:  #总页数大于11if self.current_page < 6:s= 1t= 12else:if (self.current_page + 5) <self.all_pager:s= self.current_page - 5t= self.current_page + 5 + 1else:s= self.all_pager - 11t= self.all_pager + 1#首页#first = '<a href="%s1">首页</a>' % base_url#list_page.append(first)#上一页#当前页 pageif self.current_page == 1:prev= '<a href="javascript:void(0);">上一页</a>'else:prev= '<a href="%s%s">上一页</a>' % (base_url, self.current_page - 1,)list_page.append(prev)for p in range(s, t):  #1-11if p ==self.current_page:temp= '<a class="active" href="%s%s">%s</a>' %(base_url,p, p)else:temp= '<a href="%s%s">%s</a>' %(base_url,p, p)list_page.append(temp)if self.current_page ==self.all_pager:nex= '<a href="javascript:void(0);">下一页</a>'else:nex= '<a href="%s%s">下一页</a>' % (base_url, self.current_page + 1,)list_page.append(nex)#尾页last = '<a href="%s%s">尾页</a>' %(base_url, self.all_pager,)list_page.append(last)#跳转jump = """<input type='text' /><a onclick="Jump('%s',this);">GO</a>""" % ('/index/', )script= """<script>function Jump(baseUrl,ths){var val = ths.previousElementSibling.value;if(val.trim().length>0){location.href = baseUrl + val;}}</script>"""list_page.append(jump)list_page.append(script)str_page= "".join(list_page)return str_page

分页模块

注:Pagination实例化接收两个参数:当前页current_page、新闻总数all_item,其中current_page一般通过url分组元素直接获取

后台handler:

class IndexHandler(BaseRequestHandler):def get(self, page=1):conn = ORM.session()  #获取数据库session对象all_count = conn.query(ORM.News).count()#计算新闻总数obj = Pagination(page, all_count)  #实例化pagination对象current_user_id = self.session['user_info']['nid'] if self.session['is_login'] else 0  #如果登陆获取用户id,否则,用户id=0,下面的查询结果也为空result = conn.query(ORM.News.nid,ORM.News.title,ORM.News.url,ORM.News.content,ORM.News.ctime,ORM.UserInfo.username,ORM.NewsType.caption,ORM.News.favor_count,ORM.News.comment_count,ORM.Favor.nid.label('has_favor')).join(ORM.NewsType, isouter=True).join(ORM.UserInfo, isouter=True).join(ORM.Favor, and_(ORM.Favor.user_info_id == current_user_id, ORM.News.nid == ORM.Favor.news_id), isouter=True)[obj.start:10] #从每页开始向下取10条,即每页显示10条新闻conn.close()str_page = obj.string_pager('/index/') 获取页码的字符串格式htmlself.render('home/index.html', str_page=str_page, news_list=result)

十三、页面登陆验证(装饰器方式实现)

1.普通登陆验证

def auth_login_redirect(func):def inner(self, *args, **kwargs):if not self.session['is_login']:self.redirect(config.LOGIN_URL)returnfunc(self, *args, **kwargs)return inner

2.ajax提交数据的登陆验证

def auth_login_json(func):def inner(self, *args, **kwargs):if not self.session['is_login']:rep = BaseResponse()rep.summary = "auth failed"self.write(json.dumps(rep.__dict__))returnfunc(self, *args, **kwargs)return inner

十四、上传文件

前端:

<form style="display: inline-block" id="upload_img_form" name="form" action="/upload_image" method="POST"  enctype="multipart/form-data" ><a id="fakeFile" class="fake-file"><span>上传图片</span><input type="file" name="img" onchange="UploadImage(this);"/><input type="text" name="url" class="hide" /></a><iframe id='upload_img_iframe' name='upload_img_iframe' src=""  class="hide"></iframe>
</form>

 js:

function UploadImage(ths){document.getElementById('upload_img_iframe').onload = UploadImageComplete;  #页面加载完成后执行UploadImageComplete函数document.getElementById('upload_img_form').target = 'upload_img_iframe';  设置form提交到iframedocument.getElementById('upload_img_form').submit(); #提交到iframe}/*上传图片之后回掉函数*/function UploadImageComplete(){var origin = $("#upload_img_iframe").contents().find("body").text();#获取图片数据var obj = JSON.parse(origin); #转换成JavaScript对象if(obj.status){  #如果上传成功var img = document.createElement('img'); #创建img标签img.src = obj.data; 图片地址img.style.width = "200px";img.style.height = "180px";$("#upload_img_form").append(img);添加图片$('#fakeFile').addClass('hide');$('#reUploadImage').removeClass('hide');$('#fakeFile').find('input[type="text"]').val(obj.data);#保存图片地址到隐藏的input标签中}else{alert(obj.summary);#否则显示错误信息}}

路由配置:

(r"/upload_image", home.UploadImageHandler),

后台handler:

class UploadImageHandler(BaseRequestHandler):@decrator.auth_login_json  #上传前登陆验证def post(self, *args, **kwargs):rep = BaseResponse() 前端回应类try:file_metas = self.request.files["img"] 获取图片列表for meta in file_metas:file_name = meta['filename'] #图片名file_path = os.path.join('statics', 'upload', commons.generate_md5(file_name)) #保存地址with open(file_path, 'wb') as up:up.write(meta['body']) #在服务器写入图片rep.status = True #写入成功rep.data = file_path except Exception as ex:rep.summary = str(ex)#错误信息self.write(json.dumps(rep.__dict__)) #反馈给前端

十五、文章发布

1.定义需要验证的form类

class IndexForm(BaseForm):def __init__(self):self.title = StringField()  #标题self.content = StringField(required=False) 内容self.url = StringField(required=False)  图片urlself.news_type_id = IntegerField()   新闻类型super(IndexForm, self).__init__()

2.前端html:

<div class="f4"><a class="submit right" id="submit_img">提交</a><span class="error-msg right"></span>
</div>

3.js

function BindPublishSubmit(){$('#submit_link,#submit_text,#submit_img').click(function(){ // 获取输入内容并提交 var container = $(this).parent().parent(); var post_dict = {}; container.find('input[type="text"],textarea').each(function(){ post_dict[$(this).attr('name')] =$(this).val(); }); post_dict['news_type_id'] = container.find('.news-type .active').attr('value'); $.ajax({ url: '/index', type: 'POST', data: post_dict, dataType: 'json', success: function (arg) { if(arg.status){ window.location.href = '/index'; }else{ console.log(arg); } } }) }); }

后台handler:

@decrator.auth_login_json  #发布前登陆验证def post(self, *args, **kwargs):rep = BaseResponse()form = IndexForm()#实例化Indexformif form.valid(self):# title,content,href,news_type,user_info_id
#写入数据库input_dict = copy.deepcopy(form._value_dict)input_dict['ctime'] = datetime.datetime.now()input_dict['user_info_id'] = self.session['user_info']['nid']conn = ORM.session()conn.add(ORM.News(**input_dict))conn.commit()conn.close()rep.status = True #写入成功else:rep.message = form._error_dict  #错误信息self.write(json.dumps(rep.__dict__))

十六、点赞功能

前端html:

<a href="javascript:void(0);" class="digg-a" title="推荐" onclick="DoFavor(this,{{item[0]}});">{% if item[9] %}                       #是否已点过赞<span class="hand-icon icon-digg active"></span>{% else %}<span class="hand-icon icon-digg"></span>{% end %}<b id="favor_count_{{item[0]}}">{{item[7]}}</b>    #点赞数量</a>

js:

function DoFavor(ths, nid) {if($('#action_nav').attr('is-login') == 'true'){           #登陆状态才能点赞$.ajax({url: '/favor',type: 'POST',data: {news_id: nid},   #携带当前新闻iddataType: 'json',success: function(arg){if(arg.status){var $favorCount = $('#favor_count_'+nid);var c = parseInt($favorCount.text());#获取当前点赞数量if(arg.code == 2301){      #当前用户以前没点过赞$favorCount.text(c + 1);  #点赞数量加1$(ths).find('span').addClass('active'); 已经点过赞变深颜色AddFavorAnimation(ths); #+1动态效果}else if(arg.code == 2302){ #该用户以前对该新闻点过赞$favorCount.text(c - 1); #点赞数量减1$(ths).find('span').removeClass('active'); 取消点赞颜色变浅MinusFavorAnimation(ths); #-1动态效果}else{}}else{}}})}else{$('#accountDialog').removeClass('hide');$('.shadow').removeClass('hide');}} 

后台handler:

class FavorHandler(BaseRequestHandler):@decrator.auth_login_json #点赞前登陆验证def post(self, *args, **kwargs):rep = BaseResponse()news_id = self.get_argument('news_id', None) #获取新闻idif not news_id:rep.summary = "新闻ID不能为空."else:user_info_id = self.session['user_info']['nid']  #获取当前用户idconn = ORM.session()      #查询当前用户是否对该新闻点过赞has_favor = conn.query(ORM.Favor).filter(ORM.Favor.user_info_id == user_info_id,ORM.Favor.news_id == news_id).count()if has_favor:   #如果已经点过赞,删除数据库点赞表favor点赞数据,更新数据库新闻表news点赞数量-1conn.query(ORM.Favor).filter(ORM.Favor.user_info_id == user_info_id,ORM.Favor.news_id == news_id).delete()conn.query(ORM.News).filter(ORM.News.nid == news_id).update({"favor_count": ORM.News.favor_count - 1}, synchronize_session="evaluate")rep.code = StatusCodeEnum.FavorMinus    #返回已经点过赞的状态吗else:conn.add(ORM.Favor(user_info_id=user_info_id, news_id=news_id, ctime=datetime.datetime.now()))conn.query(ORM.News).filter(ORM.News.nid == news_id).update({"favor_count": ORM.News.favor_count + 1}, synchronize_session="evaluate")rep.code = StatusCodeEnum.FavorPlusconn.commit()conn.close()rep.status = True   #操作成功self.write(json.dumps(rep.__dict__))  #返回给前端

  点赞+1和-1动态效果js:

/*点赞+1效果*/functionAddFavorAnimation(ths){var offsetTop = -10;var offsetLeft = 20;var fontSize = 24;var opacity = 1;var tag = document.createElement('i');tag.innerText= "+1";tag.style.position= 'absolute';tag.style.top= offsetTop + 'px';tag.style.left= offsetLeft + 'px';tag.style.fontSize= fontSize + "px";tag.style.color= "#5cb85c";$(ths).append(tag);var addInterval = setInterval(function(){fontSize+= 5;offsetTop-= 15;offsetLeft+= 5;opacity-= 0.1;tag.style.top= offsetTop+ 'px';tag.style.left= offsetLeft+ 'px';tag.style.fontSize= fontSize + 'px';tag.style.opacity=opacity;if(opacity <= 0.5){tag.remove();clearInterval(addInterval);}},40)}

点赞+1效果

/*点赞-1效果*/functionMinusFavorAnimation(ths){var offsetTop = -10;var offsetLeft = 20;var fontSize = 24;var opacity = 1;var tag = document.createElement('i');tag.innerText= "-1";tag.style.position= 'absolute';tag.style.top= offsetTop + 'px';tag.style.left= offsetLeft + 'px';tag.style.fontSize= fontSize + "px";tag.style.color= "#787878";$(ths).append(tag);var addInterval = setInterval(function(){fontSize+= 5;offsetTop-= 15;offsetLeft+= 5;opacity-= 0.1;tag.style.top= offsetTop+ 'px';tag.style.left= offsetLeft+ 'px';tag.style.fontSize= fontSize + 'px';tag.style.opacity=opacity;if(opacity <= 0.5){tag.remove();clearInterval(addInterval);}},40)}

点赞-1效果

十七、评论功能

案例:

前端html:

<div class="box-r"><a href="javascript:void(0);" class="pub-icons add-pub-btn add-pub-btn-unvalid" onclick="DoComment({{item[0]}})">评论</a>  #携带新闻id<a href="javascript:void(0);" class="loading-ico loading-ico-top pub-loading-top hide">发布中...</a>
</div> 

{% raw tree(comment_tree) %}

创建评论树字典函数:

defbuild_tree(comment_list):comment_dic=collections.OrderedDict()for comment_obj incomment_list:if comment_obj[2] isNone:#如果是根评论,添加到comment_dic[评论对象] = {}comment_dic[comment_obj] =collections.OrderedDict()else:#如果是回复的评论,则需要在 comment_dic 中找到其回复的评论
tree_search(comment_dic, comment_obj)return comment_dic

build_tree

递归生成评论树函数:

deftree_search(d_dic, comment_obj):#在comment_dic中一个一个的寻找其回复的评论#检查当前评论的 reply_id 和 comment_dic中已有评论的nid是否相同,#如果相同,表示就是回复的此信息#如果不同,则需要去 comment_dic 的所有子元素中寻找,一直找,如果一系列中未找,则继续向下找for k, v_dic ind_dic.items():#找回复的评论,将自己添加到其对应的字典中,例如: {评论一: {回复一:{},回复二:{}}}if k[0] == comment_obj[2]:d_dic[k][comment_obj]=collections.OrderedDict()returnelse:#在当前第一个跟元素中递归的去寻找父亲tree_search(d_dic[k], comment_obj)

tree_search

前端调用uimethod,生成评论html:

TEMP1 = """<li class="items" style='padding:8px 0 0 %spx;'><span class="folder" id='comment_folder_%s'><div class="comment-L comment-L-top"><a href="#" class="icons zhan-ico"></a><a href="/user/moyujian/submitted/1"><img src="/statics/images/1.jpg"></a></div><div class="comment-R comment-R-top" style="background-color: rgb(246, 246, 246);"><div class="pp"><a class="name" href="/user/moyujian/submitted/1">%s</a><span class="p3">%s</span><span class="into-time into-time-top">%s</span></div><div class="comment-line-top"><div class="comment-state"><a class="ding" href="javascript:void(0);"><b>顶</b><span class="ding-num">[0]</span></a><a class="cai" href="javascript:void(0);"><b>踩</b><span class="cai-num">[0]</span></a><span class="line-huifu">|</span><a class="see-a jubao" href="javascript:void(0);">举报</a><span class="line-huifu">|</span><a class="see-a huifu-a" href="javascript:void(0);" onclick="reply(%s,%s,'%s')"  id='comment_reply_%s' >回复</a></div></div></div></span>"""
deftree(self, comment_dic):html= ''for k, v incomment_dic.items():html+= TEMP1 %(0,k[0], k[3],k[1],k[4],k[7],k[0], k[3],k[0])html+= generate_comment_html(v, 16)html+= "</li>"returnhtmldefgenerate_comment_html(sub_comment_dic, margin_left_val):#html = '<ul style="background: url(&quot;/statics/images/pinglun_line.gif&quot;) 0px -10px no-repeat scroll transparent;margin-left:3px;">'html = '<ul>'for k, v_dic insub_comment_dic.items():html+= TEMP1 %(margin_left_val,k[0], k[3],k[1],k[4],k[7],k[0], k[3],k[0])ifv_dic:html+=generate_comment_html(v_dic, margin_left_val)html+= "</li>"html+= "</ul>"return html

uimethod

后台handler:

class CommentHandler(BaseRequestHandler):def get(self, *args, **kwargs): #展示评论信息 # comment_list需要按照时间从小到大排列 nid = self.get_argument('nid', 0) conn = ORM.session() comment_list = conn.query( ORM.Comment.nid, ORM.Comment.content, ORM.Comment.reply_id, ORM.UserInfo.username, ORM.Comment.ctime, ORM.Comment.up, ORM.Comment.down, ORM.Comment.news_id ).join(ORM.UserInfo, isouter=True).filter(ORM.Comment.news_id == nid).all() conn.close() """ comment_list = [ (1, '111',None), #评论id,评论内容,回复id,如果是None,则代表回复新闻 (2, '222',None), (3, '33',None), (9, '999',5), (4, '444',2), (5, '555',1), (6, '666',4), (7, '777',2), (8, '888',4), ] """ comment_tree = commons.build_tree(comment_list) #最终传递给前端的是一个字典 self.render('include/comment.html', comment_tree=comment_tree) @decrator.auth_login_json #提交评论前做登陆验证 def post(self, *args, **kwargs): #提交评论信息 rep = BaseResponse() #前端反馈类 form = CommentForm() #评论的form验证 if form.valid(self):  #如果验证成功 form._value_dict['ctime'] = datetime.datetime.now() #获取当前时间 conn = ORM.session() #获取数据库session对象       # 将评论写入数据库 obj = ORM.Comment(user_info_id=self.session['user_info']['nid'], news_id=form._value_dict['news_id'], reply_id=form._value_dict['reply_id'], content=form._value_dict['content'], up=0, down=0, ctime=datetime.datetime.now()) conn.add(obj) conn.flush() conn.refresh(obj)  #同时获取评论的自增id rep.data = { 'user_info_id': self.session['user_info']['nid'], 'username': self.session['user_info']['username'], 'nid': obj.nid, 'news_id': obj.news_id, 'ctime': obj.ctime.strftime("%Y-%m-%d %H:%M:%S"), 'reply_id': obj.reply_id, 'content': obj.content, }       #更新数据库的评论数量 conn.query(ORM.News).filter(ORM.News.nid == form._value_dict['news_id']).update( {"comment_count": ORM.News.comment_count + 1}, synchronize_session="evaluate") conn.commit() conn.close() rep.status = True  #评论成功的状态信息 else: rep.message = form._error_dict  #错误信息 print(rep.__dict__) self.write(json.dumps(rep.__dict__)) #反馈给前端

js:

function DoComment(nid){var content = $("#comment_content_"+nid).val();var reply_id = $("#reply_id_"+nid).attr('target');if($('#action_nav').attr('is-login') == 'true'){  #已经登录才发ajax请求$.ajax({url: '/comment',type: 'POST',data: {content: content, reply_id:reply_id, news_id: nid},#发送评论内容,回复的评论id,新闻iddataType: 'json',success: function(arg){// 获取评论信息,将内容添加到指定位置console.log(arg);if(arg.status){$('#comment_content_' + arg.data.news_id).val('');var a = '<ul><li class="items" style="padding:8px 0 0 16px;"><span class="folder" id="comment_folder_';var b = arg.data.nid;var c = '"><div class="comment-L comment-L-top"><a href="#" class="icons zhan-ico"></a><a href="/user/moyujian/submitted/1"><img src="/statics/images/1.jpg"></a></div><div class="comment-R comment-R-top" style=""><div class="pp"><a class="name" href="/user/moyujian/submitted/1">';var d = arg.data.username;var e = '</a><span class="p3">';var f = arg.data.content;var g= '</span><span class="into-time into-time-top">';var h = arg.data.ctime;var i = '</span></div><div class="comment-line-top"><div class="comment-state"><a class="ding" href="javascript:void(0);"><b>顶</b><span class="ding-num">[0]</span></a><a class="cai" href="javascript:void(0);"><b>踩</b><span class="cai-num">[0]</span></a><span class="line-huifu">|</span> <a class="see-a jubao" href="javascript:void(0);">举报</a> <span class="line-huifu">|</span> <a class="see-a huifu-a" href="javascript:void(0);" onclick="';var j = "reply(" + arg.data.news_id + "," +arg.data.nid+",'"+arg.data.username+"')";var k = '">回复</a></div></div></div></span></li></ul>';var tag = a+b+c+d+e+f+g+h+i+j+k;console.log(arg,tag);if(arg.data.reply_id){$comment_folder = $('#comment_folder_' + arg.data.reply_id);$comment_folder.after(tag);}else{$('#comment_list_'+arg.data.news_id).append(tag);}}else{alert('error');}}})}else{$('#accountDialog').removeClass('hide');$('.shadow').removeClass('hide');}}

  总结:本博客是基于tornado实现的较完整版web开发项目,汇集了form验证、点赞、评论等高级功能,欢迎大家参考并提出相关问题,本人会在看到的第一时间回复,最后,如果您觉得本文对您有参考价值,欢迎推荐,谢谢!

  

  

转载于:https://my.oschina.net/u/3579120/blog/1507654

tornado web高级开发项目之抽屉官网的页面登陆验证、form验证、点赞、评论、文章分页处理、发送邮箱验证码、登陆验证码、注册、发布文章、上传图片...相关推荐

  1. tornado web高级开发项目

    抽屉官网:http://dig.chouti.com/ 一.配置(settings) settings ={'template_path': 'views', #模板文件路径'static_path' ...

  2. 【HTML响应式项目】成人教育官网前端页面(HTML+CSS+JS实现三端适应)

    项目源码已上传至码云仓库:云南农业职业技术学院/HTML响应式成人教育官网前端页面(HTML+CSS+JS实现) 项目演示地址:成人教育网 AAP端下载地址:成人教育网APP端.apk-互联网文档类资 ...

  3. 移动web网页开发——项目:生活资讯网(响应式开发)

    前两篇内容讲述了流式布局,flex布局,rem适配布局,响应式布局四种布局页面,今天我们就以学习的内容来完成项目"生活资讯网"的设计. 我们在设计开发生活资讯网可以选择的开发方式很 ...

  4. 清华规划院 伟景行 诚聘 Jsp/Java/Web高级开发工程师 互联网开发主管 测试工程师 etc.

    Jsp/Java/Web高级开发工程师  3人 岗位职责: 1.  参与公司网站(www.Gvitech.com)和数城网(www.diciti.com)的分析.规划.开发和维护工作: 2.  负责公 ...

  5. java ee web高级,Java EE Web高级开发案例

    核心提示:Java EE Web高级开发案例 内容简介:<Java EE Web高级开发案例>充分体现了高等职业教育的特点,突出了理论和实践的紧密结合,以充分掌握基本技术技能和必要的基本知 ...

  6. java ee web pdf_Java EE Web高级开发案例 PDF扫描版[69MB]

    Java EE Web高级开发案例 内容简介: <Java EE Web高级开发案例>充分体现了高等职业教育的特点,突出了理论和实践的紧密结合,以充分掌握基本技术技能和必要的基本知识为出发 ...

  7. 手把手教你上手Git并上传项目到GitHub官网

    手把手教你上手Git并上传项目到GitHub官网 Learning Git Branch: 学习 Git 最好的游戏及教程 https://learngitbranching.js.org 这个就是那 ...

  8. Axure企业官网通用模板web端+公司官网通用模板web端高保真原型+服务企业门户官网+加入我们+在线招聘+企业宣传+新闻动态+企业理念+产品与服务+公司通用版官方电脑端门户网站

    作品介绍:Axure企业官网通用模板web端+公司官网通用模板web端高保真原型+服务企业门户官网+加入我们+在线招聘+企业宣传+新闻动态+企业理念+产品与服务+公司通用版官方电脑端门户网站 原型演示 ...

  9. 宽屏html5网络科技有限公司官网单页面响应式模板

    宽屏html5网络科技有限公司官网单页面响应式模板文章目录 项目介绍 主要用途 包含模块 页面浏览 获取方式 项目介绍 本项目主要由html5,css样式,原生js构成的宽屏单页面响应式模板.整体结构 ...

最新文章

  1. python导入random模块_Python内置模块之random
  2. 阿里开发者们的第13个感悟:工程师需要在循环迭代中成长
  3. 华为触摸提示音怎么换_抖音苹果iPhone手机怎么改微信消息提示音 自定义换声音教程...
  4. Starter Kit for ASP.NET 2.0 家族又添新丁!
  5. 焦作市职业技术学校工业机器人专业_职业技术学院 工业机器人专业方向
  6. checkbox click和change事件
  7. cannot import name ‘Imputer‘ from ‘sklearn.preprocessing‘
  8. 2018年将会改变人工智能的5个大数据趋势
  9. Docker及常用软件的安装部署
  10. 大学及毕业总共9年时间追求过一个女孩却最终没有成功,期间的心酸,痛苦,怨恨以及最后消散写成了这篇2万字的散文诗小说。每一个字都是自己的心血,试问人生有几个9年?更何况是在你最美好的年华。喜欢的交流下。
  11. 2020年小米高级 PHP 工程师面试题
  12. 2017年北京邮电大学计算机考研机试试题及答案
  13. RSA组件之掩码生成函数(MGF)的实现(C源码)
  14. CookGAN Causality based Text-to-Image Synthesis
  15. **06-图3 六度空间 (30 分)**
  16. 计算机安全原理与实践第3版PDF,windows安全原理与技术.pdf
  17. matlab 鼠标画图,在matlab中使用鼠标在GUI上绘图
  18. 春天,陪你一起去赏雨——HTML5下雨效果
  19. Only no-arg methods may be annotated with @Scheduled
  20. Flutter之实现生成二维码,扫描二维码

热门文章

  1. golang 大数据平台_人工智能大数据平台中Golang的应用实践
  2. php强类型 vscode,VSCode 扩展入门,后缀代码补全的实现
  3. Hive分区修复命令MSCK介绍与使用
  4. 计算机中什么符号代表除号,电脑怎么打除号?word除号怎么打出来?键盘上÷号是哪个键?除以符号电脑怎么打?...
  5. java获得当月法定工作日_java获取当月的工作日
  6. c语言压缩存储,数据的压缩存储与解压缩算法实现(C语言)
  7. java信用卡卡号算法,java实现主要信息的加密解密(模拟信用卡号的保存)
  8. ABP官方文档(四十四)【后台作业和后台工人】
  9. [渝粤教育] 江西理工大学 机械之美——迷人的工程机械(双语) 参考 资料
  10. Unity ECS 简介