Docker入门使用:blue_car:

安装并配置加速地址

常用命令

docker ps  查看进程
docker ps -a 查看所有进程
systemctl restart docker 重启服务
docker stop 服务名

实操
docker run --name root -e MYSQL_ROOT_PASSWORD=123456 -p 8270:3306 -d mysql 拉取mysql docker镜像并且开启服务 将端口号映射到8270端口
docker ps 查看进程
docker stop root 停止root mysql服务
docker rm root 销毁mysql服务
docker logs -f root 查看服务具体日志

docker服务不需要考虑环境问题就能跑起服务,非常有效

docker-compose集合命令 用于创建多个docker镜像服务

#下载:
curl -L https://get.daocloud.io/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

#权限:
chmod a+x /usr/local/bin/docker-compose

#查看版本:
docker-compose --version

创建docker-compose.yml

version: '3'
services:
mysql1:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=123456
ports:
- 8271:3306

mysql2:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=123456
ports:
- 8272:3306

# Use root/example as user/password credentials
version: '3.1'

services:

mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example

mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: example

运行 docker-compose up -d

实战

service docker start  启动docker服务

# 创建持久化sql服务
docker run --restart=always --name root -e MYSQL_ROOT_PASSWORD=123456 -p 8270:3306 -d mysql

# 创建持久化redis服务
docker run -itd --restart=always --name redis-test -p 8271:6379 -v/home/redistest1:/data redis redis-server --requirepass 123456


# 区分生产环境和线上环境 运行springboot
java -jar 生成的jar包 --spring.profiles.active=prod

# 后台运行
nohub java -jar 生成的jar包 --spring.profiles.active=prod

#运行日志文件输出
nohup java -jar imissyou-0.0.1-SNAPSHOT.jar >temp.txt &

nohup java -jar imissyou-0.0.1-SNAPSHOT.jar >log.log 2>&1 &

# ps -ef
查看当前所有进程

# kill -9 Pid
杀死某个进程

# 查看端口号情况 不加后面的查看所有的端口号
netstat -ano |findstr 8080

# 杀死进程
taskkill -PID 进程号 -F

# 连接远程服务器
ssh root@47.97.180.232

Nginx学习:accept:

反向代理解决跨域问题

nginx.conf

server
{
listen 80;
server_name yuba.yangxiansheng.top;

location / {
proxy_pass https://yuba.douyu.com;
add_header Access-Control-Allow-Origin *;
}

#禁止访问的文件或目录
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
{
return 404;
}

#一键申请SSL证书验证目录相关设置
location ~ \.well-known{
allow all;
}

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
error_log off;
access_log /dev/null;
}

location ~ .*\.(js|css)?$
{
expires 12h;
error_log off;
access_log /dev/null;
}
access_log /www/wwwlogs/ceshi.yangxiansheng.top.log;
error_log /www/wwwlogs/ceshi.yangxiansheng.top.error.log;
}

项目开发

Sass学习:star:

sass和之前学习的stylus相同,只不过语法方面有点不同

语法

  1. sass必须要有大括号和分号结尾

css函数

定义函数

@function px2rem($px){
@return $px/$ratio +rem;
}
@mixin center{
display:center;
justify-content:center;
align-items:center;
}

使用方法

@include+函数名

css变量

$text-large:20px;

使用案例

@import "./mixin";

$text-large: px2rem(18);
$text-big: px2rem(16);
$text-medium: px2rem(14);
$text-small: px2rem(12);
$text-tiny: px2rem(10);

$text-large-lh: px2rem(20);
$text-big-lh: px2rem(18);
$text-medium-lh: px2rem(16);
$text-small-lh: px2rem(15);
$text-tiny-lh: px2rem(12);

$text-big-max-height3: px2rem(54);
$text-medium-max-height3: px2rem(48);
$text-samll-max-height3: px2rem(42);
$text-big-max-height2: px2rem(36);
$text-medium-max-height2: px2rem(32);
$text-medium-max-height: px2rem(16);
$text-small-max-height2: px2rem(30);
$text-small-max-height: px2rem(15);
$text-tiny-max-height: px2rem(12);

.title-big {
line-height: $text-big-lh;
font-size: $text-big;
max-height: $text-big-max-height2;
color: #444;
font-weight: bold;
@include ellipsis2(3);
}
.title-medium {
font-size: $text-medium;
line-height: $text-medium-lh;
max-height: $text-medium-max-height2;
color: #444;
font-weight: bold;
@include ellipsis2(3);
}
.title-small {
font-size: $text-small;
line-height: $text-small-lh;
max-height: $text-small-max-height2;
color: #444;
font-weight: bold;
@include ellipsis2(2);
}
.sub-title-medium {
line-height: $text-medium-lh;
font-size: $text-medium;
max-height: $text-medium-max-height2;
color: #666;
@include ellipsis2(2);
}
.sub-title {
line-height: $text-small-lh;
font-size: $text-small;
max-height: $text-small-max-height;
color: #666;
@include ellipsis2(1);
}
.sub-title-tiny {
line-height: $text-tiny-lh;
font-size: $text-tiny;
max-height: $text-tiny-max-height;
color: #666;
@include ellipsis2(1);
}
.third-title {
line-height: $text-small-lh;
font-size: $text-small;
max-height: $text-small-max-height;
color: #999;
@include ellipsis2(1);
}
.third-title-tiny {
line-height: $text-tiny-lh;
font-size: $text-tiny;
max-height: $text-tiny-max-height;
color: #999;
@include ellipsis2(1);
}

Element-Ui

安装并使用

安装并且使用

npm i element-ui -S
import 'element-ui/lib/theme-chalk/index.css'

import ElementUI from 'element-ui'

Vue.use(ElementUI)

按需加载项目

npm install babel-plugin-component -D

然后修改 babel.config.js

