1创建项目和基础配置

创建项目

安装egg.js
全局切换镜像:

npm config set registry https://registry.npm.taobao.org

我们推荐直接使用脚手架,只需几条简单指令,即可快速生成项目(npm >=6.1.0):

mkdir egg-example && cd egg-example
npm init egg --type=simple --registry https://registry.npm.taobao.org
npm i

启动项目:

npm run dev
open http://localhost:7001

关闭csrf开启跨域

安装

npm i egg-cors --save

配置插件

// {app_root}/config/plugin.js
cors:{
  enable: true,
  package: 'egg-cors',
},
config / config.default.js 目录下配置
  config.security = {
    // 关闭 csrf
    csrf: {
      enable: false,
    },
     // 跨域白名单
    domainWhiteList: [ 'http://localhost:3000' ],
  };
  // 允许跨域的方法
  config.cors = {
    origin: '*',
    allowMethods: 'GET, PUT, POST, DELETE, PATCH'
  };

2全局抛出异常处理

// app/middleware/error_handler.js

module.exports = (option, app) => {
    return async function errorHandler(ctx, next) {
      try {
        await next(); 
        // 404 处理
        if(ctx.status === 404 && !ctx.body){
           ctx.body = { 
               msg:"fail",
               data:'404 错误'
           };
        }
      } catch (err) {
        // 记录一条错误日志
        app.emit('error', err, ctx);

        const status = err.status || 500;
        // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
        const error = status === 500 && app.config.env === 'prod'
          ? 'Internal Server Error'
          : err.message;

        // 从 error 对象上读出各个属性,设置到响应中
        ctx.body = { 
            msg:"fail",
            data:error
        };
        ctx.status = status;
      }
    };
  };

// config/config.default.js

config.middleware = ['errorHandler'];

3封装api返回格式扩展

// app/extend/context.js

module.exports = {
  // 成功提示
  apiSuccess(data = '', msg = 'ok', code = 200) {
    this.body = { msg, data };
    this.status = code;
  },
  // 失败提示
  apiFail(data = '', msg = 'fail', code = 400) {
    this.body = { msg, data };
    this.status = code;
  },
};

4sequelize数据库和迁移配置

数据库配置

安装并配置egg-sequelize插件(它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上)和mysql2模块:

npm install --save egg-sequelize mysql2

在config/plugin.js中引入 egg-sequelize 插件

exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
};

在config/config.default.js

config.sequelize = {
    dialect:  'mysql',
    host:  '127.0.0.1',
    username: 'root',
    password:  'root',
    port:  3306,
    database:  'egg-wechat',
    // 中国时区
    timezone:  '+08:00',
    define: {
        // 取消数据表名复数
        freezeTableName: true,
        // 自动写入时间戳 created_at updated_at
        timestamps: true,
        // 字段生成软删除时间戳 deleted_at
        // paranoid: true,
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        // deletedAt: 'deleted_at',
        // 所有驼峰命名格式化
        underscored: true
    }
};

迁移配置

sequelize 提供了sequelize-cli工具来实现Migrations,我们也可以在 egg 项目中引入 sequelize-cli。

npm install --save-dev sequelize-cli

egg 项目中,我们希望将所有数据库 Migrations 相关的内容都放在database目录下,所以我们在项目根目录下新建一个.sequelizerc配置文件:

'use strict';

const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};

初始化 Migrations 配置文件和目录

npx sequelize init:config
npx sequelize init:migrations
// npx sequelize init:models

行完后会生成database/config.json文件和database/migrations目录,我们修改一下database/config.json中的内容,将其改成我们项目中使用的数据库配置:

{
  "development": {
    "username": "root",
    "password": null,
    "database": "eggapi",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+08:00"
  }
}

创建数据库

npx sequelize db:create
# 升级数据库
npx sequelize db:migrate
# 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
# npx sequelize db:migrate:undo
# 可以通过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all

模型关联

User.associate = function(models) {
   // 关联用户资料 一对一
   User.hasOne(app.model.Userinfo);
   // 反向一对一关联
   // Userinfo.belongsTo(app.model.User);
   // 一对多关联
   User.hasMany(app.model.Post);
   // 反向一对多关联
   // Post.belongsTo(app.model.User);
   // 多对多
   // User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId' })
   // 反向多对多
   // Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey: 'projectId' })
}

5用户表设计和迁移

数据表设计和迁移

创建数据迁移表

npx sequelize migration:generate --name=user

1.执行完命令后,会在database / migrations / 目录下生成数据表迁移文件,然后定义

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, STRING, DATE, ENUM } = Sequelize;
    // 创建表
    await queryInterface.createTable('user', {
      id: {
        type: INTEGER(20).UNSIGNED,
        primaryKey: true,
        autoIncrement: true
      },
      username: {
        type: STRING(30),
        allowNull: false,
        defaultValue: '',
        comment: '用户名称',
        unique: true
      },
      nickname: {
        type: STRING(30),
        allowNull: false,
        defaultValue: '',
        comment: '昵称',
      },
      email: {
        type: STRING(160),
        comment: '用户邮箱',
        unique: true
      },
      password: {
        type: STRING(200),
        allowNull: false,
        defaultValue: ''
      },
      avatar: {
        type: STRING(200),
        allowNull: true,
        defaultValue: ''
      },
      phone: {
        type: STRING(20),
        comment: '用户手机',
        unique: true
      },
      sex: {
        type: ENUM,
        values: ['男', '女', '保密'],
        allowNull: true,
        defaultValue: '男',
        comment: '用户性别'
      },
      status: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 1,
        comment: '状态'
      },
      sign: {
        type: STRING(200),
        allowNull: true,
        defaultValue: '',
        comment: '个性签名'
      },
      area: {
        type: STRING(200),
        allowNull: true,
        defaultValue: '',
        comment: '地区'
      },
      created_at: DATE,
      updated_at: DATE
    });
  },

  down: async queryInterface => {
    await queryInterface.dropTable('user');
  }
};

执行 migrate 进行数据库变更

npx sequelize db:migrate

6注册功能实现

新建user.js控制器

// app/controller/user.js
'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller{
    // 注册
    async reg(){
        let {ctx,app} = this;
       // 参数验证
       let {username,password,repassword} = this.ctx.request.body;
       // 验证用户是否已存在
       if(await app.model.User.findOne({
           where:{
               username
           }
       })){
           ctx.throw(400,'用户名已存在');
       }
       // 创建用户
       await app.model.User.create({
           username,
           password
       })
       if(!user){
           ctx.throw(400,'创建用户失败');
       }
       ctx.apiSuccess(user);
    //   this.ctx.body ='注册';   
    }
}


module.exports = UserController;

新建user.js数据迁移文件

// app/model/user.js
'use strict';
module.exports = app => {
    const { STRING, INTEGER, DATE, ENUM, TEXT } = app.Sequelize;
    // 配置(重要:一定要配置详细,一定要!!!)
    const User = app.model.define('user', {
        id: {
            type: INTEGER(20).UNSIGNED,
            primaryKey: true,
            autoIncrement: true
        },
        username: {
            type: STRING(30),
            allowNull: false,
            defaultValue: '',
            comment: '用户名称',
            unique: true
        },
        nickname: {
            type: STRING(30),
            allowNull: false,
            defaultValue: '',
            comment: '昵称',
        },
        email: {
            type: STRING(160),
            comment: '用户邮箱',
            unique: true
        },
        password: {
            type: STRING(200),
            allowNull: false,
            defaultValue: ''
        },
        avatar: {
            type: STRING(200),
            allowNull: true,
            defaultValue: ''
        },
        phone: {
            type: STRING(20),
            comment: '用户手机',
            unique: true
        },
        sex: {
            type: ENUM,
            values: ['男', '女', '保密'],
            allowNull: true,
            defaultValue: '男',
            comment: '用户性别'
        },
        status: {
            type: INTEGER(1),
            allowNull: false,
            defaultValue: 1,
            comment: '状态'
        },
        sign: {
            type: STRING(200),
            allowNull: true,
            defaultValue: '',
            comment: '个性签名'
        },
        area: {
            type: STRING(200),
            allowNull: true,
            defaultValue: '',
            comment: '地区'
        },
        created_at: DATE,
        updated_at: DATE
    });
    return User;
};

注册路由

   // 用户注册
  router.post('/reg',controller.user.reg);

下图是我测试的截图

在这里插入图片描述

7参数验证功能实现(一)

参数验证

插件地址:
https://www.npmjs.com/package/egg-valparams

安装

npm i egg-valparams --save

配置

// config/plugin.js
valparams : {
  enable : true,
  package: 'egg-valparams'
},
// config/config.default.js
config.valparams = {
    locale    : 'zh-cn',
    throwError: true
};

在控制器里使用

class XXXController extends app.Controller {
  // ...
  async XXX() {
    const {ctx} = this;
    ctx.validate({
      system  : {type: 'string', required: false, defValue: 'account', desc: '系统名称'},
      token   : {type: 'string', required: true, desc: 'token 验证'},
      redirect: {type: 'string', required: false, desc: '登录跳转'}
    });
    // if (config.throwError === false)
    if(ctx.paramErrors) {
      // get error infos from `ctx.paramErrors`;
    }
    let params = ctx.params;
    let {query, body} = ctx.request;
    // ctx.params        = validater.ret.params;
    // ctx.request.query = validater.ret.query;
    // ctx.request.body  = validater.ret.body;
    // ...
    ctx.body = query;
  }
  // ...
}
// app/controller/user.js
'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller{
    // 注册
    async reg(){
        let {ctx,app} = this;
       // 参数验证
       ctx.validate({
          username:{type: 'string', required: true,range:{min:10,max:20},desc: '用户名'},
          password:{type: 'string', required: true, desc: '密码'},
          repassword:{type: 'string', required: true, desc: '确认密码'}
        },{
            equals:[
                ['password','repassword']
            ]
        });
       let {username,password,repassword} = this.ctx.request.body;
       // 验证用户是否已存在
       if(await app.model.User.findOne({
           where:{
               username
           }
       })){
           ctx.throw(400,'用户名已存在');
       }
       // 创建用户
       await app.model.User.create({
           username,
           password
       })
       if(!user){
           ctx.throw(400,'创建用户失败');
       }
       ctx.apiSuccess(user);
    //   this.ctx.body ='注册';   
    }
}


module.exports = UserController;

ValParams API 说明

参数验证处理
Valparams.setParams(req, params, options);

Param Type Description Example
在这里插入图片描述

8参数验证功能实现(二)

修改 app/middleware/error_handler.js

// app/middleware/error_handler.js
module.exports = (option, app) => {
    return async function errorHandler(ctx, next) {
      try {
        await next(); 
        // 404 处理
        if(ctx.status === 404 && !ctx.body){
          ctx.body = { 
              msg:"fail",
              data:'404 错误'
          };
        }
      } catch (err) {
        // 记录一条错误日志
        app.emit('error', err, ctx);

        const status = err.status || 500;
        // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
        let error = status === 500 && app.config.env === 'prod'
          ? 'Internal Server Error'
          : err.message;

        // 从 error 对象上读出各个属性,设置到响应中
        ctx.body = { 
            msg:"fail",
            data:error
        };
        if(status === 422 && err.message === 'Validation Failed'){
         // 添加判断条件
            if(err.errors && Array.isArray(err.errors)){
                error = err.errors[0].err[0];
            }
            ctx.body = { 
                msg:"fail",
                data:error
            };
        }
        ctx.status = status;
      }
    };
  };

修改app/controller/user.js

'use strict';

const Controller = require('egg').Controller;

class UserController extends Controller{
    // 注册
    async reg(){
        let {ctx,app} = this;
       // 参数验证
       ctx.validate({
          username:{type: 'string', required: true,range:{min:5,max:20},desc: '用户名'},
          password:{type: 'string', required: true, desc: '密码'},
          repassword:{type: 'string', required: true, desc: '确认密码'}
        },{
            equals:[
                ['password','repassword']
            ]
        });
       return  this.ctx.body = 123;
       let {username,password} = ctx.request.body;
       // 验证用户是否已存在
       if(await app.model.User.findOne({
           where:{
               username
           }
       })){
           ctx.throw(400,'用户名已存在');
       }
       // 创建用户
       await app.model.User.create({
           username,
           password
       })
       if(!user){
           ctx.throw(400,'创建用户失败');
       }
       ctx.apiSuccess(user);
    //   this.ctx.body ='注册';   
    }
}


