diff --git a/package.json b/package.json index 2706e18..46cae24 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "一个项目用COOL就够了", "private": true, "dependencies": { - "@cool-midway/core": "^6.0.1", "@cool-midway/cloud": "^6.0.0", + "@cool-midway/core": "^6.0.1", "@cool-midway/file": "^6.0.0", "@cool-midway/iot": "^6.0.0", "@cool-midway/pay": "^6.0.0", @@ -24,6 +24,8 @@ "@midwayjs/typeorm": "^3.10.7", "@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", diff --git a/src/modules/user/config.ts b/src/modules/user/config.ts new file mode 100644 index 0000000..5a2cad1 --- /dev/null +++ b/src/modules/user/config.ts @@ -0,0 +1,40 @@ +import { ModuleConfig } from '@cool-midway/core'; + +/** + * 模块配置 + */ +export default () => { + return { + // 模块名称 + name: '用户模块', + // 模块描述 + description: 'APP、小程序、公众号等用户', + // 中间件,只对本模块有效 + middlewares: [], + // 中间件,全局有效 + globalMiddlewares: [], + // 模块加载顺序,默认为0,值越大越优先加载 + order: 0, + // 阿里云短信 + sms: { + signName: '', + templateCode: ' ', + accessKeyId: '', + accessKeySecret: '', + // 验证码有效期,单位秒 + timeout: 60 * 3, + }, + // 微信配置 + wx: { + // 小程序 + mini: { + appid: 'xxx', + secret: 'xxx', + }, + mp: { + appid: 'xxx', + secret: 'xxx', + }, + }, + } as ModuleConfig; +}; diff --git a/src/modules/user/entity/info.ts b/src/modules/user/entity/info.ts new file mode 100644 index 0000000..24ee5ba --- /dev/null +++ b/src/modules/user/entity/info.ts @@ -0,0 +1,28 @@ +import { BaseEntity } from '@cool-midway/core'; +import { Column, Entity, Index } from 'typeorm'; + +/** + * 用户信息 + */ +@Entity('user_info') +export class UserInfoEntity extends BaseEntity { + @Index() + @Column({ comment: '第三方登录的唯一ID,如:微信、QQ等' }) + unionid: string; + + @Column({ comment: '头像' }) + avatarUrl: string; + + @Column({ comment: '昵称' }) + nickName: string; + + @Index({ unique: true }) + @Column({ comment: '手机号' }) + phone: string; + + @Column({ comment: '性别 0-未知 1-男 2-女', default: 0 }) + gender: number; + + @Column({ comment: '状态 0-正常 1-禁用', default: 0 }) + status: number; +} diff --git a/src/modules/user/entity/wx.ts b/src/modules/user/entity/wx.ts new file mode 100644 index 0000000..925aad0 --- /dev/null +++ b/src/modules/user/entity/wx.ts @@ -0,0 +1,29 @@ +import { BaseEntity } from '@cool-midway/core'; +import { Column, Entity } from 'typeorm'; + +/** + * 微信用户 + */ +@Entity('user_wx') +export class UserWxEntity extends BaseEntity { + @Column({ comment: '头像', nullable: true }) + avatarUrl: number; + + @Column({ comment: '昵称', nullable: true }) + nickName: string; + + @Column({ comment: '性别 0-未知 1-男 2-女', default: 0 }) + gender: number; + + @Column({ comment: '语言' }) + language: number; + + @Column({ comment: '城市' }) + city: number; + + @Column({ comment: '省份' }) + province: number; + + @Column({ comment: '国家' }) + country: number; +} diff --git a/src/modules/user/service/sms.ts b/src/modules/user/service/sms.ts new file mode 100644 index 0000000..96de380 --- /dev/null +++ b/src/modules/user/service/sms.ts @@ -0,0 +1,61 @@ +import { Provide, Config, Inject } from '@midwayjs/decorator'; +import { BaseService } from '@cool-midway/core'; +import * as _ from 'lodash'; +import * as Core from '@alicloud/pop-core'; +import { CacheManager } from '@midwayjs/cache'; + +/** + * 描述 + */ +@Provide() +export class UserSmsService extends BaseService { + // 获得模块的配置信息 + @Config('module.user.sms') + config; + + @Inject() + cacheManager: CacheManager; + + /** + * 发送验证码 + * @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 + ); + } + + /** + * 发送短信 + * @param phone + * @param templateCode + * @param template + */ + async send(phone, TemplateParam) { + const { signName, accessKeyId, accessKeySecret, templateCode } = + this.config; + const client = new Core({ + accessKeyId, + accessKeySecret, + endpoint: 'https://dysmsapi.aliyuncs.com', + // endpoint: 'https://cs.cn-hangzhou.aliyuncs.com', + apiVersion: '2017-05-25', + // apiVersion: '2018-04-18', + }); + const params = { + RegionId: 'cn-shanghai', + PhoneNumbers: phone, + signName, + templateCode, + TemplateParam: JSON.stringify(TemplateParam), + }; + return await client.request('SendSms', params, { + method: 'POST', + }); + } +} diff --git a/src/modules/user/service/wx.ts b/src/modules/user/service/wx.ts new file mode 100644 index 0000000..2d2ec87 --- /dev/null +++ b/src/modules/user/service/wx.ts @@ -0,0 +1,167 @@ +import { Config, Provide } from '@midwayjs/decorator'; +import { BaseService, CoolCommException } from '@cool-midway/core'; +import axios from 'axios'; +import * as crypto from 'crypto'; + +/** + * 微信 + */ +@Provide() +export class UserWxService extends BaseService { + @Config('module.user') + config; + + /** + * 获得公众号用户信息 + * @param code + */ + async mpUserInfo(code) { + const token = await this.openOrMpToken(code, this.config.wx.mp); + return await this.openOrMpUserInfo(token); + } + + /** + * 获得微信token 不用code + * @param appid + * @param secret + */ + public async getWxToken(type = 'mp') { + //@ts-ignore + const conf = this.config.wx[type]; + return await axios.get('https://api.weixin.qq.com/cgi-bin/token', { + params: { + grant_type: 'client_credential', + appid: conf.appid, + secret: conf.secret, + }, + }); + } + + /** + * 获得用户信息 + * @param token + */ + async openOrMpUserInfo(token) { + return await axios + .get('https://api.weixin.qq.com/sns/userinfo', { + params: { + access_token: token.access_token, + openid: token.openid, + lang: 'zh_CN', + }, + }) + .then(res => { + return res.data; + }); + } + + /** + * 获得token嗯 + * @param code + * @param conf + */ + async openOrMpToken(code, conf) { + const result = await axios.get( + 'https://api.weixin.qq.com/sns/oauth2/access_token', + { + params: { + appid: conf.appid, + secret: conf.secret, + code, + grant_type: 'authorization_code', + }, + } + ); + return result.data; + } + + /** + * 获得小程序session + * @param code 微信code + * @param conf 配置 + */ + async miniSession(code) { + const { appid, secret } = this.config.wx.mini; + const result = await axios.get( + 'https://api.weixin.qq.com/sns/jscode2session', + { + params: { + appid, + secret, + js_code: code, + grant_type: 'authorization_code', + }, + } + ); + + return result.data; + } + + /** + * 获得小程序用户信息 + * @param code + * @param encryptedData + * @param iv + */ + async miniUserInfo(code, encryptedData, iv) { + const session = await this.miniSession(code); + if (session.errcode) { + throw new CoolCommException('登录失败,请重试'); + } + const info: any = await this.miniDecryptData( + encryptedData, + iv, + session.session_key + ); + if (info) { + delete info['watermark']; + return { + ...info, + openid: session['openid'], + unionid: session['unionid'], + }; + } + return null; + } + + /** + * 获得小程序手机 + * @param code + * @param encryptedData + * @param iv + */ + async miniPhone(code, encryptedData, iv) { + const session = await this.miniSession(code); + if (session.errcode) { + throw new CoolCommException('获取手机号失败,请刷新重试'); + } + return await this.miniDecryptData(encryptedData, iv, session.session_key); + } + + /** + * 小程序信息解密 + * @param encryptedData + * @param iv + * @param sessionKey + */ + async miniDecryptData(encryptedData, iv, sessionKey) { + sessionKey = Buffer.from(sessionKey, 'base64'); + encryptedData = Buffer.from(encryptedData, 'base64'); + iv = Buffer.from(iv, 'base64'); + try { + // 解密 + const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv); + // 设置自动 padding 为 true,删除填充补位 + decipher.setAutoPadding(true); + // @ts-ignore + let decoded = decipher.update(encryptedData, 'binary', 'utf8'); + // @ts-ignore + decoded += decipher.final('utf8'); + // @ts-ignore + decoded = JSON.parse(decoded); + return decoded; + } catch (err) { + throw new CoolCommException('获得信息失败'); + } + } +}