{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

引入组件

import {Button} from 'element-ui'
Vue.use(Button)

单独js文件引入

import {
Message
} from 'element-ui'

Message({
type: 'error',
message: error.response.data.msg,
duration: 4000
})

完整的组件按需引入

import {
Pagination,
Dialog,
Autocomplete,
Dropdown,
DropdownMenu,
DropdownItem,
Menu,
Submenu,
MenuItem,
MenuItemGroup,
Input,
InputNumber,
Radio,
RadioGroup,
RadioButton,
Checkbox,
CheckboxButton,
CheckboxGroup,
Switch,
Select,
Option,
OptionGroup,
Button,
ButtonGroup,
Table,
TableColumn,
DatePicker,
TimeSelect,
TimePicker,
Popover,
Tooltip,
Breadcrumb,
BreadcrumbItem,
Form,
FormItem,
Tabs,
TabPane,
Tag,
Tree,
Alert,
Slider,
Icon,
Row,
Col,
Upload,
Progress,
Spinner,
Badge,
Card,
Rate,
Steps,
Step,
Carousel,
CarouselItem,
Collapse,
CollapseItem,
Cascader,
ColorPicker,
Transfer,
Container,
Header,
Aside,
Main,
Footer,
Timeline,
TimelineItem,
Link,
Divider,
Image,
Calendar,
Backtop,
PageHeader,
CascaderPanel,
Loading,
MessageBox,
Message,
Notification
} from 'element-ui';

Vue.use(Pagination);
Vue.use(Dialog);
Vue.use(Autocomplete);
Vue.use(Dropdown);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Menu);
Vue.use(Submenu);
Vue.use(MenuItem);
Vue.use(MenuItemGroup);
Vue.use(Input);
Vue.use(InputNumber);
Vue.use(Radio);
Vue.use(RadioGroup);
Vue.use(RadioButton);
Vue.use(Checkbox);
Vue.use(CheckboxButton);
Vue.use(CheckboxGroup);
Vue.use(Switch);
Vue.use(Select);
Vue.use(Option);
Vue.use(OptionGroup);
Vue.use(Button);
Vue.use(ButtonGroup);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(DatePicker);
Vue.use(TimeSelect);
Vue.use(TimePicker);
Vue.use(Popover);
Vue.use(Tooltip);
Vue.use(Breadcrumb);
Vue.use(BreadcrumbItem);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Tabs);
Vue.use(TabPane);
Vue.use(Tag);
Vue.use(Tree);
Vue.use(Alert);
Vue.use(Slider);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Upload);
Vue.use(Progress);
Vue.use(Spinner);
Vue.use(Badge);
Vue.use(Card);
Vue.use(Rate);
Vue.use(Steps);
Vue.use(Step);
Vue.use(Carousel);
Vue.use(CarouselItem);
Vue.use(Collapse);
Vue.use(CollapseItem);
Vue.use(Cascader);
Vue.use(ColorPicker);
Vue.use(Transfer);
Vue.use(Container);
Vue.use(Header);
Vue.use(Aside);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Timeline);
Vue.use(TimelineItem);
Vue.use(Link);
Vue.use(Divider);
Vue.use(Image);
Vue.use(Calendar);
Vue.use(Backtop);
Vue.use(PageHeader);
Vue.use(CascaderPanel);

Vue.use(Loading.directive);

Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;

表单组件:baby_chick:

表单引入

model 绑定表单对象,inline设置单行显示,:rules设置表单的校验规则

<el-form
label-position="top"
:rules="rules"
:model="userInfo"
ref="form"
>
<el-form-item class="item" label="邮箱地址" prop="userName">
<el-input v-model="userInfo.userName" placeholder="邮箱"></el-input>
</el-form-item>
<el-form-item class="item" label="登录密码" prop="password">
<el-input
show-password
v-model="userInfo.password"
placeholder="密码"
></el-input>
</el-form-item>
<el-form-item class="item-code" label="验证码" prop="code">
<el-row type="flex" justify="space-between">
<el-col :span="12">
<el-input
v-model="userInfo.code"
placeholder="验证码"
></el-input>
</el-col>
<el-col :span="12">
<span @click="_getCode()" v-html="code"></span>
</el-col>
</el-row>
</el-form-item>
<el-row class="link">
<el-col :offset="20"
><el-link type="primary" @click="Toreset()"
>忘记密码</el-link
></el-col
>
</el-row>
<el-form-item>
<el-button style="width:100%" type="primary" @click="onSubmit"
>登录</el-button
>
</el-form-item>
</el-form>

表单校验常用

  1. data中定义校验规则对象,例如:rules,然后再form引入,并指定每个表单项的校验规则

    先指定规则:rules,然后指定元素的校验规则prop="规则里面的对象"

    <el-form inline :model="data" :rules="rules" ref="form">
    <el-form-item label="审批人" prop="user">
  1. 编写校验规则,rules 参考async-validator
const userValidator = (rule, value, callback) => {
if (value.length > 3) {
callback()
} else {
callback(new Error('用户名长度必须大于3'))
}
}
return {
data: {
user: 'sam',
region: '区域二'
},
rules: {
// user的校验规则 可以为数组
user: [
// 参考async-validator trigger: 什么时候触发,常用的是blur失焦 change改变,message:错误信息
{ required: true, trigger: 'change', message: '用户名必须录入' },
// 自定义校验规则
{ validator: userValidator, trigger: 'change',message:'校验不符合规范' }
]
}
}
},


// 最后提交也需要校验
method:{
submitForm(formName) {
this.$refs.form.validate(async(valid) => {
if (!valid) {
return false
}
})

}
}

校验参考

  rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
],
date1: [
{ type: 'date', required: true, message: '请选择日期', trigger: 'change' }
],
date2: [
{ type: 'date', required: true, message: '请选择时间', trigger: 'change' }
],
type: [
{ type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }
],
resource: [
{ required: true, message: '请选择活动资源', trigger: 'change' }
],
desc: [
{ required: true, message: '请填写活动形式', trigger: 'blur' }
]
}
};

password: [
{
required: true, message: '密码为必填项', trigger: 'blur'
},
{
pattern: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/, message: '密码应包含大写字母小写字母和数字和特殊字符', trigger: 'blur'
}
],

  1. 表单常见属性 参考属性

其他组件用法总结

布局

默认24分栏

一行

一列

:span 指定分布区域大小

:gutter 指定每列间隔 ,定义在行

:offset指定分栏偏移,定义在列

<el-row :gutter="20">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>

指定flex布局

<el-row type="flex" class="row-bg">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="center">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="end">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="space-around">
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>

弹出框

Tab

解决重复请求问题 切换到另一个tab才请求数据,记录对象,如果重复点击 直接退出

data() {
return {
activeName: 'first',
x: {
name: 'first'
}
};
},
methods: {
handleClick(tab, event) {
if(this.x.name !== this.activeName){
// 第一次点击 或者点击到其他tab
this.x = tab
console.log('success')
}else if(tab.name === this.x.name){
return
}
}
}

选择框

 <el-select v-model="collegeValue" clearable style="margin-left:6px" placeholder="请选择学院" @change="OnselectCollege">
<el-option
v-for="item in collegeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>


options = [{label:'1',value:'1'}...]

消息框

