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,) => {
return async function errorHandler(ctx,) {
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,) => {
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,) => {
return async function errorHandler(ctx,) {
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,) {
// 先对需要验证的密码进行加密
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,) {
// 先对需要验证的密码进行加密
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,) => {
return async (ctx,) => {
//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,) => {
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,) => {
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获取通讯录列表(二)
安装
-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,) => {
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,) => {
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,) => {
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,) => {
console.log('websocket 开启');
await next();
console.log('websocket 关闭');
});
4. 配置路由中间件
路由会依次用到 app.use, app.ws.use, 以及 app.ws.router 中配置的中间件
// app/router.js
function middleware(ctx,) {
// 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,) => {
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,) => {
// 获取参数 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,) => {
// 用户下线
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>
页面是酱紫的