import { BaseService, CoolCommException } from '@cool-midway/core';
|
import { Config, Inject, Provide } from '@midwayjs/core';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import axios from 'axios';
|
import * as crypto from 'crypto';
|
import * as moment from 'moment';
|
import { Equal, Repository } from 'typeorm';
|
import { v1 as uuid } from 'uuid';
|
import { BaseSysParamEntity } from '../../base/entity/sys/param';
|
import { PluginService } from '../../plugin/service/info';
|
import { UserInfoEntity } from '../entity/info';
|
import { UserWxEntity } from '../entity/wx';
|
|
/**
|
* 微信
|
*/
|
@Provide()
|
export class UserWxService extends BaseService {
|
@Config('module.user')
|
config;
|
|
@Config('wxMini')
|
wxMini: { appId?: string; appSecret?: string };
|
|
@InjectEntityModel(UserInfoEntity)
|
userInfoEntity: Repository<UserInfoEntity>;
|
|
@InjectEntityModel(UserWxEntity)
|
userWxEntity: Repository<UserWxEntity>;
|
|
@InjectEntityModel(BaseSysParamEntity)
|
baseSysParamEntity: Repository<BaseSysParamEntity>;
|
|
@Inject()
|
pluginService: PluginService;
|
|
private miniTokenCache: { appId: string; token: string; expireAt: number };
|
|
/**
|
* 获得插件实例
|
* @returns
|
*/
|
async getPlugin() {
|
try {
|
const wxPlugin: any = await this.pluginService.getInstance('wx');
|
return wxPlugin;
|
} catch (error) {
|
throw new CoolCommException(
|
'未配置微信插件,请到插件市场下载安装配置:https://cool-js.com/plugin/70'
|
);
|
}
|
}
|
|
/**
|
* 获得小程序实例
|
* @returns
|
*/
|
async getMiniApp() {
|
const wxPlugin: any = await this.getPlugin();
|
return wxPlugin.MiniApp();
|
}
|
|
async readParam(key: string) {
|
const row = await this.baseSysParamEntity.findOneBy({ keyName: key });
|
return (row?.data || '').trim();
|
}
|
|
/**
|
* 小程序账号。优先插件,其次系统参数 wx.mini.appId / wx.mini.appSecret
|
*/
|
async getMiniCredential() {
|
try {
|
const app = await this.getMiniApp();
|
const account = app.getAccount();
|
const appId = account.getAppId?.() || account.appId;
|
const secret = account.getSecret?.() || account.appSecret || account.secret;
|
if (appId && secret) {
|
return { appId, secret };
|
}
|
} catch (error) {
|
// 未安装微信插件时改用系统参数
|
}
|
|
const paramAppId = await this.readParam('wx.mini.appId');
|
const paramSecret = await this.readParam('wx.mini.appSecret');
|
const appId = paramAppId || this.wxMini?.appId || '';
|
const secret = paramSecret || this.wxMini?.appSecret || '';
|
if (!appId || !secret) {
|
throw new CoolCommException(
|
'未配置小程序密钥。请在系统参数填写 wx.mini.appSecret(微信公众平台-开发管理-开发设置)'
|
);
|
}
|
return { appId, secret };
|
}
|
|
async getMiniAccessToken() {
|
const { appId, secret } = await this.getMiniCredential();
|
const now = Date.now();
|
if (
|
this.miniTokenCache &&
|
this.miniTokenCache.appId === appId &&
|
this.miniTokenCache.expireAt > now
|
) {
|
return this.miniTokenCache.token;
|
}
|
|
const { data } = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
|
params: {
|
grant_type: 'client_credential',
|
appid: appId,
|
secret,
|
},
|
});
|
if (!data?.access_token) {
|
throw new CoolCommException(data?.errmsg || '获取微信凭证失败,请检查 AppSecret');
|
}
|
this.miniTokenCache = {
|
appId,
|
token: data.access_token,
|
expireAt: now + Math.max(60, (data.expires_in || 7200) - 120) * 1000,
|
};
|
return data.access_token;
|
}
|
|
/**
|
* 获得公众号实例
|
* @returns
|
*/
|
async getOfficialAccount() {
|
const wxPlugin: any = await this.getPlugin();
|
return wxPlugin.OfficialAccount();
|
}
|
|
/**
|
* 获得App实例
|
* @returns
|
*/
|
async getOpenPlatform() {
|
const wxPlugin: any = await this.getPlugin();
|
return wxPlugin.OpenPlatform();
|
}
|
|
/**
|
* 获得用户的openId
|
* @param userId
|
* @param type 0-小程序 1-公众号 2-App
|
*/
|
async getOpenid(userId: string, type = 0) {
|
const user = await this.userInfoEntity.findOneBy({
|
unionid: Equal(userId),
|
status: 1,
|
});
|
if (!user) {
|
throw new CoolCommException('用户不存在或已被禁用');
|
}
|
const wx = await this.userWxEntity
|
.createQueryBuilder('a')
|
.where('a.type = :type', { type })
|
.andWhere('(a.unionid = :unionid or a.openid =:openid )', {
|
unionid: user.unionid,
|
openid: user.unionid,
|
})
|
.getOne();
|
return wx ? wx.openid : null;
|
}
|
|
/**
|
* 获得微信配置
|
* @param appId
|
* @param appSecret
|
* @param url 当前网页的URL,不包含#及其后面部分(必须是调用JS接口页面的完整URL)
|
*/
|
public async getWxMpConfig(url: string) {
|
const token = await this.getWxToken();
|
const ticket = await axios.get(
|
'https://api.weixin.qq.com/cgi-bin/ticket/getticket',
|
{
|
params: {
|
access_token: token,
|
type: 'jsapi',
|
},
|
}
|
);
|
|
const account = (await this.getOfficialAccount()).getAccount();
|
const appid = account.getAppId();
|
// 返回结果集
|
const result = {
|
timestamp: parseInt(moment().valueOf() / 1000 + ''),
|
nonceStr: uuid(),
|
appId: appid, //appid
|
signature: '',
|
};
|
const signArr = [];
|
signArr.push('jsapi_ticket=' + ticket.data.ticket);
|
signArr.push('noncestr=' + result.nonceStr);
|
signArr.push('timestamp=' + result.timestamp);
|
signArr.push('url=' + decodeURI(url));
|
// 敏感信息加密处理
|
result.signature = crypto
|
.createHash('sha1')
|
.update(signArr.join('&'))
|
.digest('hex')
|
.toUpperCase();
|
return result;
|
}
|
|
/**
|
* 获得公众号用户信息
|
* @param code
|
*/
|
async mpUserInfo(code) {
|
const token = await this.openOrMpToken(code, 'mp');
|
return await this.openOrMpUserInfo(token);
|
}
|
|
/**
|
* 获得app用户信息
|
* @param code
|
*/
|
async appUserInfo(code) {
|
const token = await this.openOrMpToken(code, 'open');
|
return await this.openOrMpUserInfo(token);
|
}
|
|
/**
|
* 获得微信token 不用code
|
* @param appid
|
* @param secret
|
*/
|
public async getWxToken(type = 'mp') {
|
let app;
|
if (type == 'mp') {
|
app = await this.getOfficialAccount();
|
} else {
|
app = await this.getOpenPlatform();
|
}
|
return await app.getAccessToken().getToken();
|
}
|
|
/**
|
* 获得用户信息
|
* @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 type
|
*/
|
async openOrMpToken(code, type = 'mp') {
|
const account =
|
type == 'mp'
|
? (await this.getOfficialAccount()).getAccount()
|
: (await this.getMiniApp()).getAccount();
|
const result = await axios.get(
|
'https://api.weixin.qq.com/sns/oauth2/access_token',
|
{
|
params: {
|
appid: account.getAppId(),
|
secret: account.getSecret(),
|
code,
|
grant_type: 'authorization_code',
|
},
|
}
|
);
|
return result.data;
|
}
|
|
/**
|
* 获得小程序session
|
* @param code 微信code
|
* @param conf 配置
|
*/
|
async miniSession(code) {
|
const { appId, secret } = await this.getMiniCredential();
|
const { data } = await axios.get(
|
'https://api.weixin.qq.com/sns/jscode2session',
|
{
|
params: {
|
appid: appId,
|
secret,
|
js_code: code,
|
grant_type: 'authorization_code',
|
},
|
}
|
);
|
console.log('[miniSession] appId=', appId, 'code前6位=', String(code).slice(0,6), 'ret=', JSON.stringify(data));
|
return 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) {
|
// 新版获取手机号只返回动态令牌,由服务端向微信换取手机号
|
if (!encryptedData || !iv) {
|
return await this.getPhoneNumber(code);
|
}
|
|
const session = await this.miniSession(code);
|
if (session.errcode) {
|
throw new CoolCommException('获取手机号失败,请刷新重试');
|
}
|
const result = await this.miniDecryptData(
|
encryptedData,
|
iv,
|
session.session_key
|
);
|
return result.phoneNumber;
|
}
|
|
/**
|
* 用手机号动态令牌换取手机号
|
* @param code getPhoneNumber 返回的 code
|
*/
|
async getPhoneNumber(code: string) {
|
if (!code) {
|
throw new CoolCommException('未获取到手机号授权');
|
}
|
|
const accessToken = await this.getMiniAccessToken();
|
|
const { data } = await axios.post(
|
'https://api.weixin.qq.com/wxa/business/getuserphonenumber',
|
{ code },
|
{
|
params: {
|
access_token: accessToken,
|
},
|
}
|
);
|
|
if (data?.errcode) {
|
throw new CoolCommException(data.errmsg || '获取手机号失败');
|
}
|
|
const phone =
|
data?.phone_info?.purePhoneNumber || data?.phone_info?.phoneNumber;
|
|
if (!phone) {
|
throw new CoolCommException('获得手机号失败,请检查配置');
|
}
|
|
return phone;
|
}
|
|
/**
|
* 小程序信息解密
|
* @param encryptedData
|
* @param iv
|
* @param sessionKey
|
*/
|
async miniDecryptData(encryptedData, iv, sessionKey) {
|
const app = await this.getMiniApp();
|
const utils = app.getUtils();
|
return await utils.decryptSession(sessionKey, iv, encryptedData);
|
}
|
}
|