this.$confirm('确定要删除这名学生的信息吗, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});

分页器

  <el-pagination
:background="true"
:page-sizes="[10,15,20,25]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>


分页逻辑
// 初始化分页数据
async initData() {
this.listLoading = true
const res = await Student.getList(this.page, this.pageSize)
if (res.code === 200) {
this.totalPage = res.data.total_page
this.totalCount = res.data.total
this.listLoading = false
this.list = res.data.items
} else {
this.$message.error(res.msg)
}
},

// 监听分页器分页大小
handleSizeChange(value) {
this.pageSize = value
this.initData()
},
// 监听当前页改变
handleCurrentChange(value) {
this.page = value - 1
this.initData()
},

Dialog

<el-dialog title="收货地址" :visible.sync="dialogFormVisible">
<el-form :model="form">
<el-form-item label="活动名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="活动区域" :label-width="formLabelWidth">
<el-select v-model="form.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogFormVisible = false">确 定</el-button>
</div>
</el-dialog>

文件上传

<el-upload
class="avatar-uploader"
action="https://imgkr.com/api/files/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon" />
</el-upload>

// 用户头像处理
handleAvatarSuccess(res, file) {
this.userInfo.avatar = res.data
console.log(this.userInfo)
},
beforeAvatarUpload(file) {
const formatType = file.type === 'image/jpeg' || 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2

if (!formatType) {
this.$message.error('上传头像图片只能是 JPG或者PNG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return formatType && isLt2M
}

登录注册找回密码:hear_no_evil::hear_no_evil:

验证码接口

这个使用到了svg-capthcha插件生成验证码

async getCode(ctx,next){
const newCaptcha = svgCaptcha.create({
// 指定验证长度
size:4,
// 指定忽略的字符
ignoreChars:"0o1il",
color:true,
// 指定干扰线数量
noise:Math.floor(Math.random()*5),
width: 150,
height: 38
})
ctx.body={
code:200,
data:newCaptcha.data
}

}

改造验证码接口,这里我们使用redis验证码的值放进缓存,然后设置时效性

首先前端通过uuid生成随机串码,然后存储到localstorage再保存在vuex中 ,vuex使用mixin混入

storeSid(){
let sid
if(localStorage.getItem('sid')){
sid =localStorage.getItem('sid')
}else{
sid = uuid()
localStorage.setItem('sid',sid)
}
this.setSid(sid)
}

// 前端发送请求
const sid = this.sid
const code = await Public.getCode(sid)

后端保存

const sid = ctx.request.query.sid
// 十分钟键值
setValue(sid,newCaptcha.text,10*60)

登录注册接口(jwt鉴权):loudspeaker:

引入koa-jwt鉴权,然后编写登陆注册接口

首先引入koa-jwt插件帮助我们快速集成jwt鉴权

app.js中使用

const JWT = require('koa-jwt')
const config = require('./config/config')
app.use(exception)
// 选择鉴权跳过的路由前缀 比如公共路由
app.use(JWT({ secret: config.JWT_SECRET }).unless({ path: [/^\/v1\/public/] }))

如果接口是保护的接口,就会抛出401状态码,这里需要对异常进行处理**,全局异常处理**参考异常处理

鉴权异常

Http异常 未知异常

const { HttpExecption } = require('../core/http-execption')
// 定义全局异常中间件
const eception = async (ctx, next) => {
try {
await next()
} catch (error) {
const IsHttpexecption = error instanceof HttpExecption
// 校验token
if(error.status == 401){
ctx.status = 401
ctx.body = {
msg: 'token令牌不合法',
code: 401,
request: `${ctx.method} ${ctx.path}`
}
}else{
// 如果是Http已知异常
if (IsHttpexecption) {
ctx.body = {
msg: error.msg,
code: error.errorCode,
request: `${ctx.method} ${ctx.path}`
}
ctx.status = error.code
} else {
ctx.body = {
msg: '未知异常发生',
code: 999,
request: `${ctx.method} ${ctx.path}`
}
ctx.status = 500
}
}
}
}

了解jwtAPI

jwt.sign() 
// 生成令牌 需要传入参数:1.传入自定义信息(后面可封装在auth里) 2.secretKey秘钥(用户自定义) 3.配置(失效时间)单位为秒

expiresIn: expressed in seconds or a string describing a time span zeit/ms.
Eg: 60, "2 days", "10h", "7d". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms")


例:jwt.sign(
{uid,scope},secretKey,{expiresIn}
)

//1h 第一种方式
jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
data: 'foobar'
}, 'secret');

//1h 第二种方式 推荐
jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: 60 * 60 });



jwt.verify() //校验令牌 如果token无效会抛出异常
// 需要传入 token 秘钥 两个个参数
最好是放在try catch中捕获异常
try{
var decode = jwt.verify(UserToken,secretKey)
// 校验令牌合法 不合法抛出异常
}catch(error){
if (error.name == 'TokenExpiredError') {
errMsg = 'token令牌已经过期'
}
throw new ForbidenException(errMsg)
}

登录逻辑:

  • 接收用户的数据
  • 验证图形验证码的有效性,正确性
  • 数据库判断用户名密码(比较盐)是否正确
  • 返回token

相关业务代码

// 登录
async login(ctx, next) {
const v = await new LoginValidator().validate(ctx)
const userInfo = {
userName: v.get('body.userName'),
password: v.get('body.password'),
code: v.get('body.code'),
sid: v.get('body.sid'),
}
// 校验验证码合法性
const result = await UserService._checkCode(userInfo.sid, userInfo.code)
if (result) {
// 比对数据库
const res = await User.findOne({ userName: userInfo.userName })
if(res == null){
throw new NotFoundException('用户名不存在')
}
// 比对密码
const checkpwd = bcryptjs.compareSync(userInfo.password, res.password)
if (checkpwd) {
const token = JWT.sign({ _id: 'pi' }, config.JWT_SECRET, {
expiresIn: '2 days',
})
ctx.body = {
code: 200,
msg: '登录成功',
data: {
userInfo: res,
token,
},
}
} else {
// 用户名密码错误
throw new NotFoundException('密码错误')
}
} else {
// 验证码不正确异常
throw new ParameterException('验证码错误')
}
}

明文加密

const bcryptjs = require('bcryptjs')
// 保存加密密码
const sault = bcryptjs.genSaltSync(10)
const pwd = bcryptjs.hashSync(body.passsword,sault)
const user = new User({
userName:userName,
password:pwd
})
user.save()
// 比较密码
const user = User.findOne({userName:body.userName})
// 后面是加密的
const result = bcryptjs.compareSync(body.password,user.password)

注册逻辑:

  • 接收数据,处理数据
  • 核验验证码正确性和有效性
  • 比对数据中的userName是否唯一,插入数据
// 注册
async register(ctx, next) {
const v = await new RegisterValidator().validate(ctx)
const userInfo = {
userName: v.get('body.userName'),
password: v.get('body.password'),
code: v.get('body.code'),
sid: v.get('body.sid'),
nickName: v.get('body.nickName'),
}
const result = await UserService._checkCode(userInfo.sid, userInfo.code)
if (result) {
// 校验用户名是否已存在
const res = await User.findOne({ userName: userInfo.userName })
if (res == null) {
// 加密
const sault = bcryptjs.genSaltSync(10)
userInfo.password = bcryptjs.hashSync(userInfo.password, sault)
const userDocument = new User({
userName: v.get('body.userName'),
password: userInfo.password,
name: v.get('body.nickName'),
existed: moment().format('YYYY-MM-DD HH:mm:ss'),
})
await userDocument.save()
ctx.body = {
code: 200,
msg: '注册成功',
create_time: moment().format('x'),
}
} else {
throw new AllExistedException('用户名已存在')
}
} else {
throw new ParameterException('验证码错误')
}
}``

邮箱找回密码接口:relaxed:

配置公共邮箱的开启stmp服务

wsrnpqaeinswbjcc