module.exports = UserController;

下图是我测试的截图

在这里插入图片描述

9crypto 数据加密

crypto 数据加密

安装

npm install crypto --save

配置文件配置 config / config.default.js

config.crypto = {
    secret:  'qhdgw@45ncashdaksh2!#@3nxjdas*_672'
};

使用

// 引入

const crypto = require('crypto');

// 加密

async createPassword(password) {
    const hmac = crypto.createHash("sha256", app.config.crypto.secret);
    hmac.update(password);
    return hmac.digest("hex");
}

// 验证密码

async checkPassword(password, hash_password) {
    // 先对需要验证的密码进行加密
    password = await this.createPassword(password);
    return password === hash_password;
}

10用户登录功能

首先我们在app/controller/user.js中写入

在文件头部引入

const crypto = require('crypto');

然后,在下面写入方法

// 登录
    async login(){
        const {ctx,app} = this;
        // 参数验证
         ctx.validate({
          username:{type: 'string', required: true,desc: '用户名'},
          password:{type: 'string', required: true, desc: '密码'},
        });
        let {username,password} = ctx.request.body;
        //  验证用户是否已存在 验证用户状态是否禁用
        let user = await app.model.User.findOne({
            where:{
                username,
                status:1
            }
        });
        if(!user){
            ctx.throw(400,'用户不存在或用户已被禁用');
        };
        // 验证密码
        await this.checkPassword(password,user.password);
        // 生成token
        // 加入到缓存
        // 返回用户信息和token
        return ctx.apiSuccess(user);
    }
    
    // 验证密码
    async checkPassword(password, hash_password) {
        // 先对需要验证的密码进行加密
        const hmac = crypto.createHash("sha256", this.app.config.crypto.secret);
        hmac.update(password);
        password = hmac.digest("hex");
        let res = password === hash_password;
        if(!res){
            this.ctx.throw(400,'密码错误');
        }
        return true;
    }

然后我注册路由

  // 登录
  router.post('/login',controller.user.login);

接着就测试,下图是我测试的,供大家参考。

在这里插入图片描述

11jwt 加密鉴权

插件地址:

https://www.npmjs.com/package/egg-jwt

安装

npm i egg-jwt --save

配置

// {app_root}/config/plugin.js
exports.jwt = {
  enable: true,
  package: "egg-jwt"
};

// {app_root}/config/config.default.js
exports.jwt = {
  secret: 'qhdgw@45ncashdaksh2!#@3nxjdas*_672'
};

生成token

// 生成token
getToken(value) {
    return this.app.jwt.sign(value, this.config.jwt.secret);
}

验证token

try {
    user = app.jwt.verify(token, app.config.jwt.secret)
} catch (err) {
    let fail = err.name === 'TokenExpiredError' ? 'token 已过期! 请重新获取令牌' : 'Token 令牌不合法!';
    return ctx.apiFail(fail);
}

app/controller/user.js

// 登录
    async login(){
        const {ctx,app} = this;
        // 参数验证
         ctx.validate({
          username:{type: 'string', required: true,desc: '用户名'},
          password:{type: 'string', required: true, desc: '密码'},
        });
        let {username,password} = ctx.request.body;
        //  验证用户是否已存在 验证用户状态是否禁用
        let user = await app.model.User.findOne({
            where:{
                username,
                status:1
            }
        });
        if(!user){
            ctx.throw(400,'用户不存在或用户已被禁用');
        };
        // 验证密码
        await this.checkPassword(password,user.password);
        
        user = JSON.parse(JSON.stringify(user));
     
        // 生成token
        let token = ctx.getToken(user);
        user.token = token;
        delete user.password;
        
        // 加入到缓存
        // 返回用户信息和token
        return ctx.apiSuccess(user);
    }

下面是我测试的结果

在这里插入图片描述

12 redis 缓存插件和封装

安装

npm i egg-redis --save

配置

// config/plugin.js
exports.redis = {
  enable: true,
  package: 'egg-redis',
};

// redis存储
config.redis = {
    client: {
        port: 6379,          // Redis port
        host: '127.0.0.1',   // Redis host
        password: '',
        db: 2,
    },
}

缓存库封装

// app/service/cache.js
'use strict';

const Service = require('egg').Service;

class CacheService extends Service {
    /**
     * 获取列表
     * @param {string} key 键
     * @param {boolean} isChildObject 元素是否为对象
     * @return { array } 返回数组
     */
    async getList(key, isChildObject = false) {
        const { redis } = this.app
        let data = await redis.lrange(key, 0, -1)
        if (isChildObject) {
            data = data.map(item => {
                return JSON.parse(item);
            });
        }
        return data;
    }
    /**
     * 设置列表
     * @param {string} key 键
     * @param {object|string} value 值
     * @param {string} type 类型:push和unshift
     * @param {Number} expir 过期时间 单位秒
     * @return { Number } 返回索引
     */
    async setList(key, value, type = 'push', expir = 0) {
        const { redis } = this.app
        if (expir > 0) {
            await redis.expire(key, expir);
        }
        if (typeof value === 'object') {
            value = JSON.stringify(value);
        }
        if (type === 'push') {
            return await redis.rpush(key, value);
        }
        return await redis.lpush(key, value);
    }

    /**
     * 设置 redis 缓存
     * @param { String } key 键
     * @param {String | Object | array} value 值
     * @param { Number } expir 过期时间 单位秒
     * @return { String } 返回成功字符串OK
     */
    async set(key, value, expir = 0) {
        const { redis } = this.app
        if (expir === 0) {
            return await redis.set(key, JSON.stringify(value));
        } else {
            return await redis.set(key, JSON.stringify(value), 'EX', expir);
        }
    }

    /**
     * 获取 redis 缓存
     * @param { String } key 键
     * @return { String | array | Object } 返回获取的数据
     */
    async get(key) {
        const { redis } = this.app
        const result = await redis.get(key)
        return JSON.parse(result)
    }

    /**
     * redis 自增
     * @param { String } key 键
     * @param { Number } value 自增的值 
     * @return { Number } 返回递增值
     */
    async incr(key, number = 1) {
        const { redis } = this.app
        if (number === 1) {
            return await redis.incr(key)
        } else {
            return await redis.incrby(key, number)
        }
    }

    /**
     * 查询长度
     * @param { String } key
     * @return { Number } 返回数据长度
     */
    async strlen(key) {
        const { redis } = this.app
        return await redis.strlen(key)
    }

    /**
     * 删除指定key
     * @param {String} key 
     */
    async remove(key) {
        const { redis } = this.app
        return await redis.del(key)
    }

    /**
     * 清空缓存
     */
    async clear() {
        return await this.app.redis.flushall()
    }
}

module.exports = CacheService;

缓存库使用

// 控制器
await this.service.cache.set('key', 'value');
// app/controller/user.js
// 加入到缓存
if(!await this.service.cache.set('user_'+user.id,token)){
    ctx.throw(400,'登录失败');
}

13全局权限验证中间件实现(一)

首先我们需要在config.default.js中修改

  // add your middleware config here
  config.middleware = ['errorHandler','auth'];
  
config.auth = {
  ignore:['/reg','/login'] 
};
  

接着我们在app/middleware文件夹下新建auth.js
文件内容如下

module.exports = (option, app) => {
    return async (ctx, next) => {
        //1. 获取 header 头token
        const { token } = ctx.header;
        if (!token) {
            ctx.throw(400, '您没有权限访问该接口!');
        }
        ......
    }
}

14全局权限验证中间件实现(二)

完善auth.js

//2. 根据token解密,换取用户信息
        let user = {};
        try {
            user = ctx.checkToken(token);
        } catch (error) {
            let fail = error.name === 'TokenExpiredError' ? 'token 已过期! 请重新获取令牌' : 'Token 令牌不合法!';
            ctx.throw(400, fail);
        }
        //3. 判断当前用户是否登录
        let t = await ctx.service.cache.get('user_' + user.id);
        if (!t || t !== token) {
            ctx.throw(400, 'Token 令牌不合法!');
        }

        //4. 获取当前用户,验证当前用户是否被禁用
        user = await app.model.User.findByPk(user.id);
        if (!user || user.status == 0) {
            ctx.throw(400,'用户不存在或已被禁用');
        }
        // 5. 把 user 信息挂载到全局ctx上
        ctx.authUser = user;

        await next();

app/controller/user.js

 // 退出登录
    async logout(){
        console.log(this.ctx.authUser);
        this.ctx.body = '退出登录';
    }

下面是我的截图

在这里插入图片描述

15退出登录功能

app/controller/user.js

// 退出登录
    async logout(){
        const {ctx,service} = this;
        // 拿到当前用户
        let current_user_id = ctx.authUser.id;
        // 移除redis当前用户信息
        if(!await service.cache.remove('user_'+current_user_id)){
            ctx.throw(400,'退出登录失败');
        }
        ctx.apiSuccess('退出成功');
    }

下面是我测试的截图

在这里插入图片描述
由于测试了两次,这是第二次的结果

16搜索用户功能

router.js

// 搜索用户
  router.post('/search/user',controller.search.user);

app/controller/search.js

'use strict';

const Controller = require('egg').Controller;
const crypto = require('crypto');
class SearchController extends Controller{
    // 注册
    async user(){
        let {ctx,app} = this;
 
       // 参数验证
        ctx.validate({
          keyword:{type: 'string', required: true,desc: '关键词'},
        });
        let {keyword} = ctx.request.body;
        let data = await app.model.User.findOne({
            where:{
                username:keyword
            },
            // 隐藏字段
            attributes:{
                exclude:['password']
            }
        });
        ctx.apiSuccess(data);
    }
}

module.exports = SearchController;

下图是我测试的接口

在这里插入图片描述

17好友表和好友申请表设计

npx sequelize migration:generate --name=friend
npx sequelize migration:generate --name=apply

好友表

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, DATE, STRING } = Sequelize;
    // 创建表
    await queryInterface.createTable('friend', {
      id: {
        type: INTEGER(20).UNSIGNED,
        primaryKey: true,
        autoIncrement: true
      },
      user_id: {
        type: INTEGER(20).UNSIGNED,
        allowNull: false,
        comment: '用户id',
        //  定义外键(重要)
        references: {
          model: 'user', // 对应表名称(数据表名称)
          key: 'id' // 对应表的主键
        },
        onUpdate: 'restrict', // 更新时操作
        onDelete: 'cascade'  // 删除时操作
      },
      friend_id: {
        type: INTEGER(20).UNSIGNED,
        allowNull: false,
        comment: '好友id',
        //  定义外键(重要)
        references: {
          model: 'user', // 对应表名称(数据表名称)
          key: 'id' // 对应表的主键
        },
        onUpdate: 'restrict', // 更新时操作
        onDelete: 'cascade'  // 删除时操作
      },
      nickname: {
        type: STRING(30),
        allowNull: false,
        defaultValue: '',
        comment: '备注',
      },
      lookme: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 1,
        comment: '看我'
      },
      lookhim: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 1,
        comment: '看他'
      },
      star: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 0,
        comment: '是否为星标朋友:0否1是'
      },
      isblack: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 0,
        comment: '是否加入黑名单:0否1是'
      },
      created_at: DATE,
      updated_at: DATE
    });
  },

  down: async queryInterface => {
    await queryInterface.dropTable('friend');
  }
};

好友申请表

'use strict';

