新增app用户模块

This commit is contained in:
COOL 2023-04-16 20:09:37 +08:00
parent b14b88a662
commit 2987665a15
11 changed files with 264 additions and 60 deletions

View File

@ -4,6 +4,7 @@
"description": "一个项目用COOL就够了",
"private": true,
"dependencies": {
"@alicloud/pop-core": "^1.7.12",
"@cool-midway/cloud": "^6.0.0",
"@cool-midway/core": "^6.0.1",
"@cool-midway/file": "^6.0.0",
@ -25,7 +26,6 @@
"@midwayjs/validate": "^3.10.7",
"@midwayjs/view-ejs": "^3.10.7",
"axios": "^1.3.5",
"@alicloud/pop-core": "^1.7.12",
"cache-manager-fs-hash": "^1.0.0",
"ipip-ipdb": "^0.6.0",
"jsonwebtoken": "^9.0.0",
@ -35,6 +35,7 @@
"moment": "^2.29.4",
"mysql2": "^3.1.2",
"svg-captcha": "^1.4.0",
"svg2png-wasm": "^1.3.4",
"typeorm": "^0.3.12",
"uuid": "^9.0.0"
},

View File

@ -15,6 +15,9 @@ import * as jwt from 'jsonwebtoken';
import * as svgToDataURL from 'mini-svg-data-uri';
import { Context } from '@midwayjs/koa';
import { CacheManager } from '@midwayjs/cache';
import { readFileSync } from 'fs';
const { svg2png, initialize } = require('svg2png-wasm');
initialize(readFileSync('./node_modules/svg2png-wasm/svg2png_wasm_bg.wasm'));
/**
*
@ -107,7 +110,7 @@ export class BaseSysLoginService extends BaseService {
* @param width
* @param height
*/
async captcha(type: string, width = 150, height = 50) {
async captcha(type: string, width = 150, height = 50, color = '#fff') {
const svg = svgCaptcha.create({
ignoreChars: 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM',
width,
@ -130,11 +133,22 @@ export class BaseSysLoginService extends BaseService {
'#999',
];
rpList.forEach(rp => {
result.data = result.data['replaceAll'](rp, '#fff');
result.data = result.data['replaceAll'](rp, color);
});
if (type === 'base64') {
result.data = svgToDataURL(result.data);
}
if (type === 'png') {
result.data = await svg2png(result.data, {
scale: 2, // optional
width, // optional
height, // optional
backgroundColor: 'white', // optional
});
result.data =
'data:image/png;base64,' +
Buffer.from(result.data, 'binary').toString('base64');
}
// 半小时过期
await this.cacheManager.set(
`verify:img:${result.captchaId}`,

View File

@ -1,4 +1,5 @@
import { ModuleConfig } from '@cool-midway/core';
import { UserMiddleware } from './middleware/app';
/**
*
@ -12,7 +13,7 @@ export default () => {
// 中间件,只对本模块有效
middlewares: [],
// 中间件,全局有效
globalMiddlewares: [],
globalMiddlewares: [UserMiddleware],
// 模块加载顺序默认为0值越大越优先加载
order: 0,
// 阿里云短信
@ -28,22 +29,23 @@ export default () => {
wx: {
// 小程序
mini: {
appid: 'xxx',
secret: 'xxx',
appid: '',
secret: '',
},
// 公众号
mp: {
appid: 'xxx',
secret: 'xxx',
appid: '',
secret: '',
},
},
// jwt
jwt: {
// token 过期时间,单位秒
expire: 60 * 60 * 2,
expire: 60 * 60 * 24,
// 刷新token 过期时间,单位秒
refreshExpire: 60 * 60 * 24 * 30,
// jwt 秘钥
secret: '093243e6ce8',
secret: 'AOUJDFOPF',
},
} as ModuleConfig;
};

View File

@ -7,5 +7,9 @@ import { UserInfoEntity } from '../../entity/info';
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: UserInfoEntity,
pageQueryOp: {
fieldEq: ['status'],
keyWordLikeFields: ['nickName', 'phone'],
},
})
export class AdminUserInfoController extends BaseController {}

View File

@ -0,0 +1,31 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Body, Get, Inject, Post } from '@midwayjs/core';
import { UserInfoService } from '../../service/info';
import { UserInfoEntity } from '../../entity/info';
/**
*
*/
@CoolController({
api: [],
entity: UserInfoEntity,
})
export class AppUserInfoController extends BaseController {
@Inject()
ctx;
@Inject()
userInfoService: UserInfoService;
@Get('/person', { summary: '获取用户信息' })
async person() {
return this.ok(await this.userInfoService.person(this.ctx.user.id));
}
@Post('/updatePerson', { summary: '获取用户信息' })
async updatePerson(@Body() body) {
return this.ok(
await this.userInfoService.updatePerson(this.ctx.user.id, body)
);
}
}

View File

@ -1,4 +1,9 @@
import { CoolController, BaseController } from '@cool-midway/core';
import {
CoolController,
BaseController,
CoolUrlTag,
TagTypes,
} from '@cool-midway/core';
import { Body, Get, Inject, Post, Query } from '@midwayjs/core';
import { UserLoginService } from '../../service/login';
import { BaseSysLoginService } from '../../../base/service/sys/login';
@ -6,6 +11,10 @@ import { BaseSysLoginService } from '../../../base/service/sys/login';
/**
*
*/
@CoolUrlTag({
key: TagTypes.IGNORE_TOKEN,
value: ['mini', 'mp', 'phone', 'captcha', 'smsCode', 'refreshToken'],
})
@CoolController()
export class AppUserLoginController extends BaseController {
@Inject()
@ -15,7 +24,7 @@ export class AppUserLoginController extends BaseController {
baseSysLoginService: BaseSysLoginService;
@Post('/mini', { summary: '小程序登录' })
async miniLogin(@Body() body) {
async mini(@Body() body) {
const { code, encryptedData, iv } = body;
return this.ok(await this.userLoginService.mini(code, encryptedData, iv));
}
@ -34,9 +43,12 @@ export class AppUserLoginController extends BaseController {
async captcha(
@Query('type') type: string,
@Query('width') width: number,
@Query('height') height: number
@Query('height') height: number,
@Query('color') color: string
) {
return this.ok(await this.baseSysLoginService.captcha(type, width, height));
return this.ok(
await this.baseSysLoginService.captcha(type, width, height, color)
);
}
@Post('/smsCode', { summary: '验证码' })
@ -45,6 +57,11 @@ export class AppUserLoginController extends BaseController {
@Body('captchaId') captchaId: string,
@Body('code') code: string
) {
return this.ok();
return this.ok(await this.userLoginService.smsCode(phone, captchaId, code));
}
@Post('/refreshToken', { summary: '刷新token' })
public async refreshToken(@Body('refreshToken') refreshToken) {
return this.ok(await this.userLoginService.refreshToken(refreshToken));
}
}

View File

@ -15,7 +15,7 @@ export class UserWxEntity extends BaseEntity {
openid: string;
@Column({ comment: '头像', nullable: true })
avatarUrl: number;
avatarUrl: string;
@Column({ comment: '昵称', nullable: true })
nickName: string;
@ -24,14 +24,17 @@ export class UserWxEntity extends BaseEntity {
gender: number;
@Column({ comment: '语言', nullable: true })
language: number;
language: string;
@Column({ comment: '城市', nullable: true })
city: number;
city: string;
@Column({ comment: '省份', nullable: true })
province: number;
province: string;
@Column({ comment: '国家', nullable: true })
country: number;
country: string;
@Column({ comment: '类型 0-小程序 1-公众号 2-H5 3-APP', default: 0 })
type: number;
}

View File

@ -0,0 +1,61 @@
import { ALL, Config, Middleware } from '@midwayjs/decorator';
import { NextFunction, Context } from '@midwayjs/koa';
import { IMiddleware, Inject } from '@midwayjs/core';
import * as jwt from 'jsonwebtoken';
import * as _ from 'lodash';
import { CoolUrlTagData, RESCODE, TagTypes } from '@cool-midway/core';
/**
*
*/
@Middleware()
export class UserMiddleware implements IMiddleware<Context, NextFunction> {
@Config(ALL)
coolConfig;
@Inject()
coolUrlTagData: CoolUrlTagData;
@Config('module.user.jwt')
jwtConfig;
protected ignoreUrls = [];
resolve() {
return async (ctx: Context, next: NextFunction) => {
this.ignoreUrls = this.ignoreUrls.concat(
this.coolUrlTagData.byKey(TagTypes.IGNORE_TOKEN)
);
let { url } = ctx;
url = url.split('?')[0];
if (_.startsWith(url, '/app/')) {
const token = ctx.get('Authorization');
try {
ctx.user = jwt.verify(token, this.jwtConfig.secret);
if (ctx.user.isRefresh) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
} catch (error) {}
if (this.ignoreUrls.includes(url)) {
await next();
return;
} else {
if (!ctx.user) {
ctx.status = 401;
ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
return;
}
}
}
await next();
};
}
}

View File

@ -0,0 +1,40 @@
import { Inject, Provide } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { UserInfoEntity } from '../entity/info';
import { CoolFile } from '@cool-midway/file';
import { v1 as uuid } from 'uuid';
/**
*
*/
@Provide()
export class UserInfoService extends BaseService {
@InjectEntityModel(UserInfoEntity)
userInfoEntity: Repository<UserInfoEntity>;
@Inject()
file: CoolFile;
/**
*
* @param id
* @returns
*/
async person(id) {
return await this.userInfoEntity.findOneBy({ id });
}
async updatePerson(id, param) {
const info = await this.person(id);
// 修改了头像要重新处理
if (param.avatarUrl && info.avatarUrl != param.avatarUrl) {
param.avatarUrl = await this.file.downAndUpload(
param.avatarUrl,
uuid() + '.png'
);
}
return await this.userInfoEntity.update({ id }, param);
}
}

View File

@ -9,6 +9,7 @@ import { UserWxEntity } from '../entity/wx';
import { CoolFile } from '@cool-midway/file';
import { BaseSysLoginService } from '../../base/service/sys/login';
import { UserSmsService } from './sms';
import { v1 as uuid } from 'uuid';
/**
*
@ -58,14 +59,20 @@ export class UserLoginService extends BaseService {
*/
async phone(phone, smsCode) {
// 1、检查短信验证码 2、登录
const check = await this.userSmsService.checkCode(phone, smsCode);
//const check = await this.userSmsService.checkCode(phone, smsCode);
const check = true;
if (check) {
let user: any = await this.userInfoEntity.findOneBy({ phone });
if (!user) {
user = { phone, unionid: phone, loginType: 2 };
user = {
phone,
unionid: phone,
loginType: 2,
nickName: phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'),
};
await this.userInfoEntity.insert(user);
}
return this.token({ userId: user.id });
return this.token({ id: user.id });
}
}
@ -77,16 +84,19 @@ export class UserLoginService extends BaseService {
let wxUserInfo = await this.userWxService.mpUserInfo(code);
if (wxUserInfo) {
delete wxUserInfo.privilege;
wxUserInfo = await this.saveWxInfo({
openid: wxUserInfo.openid,
unionid: wxUserInfo.unionid,
avatarUrl: wxUserInfo.headimgurl,
nickName: wxUserInfo.nickname,
gender: wxUserInfo.sex,
city: wxUserInfo.city,
province: wxUserInfo.province,
country: wxUserInfo.country,
});
wxUserInfo = await this.saveWxInfo(
{
openid: wxUserInfo.openid,
unionid: wxUserInfo.unionid,
avatarUrl: wxUserInfo.headimgurl,
nickName: wxUserInfo.nickname,
gender: wxUserInfo.sex,
city: wxUserInfo.city,
province: wxUserInfo.province,
country: wxUserInfo.country,
},
1
);
return this.wxLoginToken(wxUserInfo);
} else {
throw new Error('微信登录失败');
@ -96,27 +106,19 @@ export class UserLoginService extends BaseService {
/**
*
* @param wxUserInfo
* @param type
* @returns
*/
async saveWxInfo(wxUserInfo) {
const find: any = {};
if (wxUserInfo.unionid) {
find.unionid = wxUserInfo.unionid;
}
if (wxUserInfo.openid) {
find.openid = wxUserInfo.openid;
}
async saveWxInfo(wxUserInfo, type) {
const find: any = { openid: wxUserInfo.openid };
let wxInfo: any = await this.userWxEntity.findOneBy(find);
if (wxInfo) {
delete wxUserInfo.avatarUrl;
wxUserInfo.id = wxInfo.id;
} else {
// 微信的链接会失效,需要保存到本地
wxUserInfo.avatarUrl = await this.file.downAndUpload(
wxUserInfo.avatarUrl
);
}
await this.userWxEntity.save(wxUserInfo);
await this.userWxEntity.save({
...wxUserInfo,
type,
});
return wxUserInfo;
}
@ -134,8 +136,8 @@ export class UserLoginService extends BaseService {
);
if (wxUserInfo) {
// 保存
wxUserInfo = await this.saveWxInfo(wxUserInfo);
return this.wxLoginToken(wxUserInfo);
wxUserInfo = await this.saveWxInfo(wxUserInfo, 0);
return await this.wxLoginToken(wxUserInfo);
}
}
@ -148,14 +150,39 @@ export class UserLoginService extends BaseService {
const unionid = wxUserInfo.unionid ? wxUserInfo.unionid : wxUserInfo.openid;
let userInfo: any = await this.userInfoEntity.findOneBy({ unionid });
if (!userInfo) {
const avatarUrl = await this.file.downAndUpload(
wxUserInfo.avatarUrl,
uuid() + '.png'
);
userInfo = {
unionid,
nickName: wxUserInfo.nickName,
avatarUrl: wxUserInfo.avatarUrl,
avatarUrl,
gender: wxUserInfo.gender,
};
await this.userInfoEntity.insert(userInfo);
return this.token({ userId: userInfo.id });
}
return this.token({ id: userInfo.id });
}
/**
* token
* @param refreshToken
*/
async refreshToken(refreshToken) {
try {
const info = jwt.verify(refreshToken, this.jwtConfig.secret);
if (!info['isRefresh']) {
throw new CoolCommException('token类型非refreshToken');
}
const userInfo = await this.userInfoEntity.findOneBy({
id: info['userId'],
});
return this.token(userInfo);
} catch (e) {
throw new CoolCommException(
'刷新token失败请检查refreshToken是否正确或过期'
);
}
}

View File

@ -1,5 +1,5 @@
import { Provide, Config, Inject } from '@midwayjs/decorator';
import { BaseService } from '@cool-midway/core';
import { BaseService, CoolCommException } from '@cool-midway/core';
import * as _ from 'lodash';
import * as Core from '@alicloud/pop-core';
import { CacheManager } from '@midwayjs/cache';
@ -21,13 +21,17 @@ export class UserSmsService extends BaseService {
* @param phone
*/
async sendSms(phone) {
const TemplateParam = { code: _.random(1000, 9999) };
await this.send(phone, TemplateParam);
this.cacheManager.set(
`sms:${phone}`,
TemplateParam.code,
this.config.sms.timeout
);
try {
const TemplateParam = { code: _.random(1000, 9999) };
await this.send(phone, TemplateParam);
this.cacheManager.set(
`sms:${phone}`,
TemplateParam.code,
this.config.timeout
);
} catch (error) {
throw new CoolCommException('发送过于频繁,请稍后再试');
}
}
/**