这里需要使用nodemailer插件

  • 配置nodemailer初始方法

    async function send(sendInfo) {
    let transporter = nodemailer.createTransport({
    // 发送的主机地址
    host: 'smtp.qq.com',
    port: 587,
    secure: false,
    //配置授权邮箱 和授权码
    auth: {
    user: '251205668@qq.com', // generated ethereal user
    pass: 'wsrnpqaeinswbjcc', // generated ethereal password
    },
    })
    // 配置跳转的路由
    let url = 'http://www.imooc.com'
    let info = await transporter.sendMail({
    from: '"认证邮件" <251205668@qq.com>', // sender address
    to: sendInfo.email, // 发送的邮箱账号
    // 配置主题
    subject:
    sendInfo.user !== ''
    ? `你好开发者,${sendInfo.user}!《论坛》验证码`
    : '《论坛》验证码', // Subject line
    text: `您在《论坛》中注册,您的邀请码是${
    sendInfo.code
    },邀请码的过期时间: ${sendInfo.expire}`,// 模拟生成的30分钟倒计时
    // 邮件主题内容
    html: `
    <div style="border: 1px solid #dcdcdc;color: #676767;width: 600px; margin: 0 auto; padding-bottom: 50px;position: relative;">
    <div style="height: 60px; background: #393d49; line-height: 60px; color: #58a36f; font-size: 18px;padding-left: 10px;">论坛社区——欢迎来到官方社区</div>
    <div style="padding: 25px">
    <div>您好,${sendInfo.user}童鞋,重置链接有效时间30分钟,请在${
    sendInfo.expire
    }之前重置您的密码:</div>
    <a href="${url}" style="padding: 10px 20px; color: #fff; background: #009e94; display: inline-block;margin: 15px 0;">立即重置密码</a>
    <div style="padding: 5px; background: #f2f2f2;">如果该邮件不是由你本人操作,请勿进行激活!否则你的邮箱将会被他人绑定。</div>
    </div>
    <div style="background: #fafafa; color: #b4b4b4;text-align: center; line-height: 45px; height: 45px; position: absolute; left: 0; bottom: 0;width: 100%;">系统邮件,请勿直接回复</div>
    </div>
    `, // html body
    })

    return 'Message sent: %s', info.messageId
    }

  • 编写接口方法

     async sendEmail(ctx,next){
    const v = await new SendEmailValidator().validate(ctx)
    const userName = v.get('body.userName')
    const result = await send({
    // 验证码: 暂时模拟为1234
    code:1234,
    // 有效时间:创建模拟的格式化的时间
    expire:moment().add(30,"minutes").format('YYYY-MM-DD HH:mm:ss'),
    email:userName,
    user:'努力中的杨先生'
    })
    ctx.body = {
    code:200,
    message:"邮件发送成功",
    data:result
    }
    }
  • 优化后的忘记密码

    async senEmail(ctx,next){
    // 发送邮箱 接收参数userName sid
    const v = await SenEmailValidator().validate(ctx)
    const userName = v.get('body.userName')
    const sid = v.get('body.sid')
    const code = v.get('body.code')
    // 校验验证码
    if(code!==null && code.toLowerCase === getValue(sid).toLowerCase){
    // 校验用户名是否存在
    const user = await User.findOne({userName:userName})
    if(user){
    const key = uuid()
    const token = jwt.sign({_id:user._id},JWT_SECRET,{ expiresIn:60*30})
    await setValue(key,token)
    const result = await send({
    data:{
    token,
    userName
    },
    expire:moment().add(30,"minutes").format('YYYY-MM-DD HH:mm:ss'),
    email:userName,
    user:user.name
    })
    ctx.body = {
    msg:'邮件发送成功,请注意查收',
    code:200,
    data:result
    }

    }else{
    ctx.body = {
    code:500,
    msg:'用户名不存在'
    }
    }
    }else{
    ctx.body = {
    code:500,
    msg: '验证码错误'
    }
    }

    }

    // 链接路由:localhost:8080/
    忘记密码接口
    async forgetPassword(ctx
    const v = await new ForgetPasswordValidator().valdate(ctx)
    const newPassword = v.get('body.newPassword')
    // 接收参数 newPassword
    const playLoad = await getJwtplayLoad(ctx.request.header.authorization)
    const _id = playLoad._id
    const user = await User.findOne({_id})
    // 加密
    const sault = bcryptjs.genSaltSync(10)
    password = bcryptjs.hashSync(newPassword, sault)
    // 更新
    const result = await User.updateOne({_id},{password})
    if(result.n === 1 && result.ok === 1){
    ctx.body = {
    msg:'重置密码成功',
    code:200
    }
    }else{
    ctx.body = {
    msg:'重置密码失败',
    code:500
    }
    }
    }


    另一种方法: 发送邮件时 生成一个随机四位数,然后存储在redis中.当用户忘记密码需要重置密码时,传入新密码和验证码,后端查询redis,如果正确就可以重置密码

配置项目

封装axios:alien::alien:

初步封装

// 封装 axios
// 1.封装请求返回数据 2.异常统一处理
// 鉴权处理

import axios from 'axios'
import errorHandle from './errorHandle'

const instance = axios.create({
// 统一请求配置
baseURL:
process.env.NODE_ENV === 'development'
? config.baseURL.dev
: config.baseURL.pro,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
timeout: 10000
})


// 请求拦截器
instance.interceptors.request.use(
config => {
return config
},
// 请求异常处理
err => {
errorHandle(err)
return Promise.reject(err)
}
)

// 请求结果封装
instance.interceptors.response.use(
res => {
if (res.status === 200) {
// 直接返回res.data
return Promise.resolve(res.data)
} else {
return Promise.reject(res)
}
},
error => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error 处理非200的请求返回的异常: 比如404 ,API服务暂停(没有状态码)等
errorHandle(error)
return Promise.reject(error)
}
)

异常处理初步封装

import {
Message
} from 'element-ui'
const errorHandle = (error) => {
const errorStatus = error.response.status
switch (errorStatus) {
case 401:
console.log('刷新token')
break
case 500:
Message({
type: 'error',
message: error.response.data.msg,
duration: 4000
})
break
case 404:
Message({
type: 'error',
message: '网络异常',
duration: 4000
})
break
default:
break
}
}

代理请求和路径代理(解决跨域):weary:

配置vue.conf.js

const path = require('path')

module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000/v1'
}
}
},
chainWebpack: (config) => {
config.resolve.alias
.set('scss', path.join(__dirname, './src/assets/scss/'))
.set('@', path.join(__dirname, './src/'))
}
}

环境变量

创建.env.development.env.production

VUE_APP_BASE_URL=http://localhost:3000

默认的环境变量

开发环境  process.env.NODE_ENV = 'development'
生产环境 process.env.NODE_ENV = 'production'


axios.defaults.baseURL =
process.env.NODE_ENV !== 'production'
? 'http://localhost:3000'
: 'http://your.domain.com'

然后使用环境变量

process.env.VUE_APP_BASE_URL

路由懒加载:accept:

安装插件syntax-dynamic-import 然后书写babel.config文件

plugins: [
['@babel/plugin-syntax-dynamic-import']
]
懒加载语法
const componentName = () => import('../pages/login')

优化路由切换动效:blonde_woman:

使用到了iviewLoadingBar组件

import { LoadingBar } from 'iview'
router.beforeEach((to, from, next) => {
LoadingBar.start()
next()
})
router.afterEach(() => {
LoadingBar.finish()
})

集成Mongdb和redis

mongdb入门

mongdb是属于典型的非关系型数据库,很好的处理了分布式储存,以对象的形式存储数据

Docker安装
# Use root/example as user/password credentials
version: '3.1'

services:

mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- 27017:27017
volumes
- /home/mongtest/data/db
增删改查
# 使用test数据库
use test

# 查看表 集合
show collections

# 统计集合中对象数量
db.users.find().count()