module.exports = {
    up: async (queryInterface, Sequelize) => {
        const { INTEGER, DATE,ENUM,STRING } = Sequelize;
        // 创建表
        await queryInterface.createTable('apply', {
            id: { 
              type: INTEGER(20).UNSIGNED, 
              primaryKey: true, 
              autoIncrement: true 
            },
            user_id: { 
              type: INTEGER(20).UNSIGNED, 
              allowNull: false, 
              comment: '申请人id',
              //  定义外键(重要)
              references: {
                  model: 'user', // 对应表名称(数据表名称)
                  key: 'id' // 对应表的主键
              },
              onUpdate: 'restrict', // 更新时操作
              onDelete: 'cascade'  // 删除时操作
            },
            friend_id: { 
              type: INTEGER(20).UNSIGNED, 
              allowNull: false,
              comment: '好友id',
              //  定义外键(重要)
              references: {
                  model: 'user', // 对应表名称(数据表名称)
                  key: 'id' // 对应表的主键
              },
              onUpdate: 'restrict', // 更新时操作
              onDelete: 'cascade'  // 删除时操作
            },
            nickname: {
              type: STRING(30), 
              allowNull: false, 
              defaultValue: '', 
              comment: '备注', 
            },
            lookme: {
              type: INTEGER(1), 
              allowNull: false, 
              defaultValue: 1, 
              comment: '看我'
            },
            lookhim: {
              type: INTEGER(1), 
              allowNull: false, 
              defaultValue: 1, 
              comment: '看他'
            },
            status:{
                type: ENUM,
                values: ['pending','refuse','agree','ignore'], 
                allowNull: false, 
                defaultValue: 'pending', 
                comment: '申请状态'
            },
            created_at: DATE,
            updated_at: DATE
        });
    },

    down: async queryInterface => {
        await queryInterface.dropTable('apply');
    }
};

18申请添加好友功能(一)

路由文件

  // 申请添加好友
  router.post('/apply/addfriend',controller.apply.addFriend);

app/controller/apply.js

'use strict';

const Controller = require('egg').Controller;

class ApplyController extends Controller {
  // 申请添加好友
  async addFriend() {
    const { ctx,app } = this;
    // 拿到当前用户id
    let current_user_id = ctx.authUser.id;
    // 验证参数
     ctx.validate({
          friend_id:{type: 'int', required: true,desc: '好友id'},
          nickname:{type: 'string', required: false, desc: '昵称'},
          lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'},
          lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'},
        });
    // 不能添加自己
    // 对方是否存在
    // 之前是否申请过了
    // 创建申请
    ctx.apiSuccess('ok');
  }
}

module.exports = ApplyController;

下图是我测试的截图

在这里插入图片描述

19申请添加好友功能(二)

app/controller/apply.js

'use strict';

const Controller = require('egg').Controller;

class ApplyController extends Controller {
  // 申请添加好友
  async addFriend() {
    const { ctx,app } = this;
    // 拿到当前用户id
    let current_user_id = ctx.authUser.id;
    // 验证参数
     ctx.validate({
          friend_id:{type: 'int', required: true,desc: '好友id'},
          nickname:{type: 'string', required: false, desc: '昵称'},
          lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'},
          lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'},
        });
        let {friend_id,nickname,lookme,lookhim} = ctx.request.body;
    // 不能添加自己
    if(current_user_id === friend_id){
        ctx.throw(400,'不能添加自己');
    }
    // 对方是否存在
    let user = await app.model.User.findOne({
        where:{
            id:friend_id,
            status:1
        }
    })
    if(!user){
        ctx.throw(400,'该用户不存在或者已经被禁用');
    }
    // 之前是否申请过了
    if(await app.model.Apply.findOne({
        where:{
            user_id:current_user_id,
            friend_id,
            status:['pending','agree']
        }
    })){
        ctx.throw(400,'你之前已经申请过了');
    }
    // 创建申请
   let apply = await app.model.Apply.create({
        user_id:current_user_id,
        friend_id,
        lookhim,
        lookme,
        nickname
    });
    if(!apply){
        ctx.throw(400,'申请失败');
    }
    ctx.apiSuccess(apply);
  }
}

module.exports = ApplyController;

下面是我测试的接口

在这里插入图片描述
由于我测试两遍,所以提示已经申请过了

20获取好友申请列表(一)

app/controller/apply.js

class ApplyController extends Controller {
  // 申请添加好友
  async addFriend() {
    const { ctx,app } = this;
    // 拿到当前用户id
    let current_user_id = ctx.authUser.id;
    // 验证参数
     ctx.validate({
          friend_id:{type: 'int', required: true,desc: '好友id'},
          nickname:{type: 'string', required: false, desc: '昵称'},
          lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'},
          lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'},
        });
        let {friend_id,nickname,lookme,lookhim} = ctx.request.body;
    // 不能添加自己
    if(current_user_id === friend_id){
        ctx.throw(400,'不能添加自己');
    }
    // 对方是否存在
    let user = await app.model.User.findOne({
        where:{
            id:friend_id,
            status:1
        }
    })
    if(!user){
        ctx.throw(400,'该用户不存在或者已经被禁用');
    }
    // 之前是否申请过了
    if(await app.model.Apply.findOne({
        where:{
            user_id:current_user_id,
            friend_id,
            status:['pending','agree']
        }
    })){
        ctx.throw(400,'你之前已经申请过了');
    }
    // 创建申请
   let apply = await app.model.Apply.create({
        user_id:current_user_id,
        friend_id,
        lookhim,
        lookme,
        nickname
    });
    if(!apply){
        ctx.throw(400,'申请失败');
    }
    ctx.apiSuccess(apply);
  }
  
  // 获取好友申请列表
  async list(){
      const { ctx,app } = this;
      // 拿到当前用户id
      let current_user_id = ctx.authUser.id;
      let page = ctx.params.page ? parseInt(ctx.params.page) : 1;
      let limit = ctx.query.limit ? parseInt(ctx.query.limit) : 10;
      let offset = (page-1)*limit;
      let rows = await app.model.Apply.findAll({
          where:{
              friend_id:current_user_id
          }
      })
      ctx.apiSuccess('ok');
  }
}

路由文件

  // 获取好友申请列表
  router.post('/apply/:page',controller.apply.list);

21获取好友申请列表(二)

app/model/apply.js

// 定义关联关系
    Apply.associate = function(models){
        // 反向一对多关联
        Apply.belongsTo(app.model.User,{
            foreignKey:'user_id'
        });
    };
  

app/controller/apply.js

// 获取好友申请列表
  async list(){
      const { ctx,app } = this;
      // 拿到当前用户id
      let current_user_id = ctx.authUser.id;
      
      let page = ctx.params.page ? parseInt(ctx.params.page) : 1;
      let limit = ctx.query.limit ? parseInt(ctx.query.limit) : 10;
      let offset = (page-1)*limit;
      let rows = await app.model.Apply.findAll({
          where:{
              friend_id:current_user_id
          },
          include:[{
             model:app.model.User,
             attributes:['id','username','nickname','avatar']
          }],
          offset,
          limit
      })
      let count = await app.model.Apply.count({
          where:{
              friend_id:current_user_id,
              status:'pending'
          }
      });
      ctx.apiSuccess({rows,count});
  }

下面是我测试的数据

在这里插入图片描述

22处理好友申请(一)

路由文件

  // 处理好友申请
  router.post('/apply/handle/:id',controller.apply.handle);

app/controller/apply.js

// 处理好友申请
  async handle(){
     const { ctx,app } = this;
     // 拿到当前用户id
     let current_user_id = ctx.authUser.id;
     let id = parseInt(ctx.params.id); 
     // 验证参数
     ctx.validate({
          nickname:{type: 'string', required: false, desc: '昵称'},
          status:{type: 'int', required: true,range:{in:['refuse','agree','ignore']}, desc: '处理结果'},
          lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'},
          lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'},
     });
     // 查询改申请是否存在
     let apply = await app.model.Apply.findOne({
         where:{
             id,
             friend_id:current_user_id,
             status:'pending'
         }
     });
     if(!apply){
         ctx.throw('400','该记录不存在');
     }
     // 设置该申请状态
     // 加入到好友列表
     // 将对方添加到我的好友列表
     ctx.apiSuccess('ok');
  }
}

23处理好友申请(二)

app/controller/apply.js

 // 处理好友申请
  async handle(){
     const { ctx,app } = this;
     // 拿到当前用户id
     let current_user_id = ctx.authUser.id;
     let id = parseInt(ctx.params.id); 
     // 验证参数
     ctx.validate({
          nickname:{type: 'string', required: false, desc: '昵称'},
          status:{type: 'string', required: true,range:{in:['refuse','agree','ignore']}, desc: '处理结果'},
          lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'},
          lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'},
     });
     // 查询改申请是否存在
     let apply = await app.model.Apply.findOne({
         where:{
             id,
             friend_id:current_user_id,
             status:'pending'
         }
     });
     if(!apply){
         ctx.throw('400','该记录不存在');
     }
     let {status,nickname,lookhim,lookme} = ctx.request.body;
   
     let transaction;
     try {
        // 开启事务
        transaction = await app.model.transaction();
    
        // 设置该申请状态
        await apply.update({
            status
        }, { transaction });
        // apply.status = status;
        // apply.save();
        // 同意,添加到好友列表
        if (status == 'agree') {
            // 加入到对方好友列表
            await app.model.Friend.create({
                friend_id: current_user_id,
                user_id: apply.user_id,
                nickname: apply.nickname,
                lookme: apply.lookme,
                lookhim: apply.lookhim,
            }, { transaction });
            // 将对方加入到我的好友列表
            await app.model.Friend.create({
                friend_id: apply.user_id,
                user_id: current_user_id,
                nickname,
                lookme,
                lookhim,
            }, { transaction });
        }
        
        // 提交事务
        await transaction.commit();
        // 消息推送
        return ctx.apiSuccess('操作成功');
     } catch (e) {
        // 事务回滚
        await transaction.rollback();
        return ctx.apiFail('操作失败');
     }
  }

24获取通讯录列表(一)

路由

  // 通讯录好友申请
  router.get('/friend/list',controller.friend.list);

app/model/friend.js

// app/model/user.js
'use strict';
const crypto = require('crypto');

module.exports = app => {
    const { STRING, INTEGER, DATE, ENUM, TEXT } = app.Sequelize;

    // 配置(重要:一定要配置详细,一定要!!!)
    const Friend = app.model.define('friend', {
      id: {
        type: INTEGER(20).UNSIGNED,
        primaryKey: true,
        autoIncrement: true
      },
      user_id: {
        type: INTEGER(20).UNSIGNED,
        allowNull: false,
        comment: '用户id',
        //  定义外键(重要)
        references: {
          model: 'user', // 对应表名称(数据表名称)
          key: 'id' // 对应表的主键
        },
        onUpdate: 'restrict', // 更新时操作
        onDelete: 'cascade'  // 删除时操作
      },
      friend_id: {
        type: INTEGER(20).UNSIGNED,
        allowNull: false,
        comment: '好友id',
        //  定义外键(重要)
        references: {
          model: 'user', // 对应表名称(数据表名称)
          key: 'id' // 对应表的主键
        },
        onUpdate: 'restrict', // 更新时操作
        onDelete: 'cascade'  // 删除时操作
      },
      nickname: {
        type: STRING(30),
        allowNull: false,
        defaultValue: '',
        comment: '备注',
      },
      lookme: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 1,
        comment: '看我'
      },
      lookhim: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 1,
        comment: '看他'
      },
      star: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 0,
        comment: '是否为星标朋友:0否1是'
      },
      isblack: {
        type: INTEGER(1),
        allowNull: false,
        defaultValue: 0,
        comment: '是否加入黑名单:0否1是'
      },
      created_at: DATE,
      updated_at: DATE
    });
    // 定义关联关系
    Friend.associate = function(model){
        // 反向一对多关联
        Friend.belongsTo(app.model.User,{
            as:"friendInfo",
            foreignKey:'friend_id'
        });
    };
    return Friend;
};

app/model/friend.js

'use strict';

const Controller = require('egg').Controller;

class FriendController extends Controller {
    //通讯录
  async list() {
    const { ctx,app } = this;
    let current_user_id = ctx.authUser.id;
    // 获取并统计我的好友
    let friends = await app.model.Friend.findAndCountAll({
        where:{
            user_id:current_user_id
        },
        include:[{
            as:"friendInfo",
            model:app.model.User,
            attributes:['id','username','nickname','avatar']
        }]
    });
    ctx.apiSuccess(friends);
  }
}

