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"></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"></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"></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"></text> <text class="font-md text-primary">{{isBlack ? '移除黑名单' : '发信息'}}</text> </view> <view v-else class="py-3 flex align-center justify-center bg-white" hover-class="bg-light"> <text class="font-md text-primary">添加好友</text> </view> <!-- 扩展菜单 --> <free-popup ref="action" bottom transformOrigin="center bottom" maskColor> <scroll-view style="height: 580rpx;" scroll-y="true" class="bg-white" :show-scrollbar="false"> <free-list-item v-for="(item,index) in actions" :key="index" :title="item.title" :showRight="false" :border="false" @click="popupEvent(item)"> <text slot="icon" class="iconfont font-lg py-1">{{item.icon}}</text> </free-list-item> </scroll-view> </free-popup> </view> </template> <script> import freeNavBar from '@/components/free-ui/free-nav-bar.vue'; import freeIconButton from '@/components/free-ui/free-icon-button.vue'; import freeChatItem from '@/components/free-ui/free-chat-item.vue'; import freePopup from '@/components/free-ui/free-popup.vue'; import freeListItem from '@/components/free-ui/free-list-item.vue'; import freeDivider from '@/components/free-ui/free-divider.vue'; import freeAvatar from '@/components/free-ui/free-avatar.vue'; import auth from '@/common/mixin/auth.js'; import $H from '@/common/free-lib/request.js'; export default { mixins:[auth], components: { freeNavBar, freeIconButton, freeChatItem, freePopup, freeListItem, freeDivider, freeAvatar }, data() { return { detail:{ id:0, username:'', nickname:'', avatar:'', sex:'', star:false, sign:'', area:'', friend:false }, isBlack:false, tagList:[], } }, onLoad(e) { uni.$on('saveRemarkTag',(e)=>{ this.tagList = e.tagList this.nickname = e.nickname; }) if(!e.user_id){ return this.backToast(); } this.detail.id = e.user_id; // 获取当前用户资料 this.getData(); }, beforeDestroy() { this.$refs.action.hide(); uni.$off('saveRemarkTag') }, computed:{ tagPath(){ return "mail/user-remark-tag/user-remark-tag" }, actions(){ return [{ icon:"\ue6b3", title:"设置备注和标签", type:"navigate", path:this.tagPath },{ icon:"\ue613", title:"把他推荐给朋友", type:"navigate", path:"mail/send-card/send-card" },{ icon:"\ue6b0", title:this.detail.star ? '取消星标好友' : "设为星标朋友", type:"event", event:"setStar" },{ icon:"\ue667", title:"设置朋友圈和动态权限", type:"navigate", path:"mail/user-moments-auth/user-moments-auth" },{ icon:"\ue638", title:this.detail.isblack ? '移出黑名单' : "加入黑名单", type:"event", event:"setBlack" },{ icon:"\ue61c", title:"投诉", type:"navigate", path:"mail/user-report/user-report" },{ icon:"\ue638", title:"删除", type:"event", event:"deleteItem" }] } }, methods: { getData(){ $H.get('/friend/read/'+this.detail.id).then(res=>{ if(!res){ return this.backToast('该用户不存在'); } this.detail = res; }); }, openAction(){ this.$refs.action.show() }, navigate(url){ console.log(url) uni.navigateTo({ url: '/pages/'+url, }); }, // 操作菜单事件 popupEvent(e){ if(!e.type){ return; } switch(e.type){ case 'navigate': this.navigate(e.path); break; case 'event': this[e.event](e); break; } setTimeout(()=>{ // 关闭弹出层 this.$refs.action.hide(); },150); }, // 设为星标 setStar(e){ this.detail.star = !this.detail.star }, // 加入黑名单 setBlack(e){ let msg = '加入黑名单'; if(this.isBlack){ msg = '移出黑名单'; } uni.showModal({ content:'是否要'+msg, success:(res)=>{ if(res.confirm){ this.isBlack = !this.isBlack; e.title = this.isBlack ? '移出黑名单' : '加入黑名单'; uni.showToast({ title:msg+'成功', icon:'none' }) } } }) } } } </script> <style> </style>
49修复处理好友申请api接口
egg.js 中 app/controler/apply.js
// 处理好友申请 async handle(){ const { ctx,app } = this; // 拿到当前用户id let current_user_id = ctx.authUser.id; let id = parseInt(ctx.params.id); // 验证参数 ctx.validate({ nickname:{type: 'string', required: false, desc: '昵称'}, status:{type: 'string', required: true,range:{in:['refuse','agree','ignore']}, desc: '处理结果'}, lookme:{type: 'int', required: true, range:{in:[0,1]},desc: '看我'}, lookhim:{type: 'int', required: true,range:{in:[0,1]}, desc: '看他'}, }); // 查询改申请是否存在 let apply = await app.model.Apply.findOne({ where:{ id, friend_id:current_user_id, status:'pending' } }); if(!apply){ ctx.throw('400','该记录不存在'); } let {status,nickname,lookhim,lookme} = ctx.request.body; let transaction; try { // 开启事务 transaction = await app.model.transaction(); // 设置该申请状态 await apply.update({ status }, { transaction }); // apply.status = status; // apply.save(); // 同意,添加到好友列表 if (status == 'agree') { // 加入到对方好友列表 await app.model.Friend.create({ friend_id: current_user_id, user_id: apply.user_id, nickname: apply.nickname, lookme: apply.lookme, lookhim: apply.lookhim, }, { transaction }); // 将对方加入到我的好友列表 await app.model.Friend.create({ friend_id: apply.user_id, user_id: current_user_id, nickname, lookme, lookhim, }, { transaction }); } // 提交事务 await transaction.commit(); // 消息推送 return ctx.apiSuccess('操作成功'); } catch (e) { // 事务回滚 await transaction.rollback(); return ctx.apiFail('操作失败'); } }
50添加好友功能实现
add-friend.vue
<template> <view class="page"> <!-- 导航栏 --> <free-nav-bar title="添加好友" showBack :showRight="false"> </free-nav-bar> <view class="flex flex-column"> <text class="font-sm text-secondary px-3 py-2">备注名</text> <input type="text" class="font-md border bg-white px-3" placeholder="请填写备注名" style="height: 100rpx;" v-model="form.nickname"/> </view> <free-divider></free-divider> <free-list-item title="不让他看我" :showLeftIcon="false" showRight :showRightIcon="false"> <switch slot="right" :checked="!!form.lookme" color="#08C060" @change="form.lookme = form.lookme ? 0 : 1"/> </free-list-item> <free-list-item title="不看他" :showLeftIcon="false" showRight :showRightIcon="false"> <switch slot="right" :checked="!!form.lookhim" color="#08C060" @change="form.lookhim = !form.lookhim ? 0 : 1"/> </free-list-item> <free-divider></free-divider> <view class="py-3 flex align-center justify-center bg-white" hover-class="bg-light" @click="submit"> <text class="font-md text-primary">{{ id > 0 ? '同意' : '点击添加' }}</text> </view> </view> </template> <script> import freeNavBar from '@/components/free-ui/free-nav-bar.vue'; import freeListItem from '@/components/free-ui/free-list-item.vue'; import freeDivider from '@/components/free-ui/free-divider.vue'; import $H from '@/common/free-lib/request.js'; import auth from '@/common/mixin/auth.js'; export default { mixins:[auth], components: { freeNavBar, freeListItem, freeDivider }, data() { return { form:{ friend_id:0, nickname:"", lookme:1, lookhim:1 }, id:0 } }, onLoad(e) { if(e.params){ this.form = JSON.parse(e.params) } if(e.id){ this.id = e.id } }, methods: { submit(){ // 添加好友 if(this.id == 0){ return $H.post('/apply/addfriend',this.form).then(res=>{ uni.showToast({ title: '申请成功', icon: 'none' }); uni.navigateBack({ delta: 1 }); }) } // 处理好友申请 $H.post('/apply/handle/'+this.id,{ ...this.form, status:"agree" }).then(res=>{ uni.showToast({ title: '处理成功', icon: 'none' }); uni.navigateBack({ delta: 1 }); this.$store.dispatch('getMailList') }) } } } </script> <style> </style>
页面是酱紫的