# 增加一个对象 集合习惯在后面加上s
db.users.insert({username:"马云"})

# 向username='马云'的对象中添加一个字段为地址='杭州'
db.users.update({username:"马云",
{
·$set:{
···{address:'杭州'}
···}
··}
})

db.users.update({username:"孙悟空"},{
$set:{
hobby:{
cities:["北京","上海","深圳"],
movies:["三国","英雄"]
}
}
})




# 删除一个对象
db.users.remove({username:'马云'})

# 删除users集合
db.users.remove({})
db.users.drop()

# 删除username=马云的address属性
db.users.update({username:'马云'},{$unset:{address:""}})

# 删除 name= aa city不存在的
db.users.deleteOne({name:'aa',city:{$exists:false}})



# 修改username为马云的对象为马化腾
db.users.replaceOne({username:‘马云’},{username:'马化腾'})

# 更改数据
db.users.update({name:"hjx"},{name:"hjx",age:20})




# 查询
db.users.find({"hobby.movies":"英雄"})

# 查找 age = 30的
db.users.find({age:30})

# 操作符 大于12的
db.users.find({age: {$gt:12}})

# 操作符 大于等于12的
db.users.find({age: {$gte:12}})

# 操作符 小于等于12的
db.users.find({age: {$lte:12}})

# 大于等于12 小于等于30
db.users.find({age: {$lte:30 , $gte:12}})

# 是否存在 city
db.users.find({city: {$exists:true}})

# 除了age其他字段都查出来
db.users.find({},{age:0})

# 按工资升序排列
db.emp.find({}).sort({sal:1})
# 按工资降序排列
db.emp.find({}).sort({sal:-1})



# 分页
# 查询numbers 21条到30条的数据
db.numbers.find().skip(20).limit(10)

skip((pageNo-1)*pageSize).limit(pageSize)

练习

Mongoose:cocktail::cocktail::eight_pointed_black_star:

mongdb的orm框架,让node操作mongdb变得更加方便

mongoose操作的对象是

  • Schema 模式对象,对字段约束
  • Model 相当于Mongdb的集合collections
  • Document文档

使用mongoose连接数据库

const mongoose = require('mongoose')
cosnt db = mongoose.connect("mongodb://数据库的ip地址:端口号/数据库名")
db.on('open',()=>{
console.log("数据库连接成功")
})

具体使用案例

首先明确这几个点

mongoose需要通过Schema约束模型,然后通过模型对象进行增删改查

所有首先需要创建Schema对象,再创建Model,再编写业务

创建模型

const mongoose = require('mongoose')
await mongoose.connect('mongodb://localhost/my_database', {
useNewUrlParser: true,
useUnifiedTopology: true
});

const Schema = mongoose.Schema
const UserSchema = new Schema({
name:String,
password:String
})
// 创建模型
const UserModel = mongoose.model('user',UserSchema)

这里Schema约束规范实例参考

const UserSchema =Schema({
// type:类型
// require 是否必须
// unique 在mongodb里创建索引 类似mysql唯一主键
// enum:['aa','bb'] 指定他的值必须是什么
name:{type:String,require:true,unique:true,enum:['hjx','lisi']},
// 最简单写法
// age:Number
// 数字复杂的校验
// max 最大值
// min 最小值 如果是数组 第一个值是最小范围 第二个值是报错信息
age:{type:Number,max:90,min:[18,'不能小于18岁']}
})

如果集合已经存在 ,这里是个坑


>const Schema = Mongoose.Schema()

>const ci = Mongoose.model('ci',Schema,'ci')

>// 删除一条数据
>// ci.remove({'':'5ebe3c8de468cc2157db610b'},(err,res)=>{
>// console.log('删除成功',res)
>// })

>ci.find({author:'和岘'},(err,res)=>{
console.log(res)
>})


mongoose增删改查

**插入保存在数据库**
// 创建好mongoose模型
cosnt UserModel = mongoose.model('user',UserSchema)
cosnt user = new UserModel({
username:'马云',
password:'123456'
})
// 保存
user.save((err,res)=>{
if(err){
console.log(err)
}else{
console.log(res)
}
})

更新数据

cosnt user = new UserModel({
username:'马云',
password:'123456'
})
// 更新数据
user.update({
username:'马化腾',
password:'hello'
})


// 根据id更新数据 find 查询的数组 findOne查询一条数据
let id ='1231231231'
UserModel.findByIdAndUpdate(id,{
password:'wohenpi'
},(err,res)=>{

})

// 更新数据 $set $inc增加
UserModel.updateOne({_id:'1'},{
$set:{userName:''},
$inc:{fav:}
})

删除数据

UserModel.remove({
username:'马云'
},(rer,res)=>{

})


Model.findByIdAndRemove(id,{},callback)
Model.findOneByIdAndRemove(id,{},callback)

查询

//普通查询
UserModel.find({
username:'马云'
},(err,res)=>{

})

// 根据Id查询
UserModel.findById({id=""},(err,res)=>{})

// 查询数量
USerModel.count({username:''},(err,res)=>{})
现已替换成 `countDocuments`
查询所有的数量: `estimatedDocumentCount`无法接condition

// 如果不想输出id值 选择性投影
UserModel.find({username:'马云'},{'username:1',"_id":0},(err,res)=>{

})

// 嵌套查询对象数组中 某个属性
// 例如: tags:[{class:'',name:''}],查询tag.name = 数值中的那么
model.find(tags:{$elemMathch:{name:xxx}})

// 条件查询 大于等于21,小于等于65
UserModel.find({userage: {gte: 21,lte: 65}}, callback);

$or    或关系

$nor    或关系取反

$gt    大于

$gte    大于等于

$lt     小于

$lte    小于等于

$ne 不等于

$in 在多个值范围内

$nin 不在多个值范围内

$all 匹配数组中多个值

$regex  正则,用于模糊查询

$size   匹配数组大小

$maxDistance  范围查询,距离(基于LBS)

$mod   取模运算

$near   邻域查询,查询附近的位置(基于LBS)

$exists   字段是否存在

$elemMatch  匹配内数组内的元素

$within  范围查询(基于LBS)

$box    范围查询,矩形范围(基于LBS)

$center 范围醒询,圆形范围(基于LBS)

$centerSphere  范围查询,球形范围(基于LBS)

$slice    查询字段集合中的元素(比如从第几个之后,第N到第M个元素)


// 模糊查询 使用正则表达式
let whereStr = {'username':{$regex:/m/i/}}// 查询username包含m并且不区分大小写

// 分页查询
var pageSize = 5; //一页多少条
var currentPage = 1; //当前第几页
var sort = {'logindate':-1}; //排序(按登录时间倒序)
var condition = {}; //条件
var skipnum = (currentPage - 1) * pageSize; //跳过数
User.find(condition).skip(skipnum).limit(pageSize).sort(sort).exec((err,res)=>{})