module.exports = FriendController;

下图是我自己测试的

在这里插入图片描述

25获取通讯录列表(二)

安装

  npm install sort-word -S

使用

第三参数为true时 添加热门项,默认添加传入数组前10个 不需要热门项,则不需要传第三个参数

  import SortWord from 'sort-word'

  let arr = [{name: '张三'}, {name: '李四'}]
  let newArr = new SortWord(arr, 'name')

  /*
  newArr {
    newList:[
      {title: 'L', list: [{name: '李'}]},
      {title: 'Z', list: [{name: '张'}]}
    ],
    indexList: ['L', 'Z'],
    total: 2
  }
  */

app/controller/friend.js

//通讯录
 async list() {
   const { ctx,app } = this;
   let current_user_id = ctx.authUser.id;
   // 获取并统计我的好友
   let friends = await app.model.Friend.findAndCountAll({
       where:{
           user_id:current_user_id
       },
       include:[{
           as:"friendInfo",
           model:app.model.User,
           attributes:['id','username','nickname','avatar']
       }]
   });
   let res = friends.rows.map(item=>{
       let name = item.friendInfo.nickname ? item.friendInfo.nickname : item.friendInfo.username;
       if(item.nickname){
           name = item.nickname
       }
       return {
           id:item.id,
           user_id:item.friendInfo.id,
           name,
           username:item.friendInfo.username,
           avatar:item.friendInfo.avatar
       }
   });
   // 排序
   friends.res = new SortWord(res,'name');
   ctx.apiSuccess(friends);
 }

26查看好友资料功能实现

路由

  // 查看好友资料
  router.get('/friend/read/:id',controller.friend.read);

app/controller/friend.js

// 查看好友资料
  async read(){
    const { ctx,app } = this;
    let current_user_id = ctx.authUser.id;
    let id = ctx.params.id ? parseInt(ctx.params.id) : 0;
    let friend = await app.model.Friend.findOne({
        where:{
            friend_id:id,
            user_id:current_user_id
        },
        include:[{
            model:app.model.User,
            as:'friendInfo',
            attributes:{
                exclude:['password']
            }
        }]
    });
    if(!friend){
        ctx.throw(400,'用户不存在');
    }
    ctx.apiSuccess(friend);
  }

下图是我测试的截图

在这里插入图片描述

27移入移除黑名单功能

路由

   // 移入/移除黑名单
  router.post('/friend/setblack/:id', controller.friend.setblack);

app/controller/friend.js

 // 移入/移除黑名单
    async setblack() {
        const { ctx, app } = this;
        let current_user_id = ctx.authUser.id;
        let id = ctx.params.id ? parseInt(ctx.params.id) : 0;
        // 参数验证
        ctx.validate({
            isblack: {
                type: 'int',
                range: {
                    in: [0, 1]
                },
                required: true,
                desc: '移入/移除黑名单'
            },
        });

        let friend = await app.model.Friend.findOne({
            where: {
                friend_id: id,
                user_id: current_user_id
            }
        });
        if (!friend) {
            ctx.throw(400, '该记录不存在');
        }

        friend.isblack = ctx.request.body.isblack;
        await friend.save();

        ctx.apiSuccess('ok');
    }

我的测试记录如下图

在这里插入图片描述

28设置取消星标好友

路由

   // 设置/取消星标好友
  router.post('/friend/setstar/:id', controller.friend.setstar);

app/controller/friend.js

// 设置/取消星标好友
    async setstar() {
        const { ctx, app } = this;
        let current_user_id = ctx.authUser.id;
        let id = ctx.params.id ? parseInt(ctx.params.id) : 0;
        // 参数验证
        ctx.validate({
            star: {
                type: 'int',
                range: {
                    in: [0, 1]
                },
                required: true,
                desc: '设置/取消星标好友'
            },
        });

        let friend = await app.model.Friend.findOne({
            where: {
                friend_id: id,
                user_id: current_user_id,
                isblack: 0
            }
        });

        if (!friend) {
            ctx.throw(400, '该记录不存在');
        }

        friend.star = ctx.request.body.star;
        await friend.save();

        ctx.apiSuccess('ok');
    }

下图是我测试的截图

在这里插入图片描述

29设置朋友圈权限功能

路由

 // 设置朋友圈权限
  router.post('/friend/setmomentauth/:id', controller.friend.setMomentAuth);

app/controller/friend.js

// 设置朋友圈权限
    async setMomentAuth() {
        const { ctx, app } = this;
        let current_user_id = ctx.authUser.id;
        let id = ctx.params.id ? parseInt(ctx.params.id) : 0;

        // 参数验证
        ctx.validate({
            lookme: {
                type: 'int',
                range: {
                    in: [0, 1]
                },
                required: true
            },
            lookhim: {
                type: 'int',
                range: {
                    in: [0, 1]
                },
                required: true
            },
        });

        let friend = await app.model.Friend.findOne({
            where: {
                user_id: current_user_id,
                friend_id: id,
                isblack: 0
            }
        });
        if (!friend) {
            ctx.throw(400, '该记录不存在');
        }
        let { lookme, lookhim } = ctx.request.body;
        friend.lookhim = lookhim;
        friend.lookme = lookme;

        await friend.save();

        ctx.apiSuccess('ok');
    }

下图是我测试的截图

在这里插入图片描述

30举报投诉好友或群组功能(一)

命令行

npx sequelize migration:generate --name=report

/database/migrations/xxx-report.js

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    const { INTEGER, STRING, DATE, ENUM, TEXT } = Sequelize;
    // 创建表
    await queryInterface.createTable('report', {
      id: {
        type: INTEGER(20).UNSIGNED,
        primaryKey: true,
        autoIncrement: true
      },
      user_id: {
        type: INTEGER(20).UNSIGNED,
        allowNull: false,
        comment: '用户id',
        //  定义外键(重要)
        references: {
          model: 'user', // 对应表名称(数据表名称)
          key: 'id' // 对应表的主键
        },
        onUpdate: 'restrict', // 更新时操作
        onDelete: 'cascade'  // 删除时操作
      },
      reported_id: {
        type: INTEGER(20).UNSIGNED,
        allowNull: false,
        comment: '被举报人id',
      },
      reported_type: {
        type: ENUM,
        values: ['user', 'group'],
        allowNull: false,
        defaultValue: 'user',
        comment: '举报类型'
      },
      content: {
        type: TEXT,
        allowNull: true,
        defaultValue: '',
        comment: '举报内容'
      },
      category: {
        type: STRING(10),
        allowNull: true,
        defaultValue: '',
        comment: '举报分类'
      },
      status: {
        type: ENUM,
        values: ['pending', 'refuse', 'agree'],
        allowNull: false,
        defaultValue: 'pending',
        comment: '举报状态'
      },
      created_at: DATE,
      updated_at: DATE
    });
  },

  down: async queryInterface => {
    await queryInterface.dropTable('report');
  }
};

31举报投诉好友或群组功能(二)

命令行 (创建表)

npx sequelize db:migrate

路由

// 举报投诉好友/群组
  router.post('/report/save', controller.report.save);

app/controller/report.js

'use strict';

const Controller = require('egg').Controller;

class ReportController extends Controller {
    // 举报
    async save() {
        const { ctx, app } = this;
        let current_user_id = ctx.authUser.id;
        // 参数验证
        ctx.validate({
            reported_id: {
                type: 'int',
                required: true,
                desc: '被举报人id/群组id'
            },
            reported_type: {
                type: 'string',
                required: true,
                range: {
                    in: ['user', 'group']
                },
                desc: '举报类型'
            },
            content: {
                type: 'string',
                required: true,
                desc: '举报内容'
            },
            category: {
                type: 'string',
                required: true,
                desc: '分类'
            },
        });
        let { reported_id, reported_type, content, category } = ctx.request.body;
        // 不能举报自己
        if (reported_type == 'user' && reported_id === current_user_id) {
            ctx.throw(400, '不能举报自己');
        }
        // 被举报人是否存在
        if (!await app.model.User.findOne({
            where: {
                id: reported_id,
                status: 1
            }
        })) {
            ctx.throw(400, '被举报人不存在');
        }
        // 检查之前是否举报过(还未处理)
        if (await app.model.Report.findOne({
            where: {
                reported_id,
                reported_type,
                status: "pending"
            }
        })) {
            ctx.throw(400, '请勿反复提交');
        }
        // 创建举报内容
        let res = await app.model.Report.create({
            user_id: current_user_id,
            reported_id, reported_type, content, category
        });
        ctx.apiSuccess(res);
    }
}

module.exports = ReportController;

app/model/report.js

'use strict';
const crypto = require('crypto');
module.exports = app => {
    const { INTEGER, STRING, DATE, ENUM, TEXT } = app.Sequelize;
    // 配置(重要:一定要配置详细,一定要!!!)
    const Report = app.model.define('report', {
        id: {
            type: INTEGER(20).UNSIGNED,
            primaryKey: true,
            autoIncrement: true
        },
        user_id: {
            type: INTEGER(20).UNSIGNED,
            allowNull: false,
            comment: '用户id',
            //  定义外键(重要)
            references: {
                model: 'user', // 对应表名称(数据表名称)
                key: 'id' // 对应表的主键
            },
            onUpdate: 'restrict', // 更新时操作
            onDelete: 'cascade'  // 删除时操作
        },
        reported_id: {
            type: INTEGER(20).UNSIGNED,
            allowNull: false,
            comment: '被举报人id',
        },
        reported_type: {
            type: ENUM,
            values: ['user', 'group'],
            allowNull: false,
            defaultValue: 'user',
            comment: '举报类型'
        },
        content: {
            type: TEXT,
            allowNull: true,
            defaultValue: '',
            comment: '举报内容'
        },
        category: {
            type: STRING(10),
            allowNull: true,
            defaultValue: '',
            comment: '举报分类'
        },
        status: {
            type: ENUM,
            values: ['pending', 'refuse', 'agree'],
            allowNull: false,
            defaultValue: 'pending',
            comment: '举报状态'
        },
        created_at: DATE,
        updated_at: DATE
    });

    return Report;
};

下图是我测试的截图

在这里插入图片描述

32设置备注和标签功能(一)

标签表

npx sequelize migration:generate --name=tag

迁移文件

'use strict';

module.exports = {
    up: async (queryInterface, Sequelize) => {
        const { INTEGER, STRING, DATE, ENUM } = Sequelize;
        // 创建表
        await queryInterface.createTable('tag', {
            id: { 
              type: INTEGER(20).UNSIGNED, 
              primaryKey: true, 
              autoIncrement: true 
            },
            name: { 
              type: STRING(30), 
              allowNull: false, 
              defaultValue: '', 
              comment: '标签名称', 
            },
            user_id: { 
              type: INTEGER(20).UNSIGNED, 
              allowNull: false, 
              comment: '用户id',
                //  定义外键(重要)
                references: {
                  model: 'user', // 对应表名称(数据表名称)
                  key: 'id' // 对应表的主键
              },
              onUpdate: 'restrict', // 更新时操作
              onDelete: 'cascade'  // 删除时操作
            },
            created_at: DATE,
            updated_at: DATE
        });
    },

    down: async queryInterface => {
        await queryInterface.dropTable('tag');
    }
};

标签好友关联表

npx sequelize migration:generate --name=friend_tag

迁移文件

'use strict';

module.exports = {
    up: async (queryInterface, Sequelize) => {
        const { INTEGER, DATE } = Sequelize;
        // 创建表
        await queryInterface.createTable('friend_tag', {
            id: { 
              type: INTEGER(20).UNSIGNED, 
              primaryKey: true, 
              autoIncrement: true 
            },
            friend_id: { 
              type: INTEGER(20).UNSIGNED, 
              allowNull: false,
              comment: '好友id',
              //  定义外键(重要)
              references: {
                  model: 'friend', // 对应表名称(数据表名称)
                  key: 'id' // 对应表的主键
              },
              onUpdate: 'restrict', // 更新时操作
              onDelete: 'cascade'  // 删除时操作
            },
            tag_id: { 
              type: INTEGER(20).UNSIGNED, 
              allowNull: false,
              comment: '标签id',
              //  定义外键(重要)
              references: {
                  model: 'tag', // 对应表名称(数据表名称)
                  key: 'id' // 对应表的主键
              },
              onUpdate: 'restrict', // 更新时操作
              onDelete: 'cascade'  // 删除时操作
            },
            created_at: DATE,
            updated_at: DATE
        });
    },

    down: async queryInterface => {
        await queryInterface.dropTable('friend_tag');
    }
};

