uni-app 25后端api开发和前后端交互(1-50)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: uni-app 25后端api开发和前后端交互(1-50)

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">
    <!-- 导航栏 -->