async function query(){
const db =await Mongoose.connect('mongodb://localhost:27017/poems',{useNewUrlParser:true,useUnifiedTopology:true})
if(db){
console.log('连接成功')
}

const Schema = Mongoose.Schema()

const ci = Mongoose.model('ci',Schema,'ci')
const writer =Mongoose.model('writer',Schema,'writer')

const result1 = await ci.findById({_id:'5ebe3c8de468cc2157db610b'})
console.log('ByID:'+result1)

const result2 = await ci.countDocuments({_id:'5ebe3c8de468cc2157db610b'})
console.log('count:'+result2)

const result3 = await ci.findById({_id:'5ebe3c8de468cc2157db610b'},{_id:0})
console.log('不返回某些值'+result3)

const result4 = await ci.find({author:{$regex:/和/i}})
console.log('模糊查询:'+result4)




const pageSize = 10
const currentPage = 1
const sortstr = {'id':0}
let skipNums = (currentPage-1)*pageSize
const result5 = await ci.find({id:{$lt:100}}).skip(skipNums).limit(pageSize).sort(sortstr)
console.log('分页查询:'+result5)

const result6 = await writer.find({},{_id:0}).skip(skipNums).limit(pageSize)
console.log(result6)

}


query()

除了这些 mongoose支持在初始化Schema后添加一些静态方法,相当于添加原型链

const PostSchema = new Schema({
...
})
PostSchema.static = {
getList:function(options,sort,page,limit){
// options: 前台传递的 catelog : ask,isTop:0 等
return this.find(options).sort({[sort]:-1}).skip((page)*limit).limit(limit)
}
}

// 此外 还可以在初始化schema后添加中间件 对一些属性添加默认值

PostShcma.pre('save',async (next)=>{
this.create = moment().format('YYYY-MM-DD HH:mm:ss')
await next()
})

redis

开启redis服务

# 创建持久化redis服务
docker run -itd --restart= always --name redis-test -p 8271:6379 -v/home/redistest1:/data redis redis-server --requirepass 123456

# 本地开启
默认端口是:6379

.conf文件设置 密码
requirepass 123456
# 配置开启服务
.\redis-server.exe redis.windows.conf

redis相关命令

进入cli
docker exec -it redis-test /bin.bash

# 使用命令
redis-cli

# 输入密码
auth 123456

# 退出
quit

# 切换数据库 默认有16个
select 1

# 设置键值
set name test1

# 获取键值
get name

# keys test*
匹配test开头的键值

# 匹配键值是否存在
exists test

# 删除键值
del test
发布订阅

发布
subscribe imooc imooc1

订阅
publish imooc "hello"
// 输入完整命令 发布者会受到这个命令

node.js操作redis:strawberry::strawberry:

安装依赖redis,并配置相关

const redis = require('redis')
const options ={
host: '47.97.180.232',
port: 8271,
password: '123456',
detect_buffers: true,
// 来自官方
retry_strategy: function (options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with
// a individual error
return new Error('The server refused the connection')
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands
// with a individual error
return new Error('Retry time exhausted')
}
if (options.attempt > 10) {
// End reconnecting with built in error
return undefined
}
// reconnect after
return Math.min(options.attempt * 100, 3000)
}
}

操作redis

通过client操作redis一般分为设置键值,区间值,而值可能存在string或者Object两种,所以需要对其封装

// 创建客户端
const client = redis.createClient(options)

const setValue = (key,value,time)=>{
//去空
if(typeof value === 'undefined'|| value === ''|| value === null){
return
}
if(typeof value === 'string'){
// 设置时效性 秒为单位
if(time !== 'undefined'){
return client.set(key,value,'EX',time)
}
return client.set(key,value)
}else if(typeof value === 'object'){
// 对象{key1,:value1,key2:value2}
// hset(key,对象的key,对象的值,client.print) 对象的key可以通过Object.keys(value)获取
Object.keys(value).forEach((item)=>{
return client.hset(key,item,value[item],client.print)
})

}
}

// 获取值有些特殊 需要将方法转为promise进行操作

// 获取键值
const { promisify } = require('util')

const getValue = (key) => {
const getAsync = promisify(client.get).bind(client)(key)
return getAsync
}

// 获取对象的哈希值
const gethValue= (key)=>{
const getHAsync = promisify(client.hgetall).bind(client)(key)
return getHAsync
}

首页:heart:

前端部分技巧:fist_raised:

时间格式处理
filters:{
moment(date){
// 两个月之内 显示为几天前
if((moment(date).isBefore(moment().subtract(1,'months')){
return moment(date).format('YYYY-MM-DD HH:mm:ss')
}else{
return moment(date).formNow()
}
}
}

##### 使用过滤器
{item.created | moment}
加载更多基本逻辑

data:{
return(){
isRepeat:false,
list:[],
isEnd:false
}
}
options:{
page:this.page,
limit:this.limit
}
async getlist(){
// 是否存在重复请求
if(isRepeat){
return
}
if(this.End){
return
}
this.repeat = true
const res =await Public.getlist(options)
this.repeat = false
if(res.code === 200){
if(this.list.length === 0){
this.list = res.data
}else{
this.list = this.list.concat(res.data)
}
// 最后一页
if(res.data.length < options.limit){
this.isEnd = true
}
}


}

next(){
this.page ++
this.getlist(options)
}
组件和路由拆分

抽离组件

  1. 头部导航 挂载APP

  2. panel 分类面板组件 控制路由

// 相关路由配置
{
path:'/',
component:Home,
children:[
// 分类动态路由和默认的路由
{
path:'',
name:'index',
component:Index
},
{
path:'/index/:categroy',
name:'catelog',
component:Template1
}
]
}
  1. sort排序 tab面板 请求数据,不改变路由

  2. content 长列表面板

  3. 使用iview-LoadingBar组件优化路由跳转

    router入口文件

    router.beforeEach((to,from,next)=>{
    LoadingBar.start()
    next()
    })
    router.afterEach((to,from)=>{
    LoadingBar.finish()
    })
qs库的作用处理get请求url传对象参数解析

options = {
page :0,
limit:20
}
qs.Stringify(options) // page=0&limit=20 这个库可以将对象转换为url参数形式 常用在get请求携带参数上

后端模型和开发细节:articulated_lorry:

文章模型  定义schema 联合user表查询 获取文章列表(筛选) 通过id查询文章 通过uid查询文章列表  通过uid查询文章数量
用户模型 定义schema 添加唯一索引 保存的时候设置默认常见时间 updated的时候也定义时间 过滤敏感数据 定义捕获重复异常 删除注册前面的异常判断

编写,获取列表数据的接口 引入校验器
文章详情接口 引入校验器
本周热议接口 (筛选出七天内按answer数倒叙排序的数据)
友情链接接口

首页后端开发细节概要

定义模型时 规范定义 例如文章
const PostSchema = new Schema({
// 连表查询
userInfo: { type: String, ref: 'users' },
title: { type: String },
content: { type: String },
created_time: { type: Date,default:moment().format('x')},
updated_time:{type:Date,default:moment().format('x')},
category: { type: String },
fav: { type:Number },
isEnd: { type: Number,default:0,enum:[0,1] }, // 是否结贴
reads: { type: Number, default: 0 },
answer: { type: Number, default: 0 },
status: { type: Number,default:0,enum:[0,1]}, // 是否打开回复
isTop: { type: Number,default:0,enum:[0,1] }, // 是否置顶
sort: { type: String, default: 100 }, // 随着数值的减小,按照数值的倒序进行排列
tags: {
type: Array,
default: [
// { name: '', class: '' }
]
}
})
  • 连表查询 : 父表需要有ref指定字表,字表需要定义唯一索引

    //父表
    userInfo{
    type:String,ref:"users"
    }

    // 子表
    userName:{
    type:String,
    index:{
    unique:true
    },
    // 当集合没有userName 不执行查询
    sparse: true
    }

    // 查询时附带字表相关内容
    userModel.find(...).populate({
    // 指定替代
    'path':"userInfo",
    // 指定返回的字段
    'select':"isvip avatar name"
    })
多种查询条件查询 最好定义静态方法
// 添加静态方法
PostSchema.statics = {
getList: function (options, sort, page, limit) {
return this.find(options)
.sort({ [sort]: -1 }) // -1:代表倒序排列
.skip(page * limit) // 跳过多少页
.limit(limit) // 取多少条数据
.populate({
path: 'userInfo', // 要替换的字段是uid
/**
* 联合查询的时候,有些字段是需要的,有些字段是不需要的。
* 可以通过select筛选出我们需要的字段,去隐藏敏感字段。
*/
select: 'name isVip avatar'
})
}
}
利用校验器 设置默认值
const options = {}
if (typeof body.tag !== 'undefined' && body.tag !== '') {
options.tags = { $elemMatch: { name: body.tag } }
}
options.category =
v.get('query.category') === null ? 'index' : v.get('query.category')
options.isEnd = v.get('query.isEnd') === null ? 0 : v.get('query.isEnd')
options.status = v.get('query.status') === null ? 0 : v.get('query.status')
options.isTop = v.get('query.isTop') === null ? 0 : v.get('query.isTop')
const sort =
v.get('query.sort') === null ? 'created_time' : v.get('query.sort')
const page = v.get('query.page') === null ? 0 : v.get('query.page')
const limit = v.get('query.limit') === null ? 20 : v.get('query.limit')

const result = await Post.getList(options, sort, page, limit)
利用mongose的钩子函数 在查询语句执行前执行操作
// 设置调用save和update 保存的属性
UserSchema.pre('update',(next)=>{
this.updated = moment().format('x')
this.login_time = moment().format('x')
next()
})

// 过滤重复的userName和重复的Name
UserSchema.post('save', function (error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('Error: Mongoose has a duplicate key.'))
} else {
next(error)
}
})
防止重复点击
x:{
index:0
}
selectType (item) {
this.currentIndex1 = item.key
if (this.currentIndex1 !== this.x.index) {
this.x.index = item.key
console.log('success')
}
},
当数据有很多种分类,但是默认是返回全部分类的数据时,需要删除掉默认对象的值
const options = {}
// 默认,不传type 全站
const type =v.get('query.type')
if(type !== null && type !== 0){
options.type = v.get('query.type')
}else{
if(type == null || type===0){
// 不传的时候 type=null
delete options.type
}
}