模型

app/model/tag.js

'use strict';
const crypto = require('crypto');
module.exports = app => {
    const { INTEGER, STRING, DATE, ENUM } = app.Sequelize;
    // 配置(重要:一定要配置详细,一定要!!!)
    const Tag = app.model.define('tag', {
        id: {
            type: INTEGER(20).UNSIGNED,
            primaryKey: true,
            autoIncrement: true
        },
        name: {
            type: STRING(30),
            allowNull: false,
            defaultValue: '',
            comment: '标签名称',
            unique: true
        },
        user_id: {
            type: INTEGER(20).UNSIGNED,
            allowNull: false,
            comment: '用户id',
            //  定义外键(重要)
            references: {
                model: 'user', // 对应表名称(数据表名称)
                key: 'id' // 对应表的主键
            },
            onUpdate: 'restrict', // 更新时操作
            onDelete: 'cascade'  // 删除时操作
        },
        created_at: DATE,
        updated_at: DATE
    });

    Tag.associate = function (model) {
        // 多对多(标签)
        Tag.belongsToMany(app.model.Friend, {
            through: 'friend_tag',
            foreignKey: 'tag_id'
        })
    }

    return Tag;
};

app/model/friend_tag.js

'use strict';
const crypto = require('crypto');
module.exports = app => {
    const { INTEGER, DATE } = app.Sequelize;
    // 配置(重要:一定要配置详细,一定要!!!)
    const FriendTag = app.model.define('friend_tag', {
        id: {
            type: INTEGER(20).UNSIGNED,
            primaryKey: true,
            autoIncrement: true
        },
        friend_id: {
            type: INTEGER(20).UNSIGNED,
            allowNull: false,
            comment: '好友id',
            //  定义外键(重要)
            references: {
                model: 'friend', // 对应表名称(数据表名称)
                key: 'id' // 对应表的主键
            },
            onUpdate: 'restrict', // 更新时操作
            onDelete: 'cascade'  // 删除时操作
        },
        tag_id: {
            type: INTEGER(20).UNSIGNED,
            allowNull: false,
            comment: '标签id',
            //  定义外键(重要)
            references: {
                model: 'tag', // 对应表名称(数据表名称)
                key: 'id' // 对应表的主键
            },
            onUpdate: 'restrict', // 更新时操作
            onDelete: 'cascade'  // 删除时操作
        },
        created_at: DATE,
        updated_at: DATE
    });

    return FriendTag;
};

33设置备注和标签功能(二)

路由

   // 设置好友备注和标签
  router.post('/friend/setremarktag/:id',controller.friend.setremarkTag);

app/controller/friend.js

// 设置备注和标签
  async setremarkTag(){
      const { ctx, app } = this;
      let current_user_id = ctx.authUser.id;
      let id = ctx.params.id ? parseInt(ctx.params.id) : 0;
      // 参数验证
        ctx.validate({
            nickname: {
                type: 'string',
                required: false,
                desc:'昵称'
            },
            tags: {
                type: 'string',
                required: true,
                desc:'标签'
            },
        });
        // 查看好友是否存在
        let friend = await app.model.Friend.findOne({
            where:{
                user_id:current_user_id,
                friend_id:id,
                isblack:0
            },
            include:[{
                model:app.model.Tag
            }]
        });
        if(!friend){
            ctx.throw(400,'该记录不存在');
        }
        let {tags} = ctx.request.body;
        tags = tags.split(',');
        let addTages = tags.map(name=>{return {name,user_id:current_user_id}});
        // 写入tag表
        let resTages = await app.model.Tag.bulkCreate(addTages);
        if(resTages){
            let addFriendTag = resTages.map(item=>{return {tag_id:item.id,friend_id:id}});
            console.log(addFriendTag);
            await app.model.FriendTag.bulkCreate(addFriendTag);
        }
        ctx.apiSuccess(tags);
  }

下面是我测试的截图

在这里插入图片描述

34设置备注和标签功能(三)

app/controller/friend.js

// 设置备注和标签
  async setremarkTag(){
      const { ctx, app } = this;
        let current_user_id = ctx.authUser.id;
        let id = ctx.params.id ? parseInt(ctx.params.id) : 0;
        // 参数验证
        ctx.validate({
            nickname: {
                type: 'string',
                required: false,
                desc: "昵称"
            },
            tags: {
                type: 'string',
                required: true,
                desc: "标签"
            },
        });
        // 查看该好友是否存在
        let friend = await app.model.Friend.findOne({
            where: {
                user_id: current_user_id,
                friend_id: id,
                isblack: 0
            },
            include: [{
                model: app.model.Tag
            }]
        });
        if (!friend) {
            ctx.throw(400, '该记录不存在');
        }

        let { tags, nickname } = ctx.request.body;
        // // 设置备注
        friend.nickname = nickname;
        await friend.save();

        // 获取当前用户所有标签
        let allTags = await app.model.Tag.findAll({
            where: {
                user_id: current_user_id
            }
        });

        let allTagsName = allTags.map(item => item.name);

        // 新标签
        let newTags = tags.split(',');

        // 需要添加的标签
        let addTags = newTags.filter(item => !allTagsName.includes(item));
        addTags = addTags.map(name => {
            return {
                name,
                user_id: current_user_id
            }
        });
        // 写入tag表
        let resAddTags = await app.model.Tag.bulkCreate(addTags);

        // 找到新标签的id
        newTags = await app.model.Tag.findAll({
            where: {
                user_id: current_user_id,
                name: newTags
            }
        });

        let oldTagsIds = friend.tags.map(item => item.id);
        let newTagsIds = newTags.map(item => item.id);

        let addTagsIds = newTagsIds.filter(id => !oldTagsIds.includes(id));
        let delTagsIds = oldTagsIds.filter(id => !newTagsIds.includes(id));

        // 添加关联关系
        addTagsIds = addTagsIds.map(tag_id => {
            return {
                tag_id,
                friend_id: friend.id
            }
        });

        app.model.FriendTag.bulkCreate(addTagsIds);

        // 删除关联关系
        app.model.FriendTag.destroy({
            where: {
                tag_id: delTagsIds,
                friend_id: friend.id
            }
        });

        ctx.apiSuccess('ok');
  }

35安装websocket插件

https://www.npmjs.com/package/egg-websocket-plugin

安装插件

npm i egg-websocket-plugin --save

1. 开启插件

// config/plugin.js
exports.websocket = {
  enable: true,
  package: 'egg-websocket-plugin',
};

2. 配置 WebSocket 路由

// app/router.js
app.ws.route('/ws', app.controller.home.hello);

3. 配置全局中间件

// app/router.js

// 配置 WebSocket 全局中间件
app.ws.use((ctx, next) => {
  console.log('websocket 开启');
  await next();
  console.log('websocket 关闭');
});

4. 配置路由中间件

路由会依次用到 app.use, app.ws.use, 以及 app.ws.router 中配置的中间件

// app/router.js
function middleware(ctx, next) {
  // console.log('open', ctx.starttime);
  return next();
}
// 配置路由中间件
app.ws.route('/ws', middleware, app.controller.chat.connect);

5. 在控制中使用 websocket

websocket 是一个 ws,可阅读 ws 插件的说明文档或 TypeScript 的定义

// app/controller/chat.js
import { Controller } from 'egg';

export default class ChatController extends Controller {
    // 连接socket
    async connect() {
        const { ctx, app } = this;
        if (!ctx.websocket) {
            ctx.throw(400,'非法访问');
        }

        console.log(`clients: ${app.ws.clients.size}`);

        // 监听接收消息和关闭socket
        ctx.websocket
        .on('message', msg => {
            console.log('接收消息', msg);
        })
        .on('close', (code, reason) => {
            console.log('websocket 关闭', code, reason);
        });

  }
}

常用

// 广播(发送给所有的人)
app.ws.clients.forEach((client) => {
    client.send(msg);
});
// 发送给当前用户
ctx.websocket.send('哈哈哈,链接上了');
// 当前上线人数
app.ws.clients.size
// 强制当前用户下线
ctx.websocket.close();

36连接websocket和权限验证

// app/router.js

app.ws.use(async (ctx, next) => {
    // 获取参数 ws://localhost:7001/ws?token=123456
    // ctx.query.token
    // 验证用户token
    let user = {};
    let token = ctx.query.token;
    try {
        user = ctx.checkToken(token);
        // 验证用户状态
        let userCheck = await app.model.User.findByPk(user.id);
        if (!userCheck) {
            ctx.websocket.send(JSON.stringify({
                msg: "fail",
                data: '用户不存在'
            }));
            return ctx.websocket.close();
        }
        if (!userCheck.status) {
            ctx.websocket.send(JSON.stringify({
                msg: "fail",
                data: '你已被禁用'
            }));
            return ctx.websocket.close();
        }
        // 用户上线
        app.ws.user = app.ws.user ? app.ws.user : {};
        // 下线其他设备
        if (app.ws.user[user.id]) {
            app.ws.user[user.id].send(JSON.stringify({
                msg: "fail",
                data: '你的账号在其他设备登录'
            }));
            app.ws.user[user.id].close();
        }
        // 记录当前用户id
        ctx.websocket.user_id = user.id;
        app.ws.user[user.id] = ctx.websocket;
        await next();
    } catch (err) {
        console.log(err);
        let fail = err.name === 'TokenExpiredError' ? 'token 已过期! 请重新获取令牌' : 'Token 令牌不合法!';
        ctx.websocket.send(JSON.stringify({
            msg: "fail",
            data: fail
        }))
        // 关闭连接
        ctx.websocket.close();
    }
});

// 路由配置

app.ws.route('/ws', controller.chat.connect);

// app/controller/chat.js

const Controller = require('egg').Controller;

class ChatController extends Controller {
    // 连接socket
    async connect() {
        const { ctx, app } = this;
        if (!ctx.websocket) {
            ctx.throw(400,'非法访问');
        }

        // console.log(`clients: ${app.ws.clients.size}`);

        // 监听接收消息和关闭socket
        ctx.websocket
        .on('message', msg => {
            // console.log('接收消息', msg);
        })
        .on('close', (code, reason) => {
            // 用户下线
            console.log('用户下线', code, reason);
            let user_id = ctx.websocket.user_id;
            if (app.ws.user && app.ws.user[user_id]) {
              delete app.ws.user[user_id];
            }
        });

  }
}
module.exports = ChatController;

37兼容H5端处理

首先我们需要关闭设置纯nvue

在这里插入图片描述

第二步就是将所有的.nvue改为.vue

主要是在page下

在这里插入图片描述
然后我们在浏览器打开就可以看到
在这里插入图片描述

38配置H5端跨域问题

配置白名单

config/configdefault.js

     // 跨域白名单
    domainWhiteList: ['http://localhost:8081'],

配置uni-app中,manifest.json

"h5": {
	        "devServer": {
	            "https": false,
	            "proxy": {
	                "/api": {
	                    "target": "http://localhost:7001/",
	                    "changeOrigin": true,
	                    "ws": true,
	                    "pathRewrite": {
	                        "^/api": ""
	                    }
	                }
	            }
	        }
	    }

39登录注册功能实现(一)

封装request类