个人中心页面和设置:bust_in_silhouette:

前端细节:imp::imp::imp::imp:

头像区菜单项显隐原理

当鼠标移入头像区域或者移入菜单li区域时触发显示,当鼠标移出时触发隐藏事件,但是考虑到头像区域和菜单区域有间距,必须要设置定时器触发事件防止显示事件立即隐藏

show(){
seTimeout(()=>{
this.IsShow = true
},200)
},
hide(){
seTimeout(()=>{
this.isShow = false
},500)
}
路由拆分

个人中心路由

>{
// 显示信息分为:全部 文章 讨论 分享 求助 收藏集 点过赞的帖子 关注列表 粉丝
path:'/user/:uid/:type',
name:'UserCenter',
components:'UserCenter'
>},
>{
// 个人设置 profile 个人资料 password 密码修改 账号关联 account
path:'/setting/:type'
>}
组件划分

个人中心组件:

  • userBanner,路由是当前登录用户的uid时,显示编辑资料按钮,不是则显示关注按钮

  • tabs,切换不同的typetab组件,切换路由监听路由的变化加载不同的请求数据,需要提供全部 文章 讨论 分享 求助 收藏集 点过赞的帖子 关注列表 粉丝

  • user-postContent,文章列表组件

  • userInfo组件: 显示关注了多少人,粉丝数量,获取点赞数量,签到天数,个人积分

创建路由守卫在用户未登录状态不允许进入用户设置页面:imp::imp:
{
path:'/setting/:type',
name:'setting',
component: settting,
beforeEach((to,from,next)=>{
const userInfo = parse(localStroage.getItem('userInfo'))
if(!userInfo){
next('/login')
}else{
next()
}
})
}


// 优化版本 创建全局路由守卫 增加meta标签

{
path:'/setting/:type',
name:'setting',
component:setting,
meta:{requireAuth:true}
}

router.beforeEach((to,from,next)=>{
const userInfo = localStorage.getItem('userInfo')
const token = localstorage.getItem('token')
// 官方写法 判断路由元信息 是否需要鉴权
if(to.matched.some(record=>record.meta.requireAuth)){
// jwt令牌合法校验
const isValid = await Token.isValid(token).result
if(!userInfo || userInfo && !Valid){
localStorage.clear()
next('/login')
}else{
next()
}
}else{
// 不需鉴权
next()
}
})

sessionStoragelocalstorage区别

sessionStorage 是指当会话存在时的缓存,当关闭浏览器或者tab,缓存会清空

localstorage指保存在本地的缓存

添加404导航页面

router配置文件最后一行添加这样的路由配置

{
path:‘/404’,
name:'NotFound'
components:NotFound,
},
// 所有不属于路由配置的路由都导向404
{ path: '*', redirect: '/404', hidden: true }
上传头像

原生做法

<input id="pic" type="file" name="file" accept="image/png.image/jpg,image/gif" @change="changeImage" />

<script>
export default{
data:{
return (){
pic:'',
formData:''
}
},
method:{
changeImage(e){
// 上传图片
let file = e.target.files
const formData = new FormData()
if(file.length>0){
// 添加表单 键值序列
formData.append('file',files[0])
this.formData = formData
}
// 调用服务器接口
user.uploadImg(this.formData)

// 然后调用更新用户信息接口

}
}
}
</script>

<style lang="scss">
#pic
display:none;
</style>

后端接口:kiss:

获取用户基本信息
async getUserInfo(ctx){
const uid = ctx,request.query.uid
const result = await User.findOne({_id:uid},{password:0,mobile:0})
if(result === null){
ctx.body = {
msg:'找不到该用户信息',
code:500
}
}else{
ctx.body = {
msg:'获取用户信息成功',
code:200,
data:result
}
}
}
修改基本信息
async updateUserInfo(ctx){
const body = ctx.request.body
const AuthHeader = ctx.request.header.authorization
const playload = await getJwtPlayLoad(AuthHeader)
const user = await User.findOne({_id:playload._id})

// 不加入更改邮箱权限
// 过滤没必要的更改信息
const SkipInfo = ['password','mobile']
SkipInfo.map((item)=>{
delete body[item]
})
// 更改信息
const result = await User.update({_id:playload._id},body)
if(result.ok === 1 && result.n === 1){
ctx.body = {
code:200,
msg:'更新用户信息成功'
}
}else{
ctx.body = {
code:500,
msg:'发生异常,更新失败'
}
}

}
修改邮箱绑定