// request.js
export default {
    // 全局配置
    common:{
        baseUrl:'/api',
        header:{
            'Content-Type':'application/json;charset=UTF-8',
        },
        data:{},
        method:'GET',
        dataType:'json',
        token:true
    },
    // 请求 返回promise
    request(options = {}){
        // 组织参数
        options.url = this.common.baseUrl + options.url
        options.header = options.header || this.common.header
        options.data = options.data || this.common.data
        options.method = options.method || this.common.method
        options.dataType = options.dataType || this.common.dataType
        options.token = options.token === false ?  false : this.common.token

        // 请求之前验证...
        // token验证
        if (options.token) {
            let token = uni.getStorageSync('token')
            // 二次验证
            if (!token) {
                uni.showToast({ title: '请先登录', icon: 'none' });
                // token不存在时跳转
                return uni.reLaunch({
                    url: '/pages/login/login',
                });
            }
            // 往header头中添加token
            options.header.token = token
        }

        // 请求
        return new Promise((res,rej)=>{
            // 请求中...
            uni.request({
                ...options,
                success: (result) => {
                    // 返回原始数据
                    if(options.native){
                        return res(result)
                    }
                    // 服务端失败
                    if(result.statusCode !== 200){
                        if (options.toast !== false) {
                            uni.showToast({
                                title: result.data.data || '服务端失败',
                                icon: 'none'
                            });
                        }
                        return rej(result.data) 
                    }
                    // 其他验证...
                    // 成功
                    let data = result.data.data
                    res(data)
                },
                fail: (error) => {
                    uni.showToast({ title: error.errMsg || '请求失败', icon: 'none' });
                    return rej(error)
                }
            });
        })
    },
    // get请求
    get(url,data = {},options = {}){
        options.url = url
        options.data = data
        options.method = 'GET'
        return this.request(options)
    },
    // post请求
    post(url,data = {},options = {}){
        options.url = url
        options.data = data
        options.method = 'POST'
        return this.request(options)
    },
    // delete请求
    del(url,data = {},options = {}){
        options.url = url
        options.data = data
        options.method = 'DELETE'
        return this.request(options)
    },
}

前端代码

<template>
	<view class="">
		<view v-if="show" class="position-fixed top-0 bottom-0 left-0  right-0 bg-light flex align-center justify-center">
			<text class="text-muted font">正在加载...</text>
		</view>
		<view class="" v-else>
			<view class="flex align-center justify-center pt-5" style="height: 350rpx;">
				<text style="font-size: 50rpx;">LOGO</text>
			</view>
			<view class="px-3">
				<input type="text" class="bg-light px-3 mb-3 font" style="height: 100rpx;" v-model="form.username" placeholder="请输入用户名" />
				<input type="text" class="bg-light px-3 mb-3 font" style="height: 100rpx;" v-model="form.password" placeholder="请输入密码" />
					<input v-if="type==='reg'" type="text" class="bg-light px-3 mb-3 font" style="height: 100rpx;" v-model="form.repassword" placeholder="请输入确认密码" />
			</view>
			<view class="p-3 flex align-center justify-center">
				<view class="flex-1 main-bg-color rounded p-3 flex align-center justify-center" hover-class="main-bg-hover-color" @click="submit">
					<text class="text-white font-md">{{type==='login' ? '登 录' : '注 册'}}</text>
				</view>
			</view>
			
			<view class="flex align-center justify-center">
				<text class='text-light-muted font  p-2' @click="changeType">{{type==='login' ? '注册账号' : '登录账号'}}</text>
				<text class='text-light-muted font'>|</text>
				<text class='text-light-muted font  p-2'>忘记密码</text>
			</view>
		</view>
	  
	</view>
</template>

<script>
	import $H from '@/common/free-lib/request.js';
	export default {
		data() {
			return {
				type:'login',
				show:false,
				form:{
					username:'',
					password:'',
					repassword:''
				}
			}
		},
		created() {
			
			// uni.switchTab({
			// 	url:'../../tabbar/index/index'
			// })
			// setTimeout(()=>{
			// 	// 用户登录
			// 	this.show = true;
			// 	用户登录
			// 	uni.switchTab({
			// 		url:'../../tabbar/index/index',
			// 	})
			// },800);
		},
		methods: {
			changeType(){
				this.type = this.type==='login' ? 'reg' : 'login';
			},
			submit(){
				//请求登录接口
				$H.post('/login',this.form,{token:false}).then(res=>{
					console.log(res);
				})
			}
		}
	}
</script>

<style>
.page-loading{
	background-color: #C8C7CC;
	/* #ifdef APP-PLUS-NVUE */
	min-height: 100%;
	height: auto;
	/* #endif */
	/* #ifdef APP-PLUS-NVUE */
	flex:1;
	/* #endif */
}
</style>

40登录注册功能实现(二)

login.vue

<template>
	<view class="">
		<view v-if="show" class="position-fixed top-0 bottom-0 left-0  right-0 bg-light flex align-center justify-center">
			<text class="text-muted font">正在加载...</text>
		</view>
		<view class="" v-else>
			<view class="flex align-center justify-center pt-5" style="height: 350rpx;">
				<text style="font-size: 50rpx;">LOGO</text>
			</view>
			<view class="px-3">
				<input type="text" class="bg-light px-3 mb-3 font" style="height: 100rpx;" v-model="form.username" placeholder="请输入用户名" />
				<input type="text" class="bg-light px-3 mb-3 font" style="height: 100rpx;" v-model="form.password" placeholder="请输入密码" />
					<input v-if="type==='reg'" type="text" class="bg-light px-3 mb-3 font" style="height: 100rpx;" v-model="form.repassword" placeholder="请输入确认密码" />
			</view>
			<view class="p-3 flex align-center justify-center">
				<view class="flex-1 main-bg-color rounded p-3 flex align-center justify-center" hover-class="main-bg-hover-color" @click="submit">
					<text class="text-white font-md">{{type==='login' ? '登 录' : '注 册'}}</text>
				</view>
			</view>
			
			<view class="flex align-center justify-center">
				<text class='text-light-muted font  p-2' @click="changeType">{{type==='login' ? '注册账号' : '登录账号'}}</text>
				<text class='text-light-muted font'>|</text>
				<text class='text-light-muted font  p-2'>忘记密码</text>
			</view>
		</view>
	  
	</view>
</template>

<script>
	import $H from '@/common/free-lib/request.js';
	export default {
		data() {
			return {
				type:'login',
				show:false,
				form:{
					username:'',
					password:'',
					repassword:''
				}
			}
		},
		created() {
			
			// uni.switchTab({
			// 	url:'../../tabbar/index/index'
			// })
			// setTimeout(()=>{
			// 	// 用户登录
			// 	this.show = true;
			// 	用户登录
			// 	uni.switchTab({
			// 		url:'../../tabbar/index/index',
			// 	})
			// },800);
		},
		methods: {
			changeType(){
				this.type = this.type==='login' ? 'reg' : 'login';
				this.form = {
					username:'',
					password:'',
					repassword:''
				}
			},
			submit(){
				//请求登录接口
				$H.post('/'+this.type,this.form,{token:false}).then(res=>{
					// 登录
					if(this.type === 'login'){
						this.$store.dispatch('login',res);
						uni.showToast({
							title:'登录成功',
							icon:'none'
						});
						return uni.switchTab({
							url:'/pages/tabbar/index/index'
						})
					}else{
						// 注册
						this.changeType();
						uni.showToast({
							title:'注册成功,去登陆',
							icon:'none'
						})
					}
					
				})
			}
		}
	}
</script>

<style>
.page-loading{
	background-color: #C8C7CC;
	/* #ifdef APP-PLUS-NVUE */
	min-height: 100%;
	height: auto;
	/* #endif */
	/* #ifdef APP-PLUS-NVUE */
	flex:1;
	/* #endif */
}
</style>

新建/store/modules/user.js

export default{
	state:{
		user:false
	},
	actions:{
		// 登录后处理
		login({state},user){
			// 存到状态种
			state.user=user;
			// 存储到本地存储
			uni.setStorageSync('token',user.token);
			uni.setStorageSync('user',JSON.stringify(user));
			uni.setStorageSync('user_id',JSON.stringify(user.id));
		}
	}
}

/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import audio from '@/store/modules/audio.js';
import user from '@/store/modules/user.js'
export default new Vuex.Store({
	modules:{
		audio,
		user
	}
})
// export default new Vuex.Store({
// 	modules:{
// 		audio,
// 		user,
// 		common
// 	}
// })

41部署聊天调试环境

新建common/util.js

import $C from './config.js'
export default {
    // 获取存储列表数据
    getStorage(key){
        let data = null;
        if($C.env === 'dev'){
            data = window.sessionStorage.getItem(key)
        } else {
            data = uni.getStorageSync(key)
        }
        return data
    },
    // 设置存储
    setStorage(key,data){
        if($C.env === 'dev'){
            return window.sessionStorage.setItem(key,data)
        } else {
            return uni.setStorageSync(key,data)
        }
    },
    // 删除存储
    removeStorage(key){
        if($C.env === 'dev'){
            return window.sessionStorage.removeItem(key);
        } else {
            return uni.removeStorageSync(key)
        }
    }
}

修改store/modules/user.js

import $U from '@/common/free-lib/util.js';
export default{
	state:{
		user:false
	},
	actions:{
		// 登录后处理
		login({state},user){
			// 存到状态种
			state.user=user;
			// 存储到本地存储
			$U.setStorage('token',user.token);
			$U.setStorage('user',JSON.stringify(user));
			$U.setStorage('user_id',user.id);
		}
	}
}

42退出登录功能实现

page/my/setting.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<free-nav-bar title="我的设置" showBack :showRight="false"></free-nav-bar>
		<!-- 退出登录 -->
		<free-divider></free-divider>
		<view @click="logout" class="py-3 flex align-center justify-center bg-white" hover-class="bg-light">
			<text class="font-md text-primary">退出登录</text>
		</view>
	</view>
</template>

<script>
	import freeNavBar from '@/components/free-ui/free-nav-bar.vue';
	import freeDivider from '@/components/free-ui/free-divider.vue';
	import $H from '@/common/free-lib/request.js';
	export default {
		components:{
			freeDivider,
			freeNavBar
		},
		data() {
			return {
				
			}
		},
		methods: {
			//退出登录
			logout(){
				$H.post('/logout').then(res=>{
					uni.showToast({
						title:'退出登录成功',
						icon:'none'
					})
					this.$store.dispatch('logout');
				})
			}
		}
	}
</script>

<style>

</style>

修改 store/modules/user.js

import $U from '@/common/free-lib/util.js';
export default{
	state:{
		user:false
	},
	actions:{
		// 登录后处理
		login({state},user){
			// 存到状态种
			state.user=user;
			// 存储到本地存储
			$U.setStorage('token',user.token);
			$U.setStorage('user',JSON.stringify(user));
			$U.setStorage('user_id',user.id);
		}
	},
	// 退出登录
	logout({state}){
		// 清除登录状态
		state.user = false;
		// 清除本地存储数据
		$U.removeStorage('token');
		$U.removeStorage('user');
		$U.removeStorage('user_id');
		// 跳转到登录页
		uni.reLaunch({
			url:'/pages/common/login/login'
		})
	}
}

43全局mixin权限验证实现

common/mixin/auth.js

import $U from '@/common/free-lib/util.js';
export default{
	onShow() {
		let token = $U.getStorage('token');
		if(!token){
			return uni.reLaunch({
				url:'/pages/common/login/login'
			})
			uni.showToast({
				title:'请先登录',
				icon:'none'
			})
		}
	},
}

/pages/tabbar 中都要引入

import auth from '@/common/mixin/auth.js';
export default {
		mixins:[auth],
		//......
}

44初始化登录状态

App.vue

<script>
	export default {
		onLaunch: function() {
			// #ifdef APP-PLUS-NVUE
			// 加载公共图标库
			const domModule = weex.requireModule('dom')
			domModule.addRule('fontFace', {
			    'fontFamily': "iconfont",
			    'src': "url('/static/font_1365296_2ijcbdrmsg.ttf')"
			});
			// #endif
			// 初始化录音管理器
			this.$store.commit('initRECORD');
			// 初始化登录状态
			this.$store.dispatch('initLogin');
			console.log('App Launch')
		},
		onShow: function() {
			console.log('App Show')
		},
		onHide: function() {
			console.log('App Hide')
		}
	}
</script>

<style>
	/*每个页面公共css */
	@import "./common/free.css";
    @import "./common/common.css";
	/* #ifndef APP-PLUS-NVUE */
	@import  "./common/free-icon.css";
	/* #endif */
</style>

store/modules/user.js

import $U from '@/common/free-lib/util.js';
export default{
	state:{
		user:false
	},
	actions:{
		// 登录后处理
		login({state},user){
			// 存到状态种
			state.user=user;
			// 存储到本地存储
			$U.setStorage('token',user.token);
			$U.setStorage('user',JSON.stringify(user));
			$U.setStorage('user_id',user.id);
		},
		// 退出登录
		logout({state}){
			// 清除登录状态
			state.user = false;
			// 清除本地存储数据
			$U.removeStorage('token');
			$U.removeStorage('user');
			$U.removeStorage('user_id');
			// 跳转到登录页
			uni.reLaunch({
				url:'/pages/common/login/login'
			})
		},
		// 初始化登录状态
		initLogin({ state }){
			// 拿到存储的数据
			let user = $U.getStorage('user');
			if(user){
				// 初始化登录状态
				state.user=JSON.parse(user);
			    // 连接socket
				// 获取离线信息
			}
		}
	},
}

45搜索用户功能实现

/pages/common/serach/search.js

<template>
	<view class="page">
		<!-- 导航栏 -->
		<free-nav-bar title="我的收藏" showBack :showRight="false">
			<input type="text" v-model="keyword" placeholder="请输入关键字" style="width: 650rpx;" class="font-md" @confirm="confirm"/>
		</free-nav-bar>
		
		<block v-if="searchType==''&&list.length===0">
		<view class="py-3 flex align-center justify-center">
			<text class="font text-light-muted">搜索指定内容</text>
		</view>
		
		<view class="px-4 flex flex-wrap">
			<view class="flex align-center justify-center mb-3" style="width: 223rpx;" v-for="(item,index) in typeList" :key="index">
				<text class="font text-hover-primary">{{item.name}}</text>
			</view>
		</view>
		</block>
		
		<free-list-item v-for="(item,index) in list" :key="index" :title="item.nickname ? item.nickname : item.username" :cover="item.avatar ? item.avatar : '/static/images/userpic.png'"></free-list-item>
	</view>
</template>

<script>
	import freeNavBar from '@/components/free-ui/free-nav-bar.vue';
	import freeListItem from '@/components/free-ui/free-list-item.vue';
	import $H from '@/common/free-lib/request.js';
	export default {
		components:{
			freeNavBar,
			freeListItem
		},
		data() {
			return {
				typeList:[{
					name:'聊天记录',
					key:'history'
				},
				{
					name:'用户',
					key:'user'
				},
				{
					name:'群聊',
					key:'group'
				}],
				keyword:'',
				list:[],
				searchType:''
			}
		},
		methods: {
			confirm(){
				$H.post('/search/user',{keyword:this.keyword}).then(res=>{
					this.list=[];
					if(res){
						this.list.push(res);
					}
					
				})
			}
		}
	}
</script>

<style>

</style>

46查看用户资料功能(一)

user-base.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<free-nav-bar showBack :showRight="true" bgColor="bg-white">
			<free-icon-button slot="right"><text class="iconfont font-md" @click="openAction">&#xe6fd;</text></free-icon-button>
		</free-nav-bar>
		<view class="px-3 py-4 flex align-center bg-white border-bottom">
			<free-avatar src="/static/images/demo/demo6.jpg" size="120"></free-avatar>
			
			<view class="flex flex-column ml-3 flex-1">
				<view class="font-lg font-weight-bold flex justify-between">
					<text class="font-lg font-weight-bold mb-1">{{nickname}}</text>
					<image v-if="detail.star" src="/static/images/star.png" style="width: 40rpx;height: 40rpx;"></image>
				</view>
				<text class="font-md text-light-muted mb-1">账号:VmzhbjzhV</text>
				<text class="font-md text-light-muted">地区:广东广州</text>
			</view>
		</view>
		
		<free-list-item showRight :showLeftIcon="false">
			<view class="flex align-center">
				<text class="font-md text-dark mr-3">标签</text>
				<text class="font-md text-light-muted mr-2" v-for="(item,index) in tagList" :key="index">{{item}}</text>
			</view>
		</free-list-item>
		<free-divider></free-divider>
		<free-list-item showRight :showLeftIcon="false">
			<view class="flex align-center">
				<text class="font-md text-dark mr-3">朋友圈</text>
				<image src="/static/images/demo/cate_01.png" style="width: 90rpx; height: 90rpx;" class=" mr-2"></image>
				<image src="/static/images/demo/cate_01.png" style="width: 90rpx; height: 90rpx;" class=" mr-2"></image>
				<image src="/static/images/demo/cate_01.png" style="width: 90rpx; height: 90rpx;" class=" mr-2"></image>
			</view>
		</free-list-item>
		<free-list-item title="更多信息" showRight :showLeftIcon="false"></free-list-item>
		<free-divider></free-divider>
		<view class="py-3 flex align-center justify-center bg-white" hover-class="bg-light">
			<text class="iconfont text-primary mr-1" v-if="!isBlack">&#xe64e;</text>
			<text class="font-md text-primary">{{isBlack ? '移除黑名单' : '发信息'}}</text>
		</view>
		
		<!-- 扩展菜单 -->
		<free-popup ref="action" bottom transformOrigin="center bottom" maskColor>
			<scroll-view style="height: 580rpx;" scroll-y="true" class="bg-white" :show-scrollbar="false">
				<free-list-item v-for="(item,index) in actions"  :key="index" :title="item.title" :showRight="false" :border="false" @click="popupEvent(item)">
					<text slot="icon" class="iconfont font-lg py-1">{{item.icon}}</text>
				</free-list-item>
			</scroll-view>
		</free-popup>
	</view>
</template>

<script>
	import freeNavBar from '@/components/free-ui/free-nav-bar.vue';
	import freeIconButton from '@/components/free-ui/free-icon-button.vue';
	import freeChatItem from '@/components/free-ui/free-chat-item.vue';
	import freePopup from '@/components/free-ui/free-popup.vue';
	import freeListItem from '@/components/free-ui/free-list-item.vue';
	import freeDivider from '@/components/free-ui/free-divider.vue';
	import freeAvatar from '@/components/free-ui/free-avatar.vue';
	import auth from '@/common/mixin/auth.js';
	import $H from '@/common/free-lib/request.js';
	
	export default {
		mixins:[auth],
		components: {
			freeNavBar,
			freeIconButton,
			freeChatItem,
			freePopup,
			freeListItem,
			freeDivider,
			freeAvatar
		},
		data() {
			return {
				detail:{
					star:false,
					id:0
				},
				isBlack:false,
				tagList:[],
				nickname:'昵称'
			}
		},
		onLoad(e) {
			uni.$on('saveRemarkTag',(e)=>{
				this.tagList = e.tagList
				this.nickname = e.nickname;
			})
			if(!e.user_id){
				return this.backToast();
			}
			this.detail.id =  e.user_id;
			// 获取当前用户资料
			this.getData();
		},
		beforeDestroy() {
			this.$refs.action.hide();
			uni.$off('saveRemarkTag')
		},
		computed:{
			tagPath(){
				return "mail/user-remark-tag/user-remark-tag"
			},
			actions(){
					return [{
						icon:"\ue6b3",
						title:"设置备注和标签",
						type:"navigate",
						path:this.tagPath
					},{
						icon:"\ue613",
						title:"把他推荐给朋友",
						type:"navigate",
						path:"mail/send-card/send-card"
					},{
						icon:"\ue6b0",
						title:this.detail.star ? '取消星标好友' : "设为星标朋友",
						type:"event",
						event:"setStar"
					},{
						icon:"\ue667",
						title:"设置朋友圈和动态权限",
						type:"navigate",
						path:"mail/user-moments-auth/user-moments-auth"
					},{
						icon:"\ue638",
						title:this.detail.isblack ? '移出黑名单' : "加入黑名单",
						type:"event",
						event:"setBlack"
					},{
						icon:"\ue61c",
						title:"投诉",
						type:"navigate",
						path:"mail/user-report/user-report"
					},{
						icon:"\ue638",
						title:"删除",
						type:"event",
						event:"deleteItem"
					}]
				}
		},
		methods: {
			getData(){
				$H.get('/friend/read/'+this.detail.id).then(res=>{
					console.log(res)
				});
			},
			openAction(){
				this.$refs.action.show()
			},
			navigate(url){
				console.log(url)
				uni.navigateTo({
					url: '/pages/'+url,
				});
			},
			// 操作菜单事件
            popupEvent(e){
				if(!e.type){
					return;
				}
				switch(e.type){
					case 'navigate':
					this.navigate(e.path);
					break;
					case 'event':
					this[e.event](e);
					break;
				}
				setTimeout(()=>{
					// 关闭弹出层
					this.$refs.action.hide();
				},150);
			},
			// 设为星标
			setStar(e){
				this.detail.star = !this.detail.star
			},
			// 加入黑名单
			setBlack(e){
				let msg  = '加入黑名单';
				if(this.isBlack){
					msg = '移出黑名单';
				}
				uni.showModal({
					content:'是否要'+msg,
					success:(res)=>{
						if(res.confirm){
							this.isBlack = !this.isBlack;
							e.title = this.isBlack ? '移出黑名单' : '加入黑名单';
							uni.showToast({
								title:msg+'成功',
								icon:'none'
							})
						}
					}
				})
			}
		}
	}
</script>

<style>

</style>

search.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<free-nav-bar title="我的收藏" showBack :showRight="false">
			<input type="text" v-model="keyword" placeholder="请输入关键字" style="width: 650rpx;" class="font-md" @confirm="confirm"/>
		</free-nav-bar>
		
		<block v-if="searchType==''&&list.length===0">
		<view class="py-3 flex align-center justify-center">
			<text class="font text-light-muted">搜索指定内容</text>
		</view>
		
		<view class="px-4 flex flex-wrap">
			<view class="flex align-center justify-center mb-3" style="width: 223rpx;" v-for="(item,index) in typeList" :key="index">
				<text class="font text-hover-primary">{{item.name}}</text>
			</view>
		</view>
		</block>
		
		<free-list-item v-for="(item,index) in list" :key="index" :title="item.nickname ? item.nickname : item.username" :cover="item.avatar ? item.avatar : '/static/images/userpic.png'" @click="open(item.id)"></free-list-item>
	</view>
</template>

<script>
	import freeNavBar from '@/components/free-ui/free-nav-bar.vue';
	import freeListItem from '@/components/free-ui/free-list-item.vue';
	import $H from '@/common/free-lib/request.js';
	export default {
		components:{
			freeNavBar,
			freeListItem
		},
		data() {
			return {
				typeList:[{
					name:'聊天记录',
					key:'history'
				},
				{
					name:'用户',
					key:'user'
				},
				{
					name:'群聊',
					key:'group'
				}],
				keyword:'',
				list:[],
				searchType:''
			}
		},
		methods: {
			confirm(){
				$H.post('/search/user',{keyword:this.keyword}).then(res=>{
					this.list=[];
					if(res){
						this.list.push(res);
					}
					
				})
			},
			// 打开用户资料
			open(id){
				uni.navigateTo({
					url:'../../mail/user-base/user-base?user_id='+id
				})
			}
		}
	}
</script>

<style>

</style>

47查看用户资料功能(二)

egg.js中friend.js

// 查看用户资料
    async read() {
        const { ctx, app } = this;
        let current_user_id = ctx.authUser.id;

        let user_id = ctx.params.id ? parseInt(ctx.params.id) : 0;

        let user = await app.model.User.findOne({
            where: {
                id: user_id,
                status: 1
            },
            attributes: {
                exclude: ['password']
            },
            include: [{
                model: app.model.Moment,
                order: [
                    ['id', 'desc']
                ],
                limit: 1
            }]
        });

        if (!user) {
            ctx.throw(400, '用户不存在');
        }


        let res = {
            id: user.id,
            username: user.username,
            nickname: user.nickname ? user.nickname : user.username,
            avatar: user.avatar,
            sex: user.sex,
            sign: user.sign,
            area: user.area,
            friend: false
        }

        let friend = await app.model.Friend.findOne({
            where: {
                friend_id: user_id,
                user_id: current_user_id
            },
            include: [{
                model: app.model.Tag,
                attributes: ['name']
            }]
        });

        if (friend) {
            res.friend = true
            if (friend.nickname) {
                res.nickname = friend.nickname;
            }
            res = {
                ...res,
                lookme: friend.lookme,
                lookhim: friend.lookhim,
                star: friend.star,
                isblack: friend.isblack,
                tags: friend.tags.map(item => item.name),
                moments: user.moments
            };
        }

        ctx.apiSuccess(res);
    }