发送一封确认更改邮箱绑定的邮箱

const token = jwt.sign({_id:playload._id},JWT-SERCERT,{ expiresIn: 60 * 30 })
// 缓存在redis中
const key = uuid()
setValue(key,token)
await send({
expire:moment().add(30,'minutes').format('YYYY-MM-DD HH:mm:ss'),
email:user.userName,
name:user.name ,
userName:body.userName,
key
})

然后邮箱的重置链接需要携带数据-跳转的页面路由

ep:localhost:8080/resetEmail?key=uuid()&userName=email

最后需要定义用户修改邮箱的接口

async updateUserName(ctx){
const v = await new UserNameValidator().validate(ctx)
const key = v.get('query.key')
const userName = v.get('query.userName')
// 获取redis数据 获取_id
if(key){
const token = await getValue(key)
const playload = await getJwtplayLoad('Bearer '+token)
// 判断邮箱是否存在
const user = await User.finOne({userName})
if(user && user.password){
throw new UserExistedException('邮箱已存在',200)
}else{
await User.updateOne({_id:playload._id},{userName})
ctx.body = {
code:200,
msg:'换绑成功'
}
}
}

}
修改密码
async modifyPassword(ctx){
const playload = await getJwtPlayload(ctx.request.header.authorization)
const v = await new PasswordValidator().validate(ctx)
const password = v.get('body.password')
const newPassword = v.get('body.newPassword')
if(password === newPassword){
throw new Exception('两次密码不能相同')
}else{
const user = await User.findOne({_id:playload._id})
// 比对旧密码是否正确
const checkPassword = bcript.compareSync(password,user.password)
if(checkPassword){
const sault = bcryptjs.genSaltSync(10)
const pwd = bcryptjs.hashSync(newPassword,sault)
await User.updateOne({_id:playload._id},{
password:pwd
})
ctx.body = {
code:200,
msg:'修改密码成功'
}
}else{
ctx.body = {
code:500,
msg:'原密码不正确'
}
}
}
}
上传文件

定义上传的路径

const uploadPath =  `${pwd.process()}/static`

Node.js判断路径是否目录存在 :statis.isDirectory()

const fs = require('fs')
const mkdir = require('make-dir')
async upload(ctx){
// append的键
const file = ctx.request.files.file
// 获取图片名称 图片格式 图片的路径
const ext = file.spilt('.')[1]
// 获取当前文件的目录 没有则创建 分时间创建目录
const dir = `${updatePath}/${moment().format('YYYYMMDD')}`
// 创建目录
await mkdir(dir)

// 读写文件
const picname = uuid()
// 文件真实路径
const realPath = `${dir}/${picname}.${ext}`
// 读取上传的文件的流
const reader = fs.createReadStream(file.path)
// 写入目录中
const upStream = fs.createdWriteStream(realPath)
reader.pipe(upstream)

ctx.body = {
code:200,
msg:'图片上传成功',
data:{
path:realPath
}
}

}

判断目录是否存在,如果觉得麻烦就直接用make-dir第三方库

// D:User/1212/121.jpg

// 传入路径 获取文件状态 node.js的api fs.stats 查看文件状态,转换为promise
const getStats = (path)=>{
return new Promise((resolve)=>{
fs.stat(path,(err,stats)=>{
if(err){
// 获取失败 不是文件
resove(false)
}else{
// 获取成功
resolve(stats)
}
})
})
}
// 判断是否是目录 如果不是就创建目录
const dirExists = async(dir)=>{
const isExisted = await getStats(dir)
// 如果文件信息存在 分目录或者文件
if(isExisted && isExisted.isDirectory()){
return true
}else if(isExisted){
return false
}
// 如果不存在 创建目录


}

签到系统

设计签到记录表

字段 意义
created_time 创建时间
last_signTime 上一次签到时间
user 用户id
favs 当前签到积分

签到系统基本逻辑

查询用户签到信息

  • 是否是连续签到 ,连表查询出user中的count和总积分favs
  • sign_record只记录用户单次签到的情况
  • 无签到记录

    一次积分是5,然后保存当前的记录表,更新用户表总积分和连续签到数量。

  • 如果当前用户有签到记录

    二种情况:

    • 上一次签到时间是今天,此时抛出异常

    • 上一次签到时间不是今天

      • 上一次签到时间是昨天

        此时先将用户连续签到的数量获取 然后设置积分逻辑,更新用户的count和总积分

      • 间隔时间签到

        此时一次签到积分又变回5,用户连续签到天数设置为1

具体实现

定义Schema

const SignRecordSchema = new Schema({
created_time:Date,
favs:NUmber,
user:{
type:String,
ref:'users'
}
})

SignRecordSchema.pre('save',function(next){
this.created_time = moment().format('YYYY-MM-DD HH:mm:ss')
next()
})

// 查询出最新的一条签到记录
SignRecordSchema.statics = {
findByUid : function(uid){
return this.findOne({user:uid}).sort(created_time:-1)
}
}

签到逻辑具体

获取jwt身份验证信息

async getJwtPlayLoad(AuthHeader){
// Authorization : Bearer token
return await jwt.verify(AuthHeader.split(' ')[1],JWT_SERVET)
}

积分签到

const SignRecord = require('../models/sign_record')
async Sign(ctx){
const res = await getJwtPlayLoad(ctx.request.header.authorization)
// 查询用户和签到记录
const record = await SignRecord.findByUid(res._id)
const signUser = await User.findOne({_id:res._id})
let data = {}
if(record!==null){
// 有签到记录
if(moment(record.created_time).format('YYYY-MM-DD')=== moment().format('YYYY-MM-DD')){
throw new AllExistedError('今日已经签到',200)
}else{
let fav
// 连续签到 积分逻辑
if(moment(record.created_time).format('YYYY-MM-DD') === moment().substr(1,'days').format('YYYY-MM-DD')){
const count = signUser.count +1
if(count<5){
fav = 5
}else if(count>=5&&count<15){
fav =10
}
await User.updateOne({
_id:res._id
},{
$set:{count:count},
$inc:{favs:fav}
})
data = {
favs:signUser.favs+SignFav,
count
}

}else{
// 不是连续签到
fav = 5
await signUser.update({_id:res.id},{
$set:{count:1},
$inc:{favs:fav}
})
data = {
favs:signUser.favs+5,
count:1
}
}}

}else{
// 无签到记录
let fav = 5
signUser.update({_id:res._id},{
$set:{count:1},
$inc:{favs:fav}
})
}
}

前端部分逻辑

每次取缓存中的userInfo数据,保存在变量userObject,如果缓存中有`count`就为count,否则为0
fav 签到接口调用的数据

签到 修改userObject的count和isSign状态

count: 0

const userInfo = parse(localstorage.userInfo)
if(userInfo){
this.userObject = userInfo
}else{
this.count =0
}

count(){
if(this.userObject.userName){
return this.userObject.count
}else{
return 0
}
}

sign(){
if(!this.userObject.userName){
this.$message.error('需要登录才能使用')
}else{
cosnt res = await User.sign()
if(res.code=200){
this.userObject.count = res.data.count
this.fav = re.data.fav
this.isSign = true
}else{
this,$message.error(res.msg)
}
}
}