48查看用户资料功能(三)

uni-app中的/pages/mail/user-base/user-base.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<free-nav-bar showBack :showRight="detail.friend" bgColor="bg-white">
			<free-icon-button slot="right" v-if="detail.friend"><text class="iconfont font-md" @click="openAction">&#xe6fd;</text></free-icon-button>
		</free-nav-bar>
		<view class="px-3 py-4 flex align-center bg-white border-bottom">
			<free-avatar :src="detail.avatar" size="120"></free-avatar>
			
			<view class="flex flex-column ml-3 flex-1">
				<view class="font-lg font-weight-bold flex justify-between">
					<text class="font-lg font-weight-bold mb-1">{{detail.nickname}}</text>
					<image v-if="detail.star" src="/static/images/star.png" style="width: 40rpx;height: 40rpx;"></image>
				</view>
				<text class="font-md text-light-muted mb-1">账号:{{detail.username}}</text>
				<!-- <text class="font-md text-light-muted">地区:广东广州</text> -->
			</view>
		</view>
		
		<free-list-item v-if="detail.friend" showRight :showLeftIcon="false">
			<view class="flex align-center">
				<text class="font-md text-dark mr-3">标签</text>
				<text class="font-md text-light-muted mr-2" v-for="(item,index) in tagList" :key="index">{{item}}</text>
			</view>
		</free-list-item>
		<free-divider></free-divider>
		<free-list-item v-if="detail.friend" showRight :showLeftIcon="false">
			<view class="flex align-center">
				<text class="font-md text-dark mr-3">朋友圈</text>
				<image src="/static/images/demo/cate_01.png" style="width: 90rpx; height: 90rpx;" class=" mr-2"></image>
				<image src="/static/images/demo/cate_01.png" style="width: 90rpx; height: 90rpx;" class=" mr-2"></image>
				<image src="/static/images/demo/cate_01.png" style="width: 90rpx; height: 90rpx;" class=" mr-2"></image>
			</view>
		</free-list-item>
		<free-list-item title="更多信息" showRight :showLeftIcon="false"></free-list-item>
		<free-divider></free-divider>
		<view v-if="detail.friend" class="py-3 flex align-center justify-center bg-white" hover-class="bg-light">
			<text class="iconfont text-primary mr-1" v-if="!isBlack">&#xe64e;</text>
			<text class="font-md text-primary">{{isBlack ? '移除黑名单' : '发信息'}}</text>
		</view>
		
		<view v-else class="py-3 flex align-center justify-center bg-white" hover-class="bg-light">
			<text class="font-md text-primary">添加好友</text>
		</view>
		
		<!-- 扩展菜单 -->
		<free-popup ref="action" bottom transformOrigin="center bottom" maskColor>
			<scroll-view style="height: 580rpx;" scroll-y="true" class="bg-white" :show-scrollbar="false">
				<free-list-item v-for="(item,index) in actions"  :key="index" :title="item.title" :showRight="false" :border="false" @click="popupEvent(item)">
					<text slot="icon" class="iconfont font-lg py-1">{{item.icon}}</text>
				</free-list-item>
			</scroll-view>
		</free-popup>
	</view>
</template>

<script>
	import freeNavBar from '@/components/free-ui/free-nav-bar.vue';
	import freeIconButton from '@/components/free-ui/free-icon-button.vue';
	import freeChatItem from '@/components/free-ui/free-chat-item.vue';
	import freePopup from '@/components/free-ui/free-popup.vue';
	import freeListItem from '@/components/free-ui/free-list-item.vue';
	import freeDivider from '@/components/free-ui/free-divider.vue';
	import freeAvatar from '@/components/free-ui/free-avatar.vue';
	import auth from '@/common/mixin/auth.js';
	import $H from '@/common/free-lib/request.js';
	
	export default {
		mixins:[auth],
		components: {
			freeNavBar,
			freeIconButton,
			freeChatItem,
			freePopup,
			freeListItem,
			freeDivider,
			freeAvatar
		},
		data() {
			return {
				detail:{
					id:0,
					username:'',
					nickname:'',
					avatar:'',
					sex:'',
					star:false,
					sign:'',
					area:'',
					friend:false
				},
				isBlack:false,
				tagList:[],
			}
		},
		onLoad(e) {
			uni.$on('saveRemarkTag',(e)=>{
				this.tagList = e.tagList
				this.nickname = e.nickname;
			})
			if(!e.user_id){
				return this.backToast();
			}
			this.detail.id =  e.user_id;
			// 获取当前用户资料
			this.getData();
		},
		beforeDestroy() {
			this.$refs.action.hide();
			uni.$off('saveRemarkTag')
		},
		computed:{
			tagPath(){
				return "mail/user-remark-tag/user-remark-tag"
			},
			actions(){
					return [{
						icon:"\ue6b3",
						title:"设置备注和标签",
						type:"navigate",
						path:this.tagPath
					},{
						icon:"\ue613",
						title:"把他推荐给朋友",
						type:"navigate",
						path:"mail/send-card/send-card"
					},{
						icon:"\ue6b0",
						title:this.detail.star ? '取消星标好友' : "设为星标朋友",
						type:"event",
						event:"setStar"
					},{
						icon:"\ue667",
						title:"设置朋友圈和动态权限",
						type:"navigate",
						path:"mail/user-moments-auth/user-moments-auth"
					},{
						icon:"\ue638",
						title:this.detail.isblack ? '移出黑名单' : "加入黑名单",
						type:"event",
						event:"setBlack"
					},{
						icon:"\ue61c",
						title:"投诉",
						type:"navigate",
						path:"mail/user-report/user-report"
					},{
						icon:"\ue638",
						title:"删除",
						type:"event",
						event:"deleteItem"
					}]
				}
		},
		methods: {
			getData(){
				$H.get('/friend/read/'+this.detail.id).then(res=>{
					if(!res){
						return this.backToast('该用户不存在');
					}
					this.detail = res;
				});
			},
			openAction(){
				this.$refs.action.show()
			},
			navigate(url){
				console.log(url)
				uni.navigateTo({
					url: '/pages/'+url,
				});
			},
			// 操作菜单事件
            popupEvent(e){
				if(!e.type){
					return;
				}
				switch(e.type){
					case 'navigate':
					this.navigate(e.path);
					break;
					case 'event':
					this[e.event](e);
					break;
				}
				setTimeout(()=>{
					// 关闭弹出层
					this.$refs.action.hide();
				},150);
			},
			// 设为星标
			setStar(e){
				this.detail.star = !this.detail.star
			},
			// 加入黑名单
			setBlack(e){
				let msg  = '加入黑名单';
				if(this.isBlack){
					msg = '移出黑名单';
				}
				uni.showModal({
					content:'是否要'+msg,
					success:(res)=>{
						if(res.confirm){
							this.isBlack = !this.isBlack;
							e.title = this.isBlack ? '移出黑名单' : '加入黑名单';
							uni.showToast({
								title:msg+'成功',
								icon:'none'
							})
						}
					}
				})
			}
		}
	}
</script>

<style>

</style>

49修复处理好友申请api接口

egg.js 中 app/controler/apply.js

 // 处理好友申请
  async handle(){
     const { ctx,app } = this;
     // 拿到当前用户id
     let current_user_id = ctx.authUser.id;
     let id = parseInt(ctx.params.id); 
     // 验证参数
     ctx.validate({
          nickname:{type: 'string', required: false, desc: '昵称'},
          status:{type: 'string', required: true,range:{in:['refuse','agree','ignore']}, desc: '处理结果'},
          lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'},
          lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'},
     });
     // 查询改申请是否存在
     let apply = await app.model.Apply.findOne({
         where:{
             id,
             friend_id:current_user_id,
             status:'pending'
         }
     });
     if(!apply){
         ctx.throw('400','该记录不存在');
     }
     let {status,nickname,lookhim,lookme} = ctx.request.body;
   
     let transaction;
     try {
        // 开启事务
        transaction = await app.model.transaction();
    
        // 设置该申请状态
        await apply.update({
            status
        }, { transaction });
        // apply.status = status;
        // apply.save();
        // 同意,添加到好友列表
        if (status == 'agree') {
            // 加入到对方好友列表
            await app.model.Friend.create({
                friend_id: current_user_id,
                user_id: apply.user_id,
                nickname: apply.nickname,
                lookme: apply.lookme,
                lookhim: apply.lookhim,
            }, { transaction });
            // 将对方加入到我的好友列表
            await app.model.Friend.create({
                friend_id: apply.user_id,
                user_id: current_user_id,
                nickname,
                lookme,
                lookhim,
            }, { transaction });
        }
        
        // 提交事务
        await transaction.commit();
        // 消息推送
        return ctx.apiSuccess('操作成功');
     } catch (e) {
        // 事务回滚
        await transaction.rollback();
        return ctx.apiFail('操作失败');
     }
  }

50添加好友功能实现

add-friend.vue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<free-nav-bar title="添加好友" showBack :showRight="false">
		</free-nav-bar>
		<view class="flex flex-column">
			<text class="font-sm text-secondary px-3 py-2">备注名</text>
			<input type="text" class="font-md border bg-white px-3" placeholder="请填写备注名" style="height: 100rpx;" v-model="form.nickname"/>
		</view>
		<free-divider></free-divider>
		<free-list-item title="不让他看我" :showLeftIcon="false"
		showRight :showRightIcon="false">
			<switch slot="right" :checked="!!form.lookme" color="#08C060" @change="form.lookme = form.lookme ? 0 : 1"/>
		</free-list-item>
		<free-list-item title="不看他" :showLeftIcon="false"
		showRight :showRightIcon="false">
			<switch slot="right" :checked="!!form.lookhim" color="#08C060" @change="form.lookhim = !form.lookhim ? 0 : 1"/>
		</free-list-item>
		<free-divider></free-divider>
		<view class="py-3 flex align-center justify-center bg-white"
		hover-class="bg-light" @click="submit">
			<text class="font-md text-primary">{{ id > 0 ? '同意' : '点击添加' }}</text>
		</view>
	</view>
</template>

<script>
	import freeNavBar from '@/components/free-ui/free-nav-bar.vue';
	import freeListItem from '@/components/free-ui/free-list-item.vue';
	import freeDivider from '@/components/free-ui/free-divider.vue';
	import $H from '@/common/free-lib/request.js';
	import auth from '@/common/mixin/auth.js';
	export default {
		mixins:[auth],
		components: {
			freeNavBar,
			freeListItem,
			freeDivider
		},
		data() {
			return {
				form:{
					friend_id:0,
					nickname:"",
					lookme:1,
					lookhim:1
				},
				id:0
			}
		},
		onLoad(e) {
			if(e.params){
				this.form = JSON.parse(e.params)
			}
			if(e.id){
				this.id = e.id
			}
		},
		methods: {
			submit(){
				// 添加好友
				if(this.id == 0){
					return $H.post('/apply/addfriend',this.form).then(res=>{
						uni.showToast({
							title: '申请成功',
							icon: 'none'
						});
						uni.navigateBack({
							delta: 1
						});
					})
				}
				// 处理好友申请
				$H.post('/apply/handle/'+this.id,{
					...this.form,
					status:"agree"
				}).then(res=>{
					uni.showToast({ title: '处理成功', icon: 'none' });
					uni.navigateBack({ delta: 1 });
					this.$store.dispatch('getMailList')
				})
			}
		}
	}
</script>

<style>

</style>

页面是酱紫的

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