update
This commit is contained in:
@@ -4,8 +4,6 @@ const accessURL = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_cre
|
||||
type AccessData = {
|
||||
"access_token": string;
|
||||
"expires_in": number; // 7200, 单位秒 2小时
|
||||
"accessToken": string;
|
||||
"expiredAt": number; // 到期时间戳,单位毫秒
|
||||
}
|
||||
|
||||
type ErrorData = {
|
||||
@@ -23,26 +21,16 @@ export const getAccessToken = async (appId: string, appSecret: string): Promise<
|
||||
}> => {
|
||||
const url = getAccessURL(appId, appSecret);
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
const data = await response.json() as AccessData | ErrorData;
|
||||
console.log('Access token response:', data);
|
||||
if ((data as ErrorData).errcode) {
|
||||
return {
|
||||
code: 500,
|
||||
message: (data as ErrorData).errmsg,
|
||||
}
|
||||
}
|
||||
console.log('access token data', data);
|
||||
data.accessToken = data.access_token;
|
||||
data.expiredAt = Date.now() + data.expires_in * 1000;
|
||||
return {
|
||||
code: 200,
|
||||
data
|
||||
data: data as AccessData,
|
||||
}
|
||||
}
|
||||
|
||||
if(require.main === module) {
|
||||
// 测试代码
|
||||
const appId = 'wxff97d569b1db16b6';
|
||||
const appSecret = '012d84d0d2b914de95f4e9ca84923aed';
|
||||
const res = await getAccessToken(appId, appSecret);
|
||||
console.log('getAccessToken res', res);
|
||||
}
|
||||
26
src/routes/user/modules/get-cache-access-token.ts
Normal file
26
src/routes/user/modules/get-cache-access-token.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { redis } from '@/app.ts'
|
||||
import { getAccessToken } from './get-access-token.ts';
|
||||
import { config } from '@/modules/config.ts';
|
||||
|
||||
/**
|
||||
* 公众号获取缓存的 access token,和获取的平台的 access token 是分开的
|
||||
* @param appId
|
||||
* @returns
|
||||
*/
|
||||
export const getCacheAccessToken = async (): Promise<string | null> => {
|
||||
const appId = config.WX_MP_APP_ID;
|
||||
const appSecret = config.WX_MP_APP_SECRET;
|
||||
const cacheKey = `wx:access_token:${appId}`;
|
||||
let accessToken = await redis.get(cacheKey);
|
||||
if (!accessToken) {
|
||||
const { code, data, message } = await getAccessToken(appId, appSecret);
|
||||
if (code === 200 && data) {
|
||||
accessToken = data.access_token;
|
||||
await redis.set(cacheKey, accessToken, 'EX', data.expires_in - 200); // 提前200秒过期
|
||||
} else {
|
||||
console.error('Error getting access token:', message);
|
||||
throw new Error(message || 'Error getting access token');
|
||||
}
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { WxTokenResponse, fetchToken, getUserInfo } from './wx.ts';
|
||||
import { WxTokenResponse, fetchToken, getUserInfo, getUserInfoByMp, post } from './wx.ts';
|
||||
import { useContextKey } from '@kevisual/use-config/context';
|
||||
import { UserModel } from '@kevisual/code-center-module';
|
||||
import { Buffer } from 'buffer';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { getCacheAccessToken } from './get-cache-access-token.ts';
|
||||
import { redis } from '@/app.ts';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
|
||||
const User = useContextKey<typeof UserModel>('UserModel');
|
||||
export class WxServices {
|
||||
@@ -89,6 +91,7 @@ export class WxServices {
|
||||
let data = {
|
||||
...user.data,
|
||||
wxUnionId: unionid,
|
||||
canChangeUsername: true,
|
||||
};
|
||||
user.data = data;
|
||||
if ((type = 'mp')) {
|
||||
@@ -100,7 +103,7 @@ export class WxServices {
|
||||
}
|
||||
this.user = await user.save({ fields: ['data'] });
|
||||
|
||||
this.getUserInfo();
|
||||
await this.getUserInfo();
|
||||
this.isNew = true;
|
||||
}
|
||||
this.user = user;
|
||||
@@ -122,16 +125,96 @@ export class WxServices {
|
||||
isNew: this.isNew,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 通过ticket登录
|
||||
* @param msgInfo
|
||||
*/
|
||||
async loginByTicket(msgInfo: { openid: string, ticket: string }) {
|
||||
const { ticket, openid } = msgInfo;
|
||||
const key = `wx:mp:login:qrcode:${ticket}`;
|
||||
const access_token = await getCacheAccessToken();
|
||||
this.wxToken = {
|
||||
access_token: access_token,
|
||||
expires_in: 7200,
|
||||
refresh_token: '',
|
||||
openid: openid,
|
||||
scope: '',
|
||||
unionid: '',
|
||||
};
|
||||
const userInfo = await getUserInfoByMp(access_token, openid);
|
||||
const { unionid } = userInfo;
|
||||
let user = await User.findOne({
|
||||
where: {
|
||||
data: {
|
||||
wxUnionId: unionid,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
const username = await this.randomUsername();
|
||||
user = await User.createUser(username, nanoid(10));
|
||||
let data = {
|
||||
...user.data,
|
||||
wxUnionId: unionid,
|
||||
canChangeUsername: true,
|
||||
};
|
||||
user.data = data;
|
||||
// @ts-ignore
|
||||
data.wxmpOpenid = openid;
|
||||
const fileds = ['data']
|
||||
const { nickname, headimgurl } = userInfo;
|
||||
if (nickname) {
|
||||
this.user.nickname = nickname;
|
||||
fileds.push('nickname');
|
||||
}
|
||||
if (headimgurl) {
|
||||
const image = await this.downloadImg(headimgurl);
|
||||
this.user.avatar = image;
|
||||
fileds.push('avatar');
|
||||
}
|
||||
this.isNew = true;
|
||||
this.user = await user.save({ fields: fileds });
|
||||
}
|
||||
this.user = user;
|
||||
const tokenInfo = await user.createToken(null, 'plugin', {
|
||||
wx: {
|
||||
openid: openid,
|
||||
unionid: unionid,
|
||||
type: 'mp',
|
||||
},
|
||||
});
|
||||
this.webToken = tokenInfo.accessToken;
|
||||
|
||||
async checkHasUser() {}
|
||||
this.accessToken = tokenInfo.accessToken;
|
||||
this.refreshToken = tokenInfo.refreshToken;
|
||||
this.user = user;
|
||||
const newToken = {
|
||||
accessToken: this.accessToken,
|
||||
refreshToken: this.refreshToken,
|
||||
isNew: this.isNew,
|
||||
};
|
||||
await redis.set(key, JSON.stringify(newToken), 'EX', 300); // 5分钟过期
|
||||
return newToken;
|
||||
}
|
||||
|
||||
|
||||
async checkHasUser() { }
|
||||
async getUserInfo() {
|
||||
try {
|
||||
if (!this.wxToken) {
|
||||
throw new CustomError(400, 'wxToken is not set');
|
||||
}
|
||||
const userInfo = await getUserInfo(this.wxToken.access_token, this.wxToken.openid);
|
||||
const openid = this.wxToken.openid;
|
||||
const access_token = this.wxToken.access_token;
|
||||
const userInfo = await getUserInfo(access_token, openid);
|
||||
// @ts-ignore
|
||||
if (userInfo?.errcode) {
|
||||
console.error('Failed to get user info: ', userInfo);
|
||||
throw new Error(`Failed to get user info: ${openid}`,);
|
||||
}
|
||||
const { nickname, headimgurl } = userInfo;
|
||||
this.user.nickname = nickname;
|
||||
console.log('User info retrieved', userInfo);
|
||||
try {
|
||||
const downloadImgUrl = await this.downloadImg(headimgurl);
|
||||
this.user.avatar = downloadImgUrl;
|
||||
@@ -160,6 +243,23 @@ export class WxServices {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getQrCodeTicket(): Promise<QrCodeRespone | null> {
|
||||
const res = await createQrcodeTicket();
|
||||
const ticket = res?.ticket;
|
||||
|
||||
if (ticket) {
|
||||
const imageBlob = await getShowQrCode(ticket);
|
||||
const buffer = await imageBlob.arrayBuffer();
|
||||
return {
|
||||
ticket: res.ticket,
|
||||
expire_seconds: res.expire_seconds,
|
||||
originUrl: res.url,
|
||||
url: `data:image/jpeg;base64,${Buffer.from(buffer).toString('base64')}`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// https://thirdwx.qlogo.cn/mmopen/vi_32/WvOPpbDwUKEVJvSst8Z91Y68m7CsBeecMqRGlqey5HejByePD89boYGaVCM8vESsYmokk1jABUDsK08IrfI6JEkibZkDIC2zsb96DGBTEF7E/132
|
||||
@@ -185,3 +285,73 @@ export const downloadImag = async (url: string) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// https://juejin.cn/post/7235118809605144631
|
||||
type QrCodeOpts = {
|
||||
/**
|
||||
* 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)。
|
||||
*/
|
||||
expired_seconds: number;
|
||||
/**
|
||||
* QR_SCENE为临时,QR_STR_SCENE为临时的字符串参数值,QR_LIMIT_SCENE为永久,QR_LIMIT_STR_SCENE为永久的字符串参数值
|
||||
*/
|
||||
scene_str: 'QR_SCENE' | 'QR_STR_SCENE' | 'QR_LIMIT_SCENE' | 'QR_LIMIT_STR_SCENE';
|
||||
action_info: {
|
||||
scene: {
|
||||
/**
|
||||
* 1-100000
|
||||
*/
|
||||
scene_id?: number;
|
||||
/**
|
||||
* 1-64字符
|
||||
*/
|
||||
scene_str?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type QrCodeRespone = {
|
||||
ticket: string;
|
||||
expire_seconds: number;
|
||||
url: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 第二步:创建二维码, 浏览器去获取图片
|
||||
* @param ticket
|
||||
* @returns
|
||||
*/
|
||||
export const getShowQrCode = async (ticket: string) => {
|
||||
const url = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${encodeURIComponent(ticket)}`;
|
||||
return fetch(url).then((res) => res.blob());
|
||||
}
|
||||
/**
|
||||
* 第一步:创建二维码,服务端去获取ticket
|
||||
* @param QrCodeOpts
|
||||
* @returns
|
||||
*/
|
||||
export const createQrcodeTicket = async (qrCodeOpts?: QrCodeOpts): Promise<QrCodeRespone | null> => {
|
||||
const accessToken = await getCacheAccessToken();
|
||||
const url = `https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${accessToken}`;
|
||||
const data = {
|
||||
expire_seconds: 300, // 5分钟
|
||||
action_name: "QR_STR_SCENE",
|
||||
...qrCodeOpts,
|
||||
action_info: {
|
||||
scene: {
|
||||
scene_str: "login",
|
||||
...qrCodeOpts?.action_info?.scene
|
||||
}
|
||||
}
|
||||
};
|
||||
const res = await post(url, data);
|
||||
if (res.errcode) {
|
||||
console.error('Failed to create QR code:', res);
|
||||
return null;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export const fetchToken = async (code: string, type: 'open' | 'mp' = 'open'): Pr
|
||||
appSecret = wx.appSecret;
|
||||
}
|
||||
if (!appId || !appSecret) {
|
||||
throw new CustomError(500, 'appId or appSecret is not set');
|
||||
throw new CustomError(500, 'appId or appSecret is not set');
|
||||
}
|
||||
console.log('fetchToken===', appId, appSecret, code);
|
||||
const wxUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`;
|
||||
@@ -65,7 +65,8 @@ type UserInfo = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* 获取用户信息, 通过授权登录
|
||||
* 例子:微信内部网页,开放平台授权网页,确定登录的时候
|
||||
* @param token
|
||||
* @param openid
|
||||
* @returns
|
||||
@@ -80,10 +81,34 @@ export const getUserInfo = async (token: string, openid: string): Promise<UserIn
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 公众号获取用户信息,
|
||||
* 微信公众号扫码,
|
||||
* 订阅登录,非授权,
|
||||
* 而是根据你关注了用户才登录
|
||||
* @param token
|
||||
* @param openid
|
||||
* @returns
|
||||
*/
|
||||
export const getUserInfoByMp = async (token: string, openid: string) => {
|
||||
// const phoneUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${token}&openid=${openid}`;
|
||||
const phoneUrl = `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${openid}&lang=zh_CN`;
|
||||
|
||||
const res = await fetch(phoneUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
// getUserInfo(token.access_token, token.openid)
|
||||
|
||||
type AuthRes = {
|
||||
@@ -123,3 +148,12 @@ export const refreshToken = async (refreshToken: string): Promise<RefreshToken>
|
||||
};
|
||||
|
||||
// refreshToken(token.refresh_token)
|
||||
|
||||
|
||||
export const post = async (url: string, data: any) => {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return await res.json();
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { WxServices } from "./modules/wx-services.ts";
|
||||
import { app, redis } from "@/app.ts";
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'wx',
|
||||
key: 'checkLogin',
|
||||
description: '微信网页登录后获取登录结果(遗弃)',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const state = ctx.query.state;
|
||||
@@ -28,6 +30,7 @@ app
|
||||
.route({
|
||||
path: 'wx',
|
||||
key: 'mplogin',
|
||||
description: '微信网页登录后提交code去登录(遗弃)',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const state = ctx.query.state;
|
||||
@@ -50,7 +53,7 @@ app
|
||||
.route({
|
||||
path: 'wx',
|
||||
key: 'mp-get-openid',
|
||||
isDebug: true,
|
||||
description: '微信公众平台获取openid',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const code = ctx.query.code;
|
||||
@@ -68,7 +71,7 @@ app
|
||||
.route({
|
||||
path: 'wx',
|
||||
key: 'open-login',
|
||||
isDebug: true,
|
||||
description: '微信开放平台登录',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const code = ctx.query.code;
|
||||
@@ -89,3 +92,71 @@ app
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'wx',
|
||||
key: 'get-qrcode-ticket',
|
||||
description: '获取微信二维码ticket',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const wx = new WxServices();
|
||||
const res = await wx.getQrCodeTicket();
|
||||
if (!res) {
|
||||
ctx.throw(500, 'Get qrcode ticket failed');
|
||||
return;
|
||||
}
|
||||
const key = `wx:mp:login:qrcode:${res.ticket}`;
|
||||
await redis.set(key, '-', 'EX', 360); // 6分钟过期
|
||||
ctx.body = res;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'wx',
|
||||
key: 'check-qrcode-login',
|
||||
description: '检查微信二维码登录状态',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const ticket = ctx.query.ticket;
|
||||
if (!ticket) {
|
||||
ctx.throw(400, 'ticket is required');
|
||||
return;
|
||||
}
|
||||
const token = await redis.get(`wx:mp:login:qrcode:${ticket}`);
|
||||
if (!token) {
|
||||
ctx.throw(400, 'Invalid ticket');
|
||||
return;
|
||||
}
|
||||
if (token === '-') {
|
||||
ctx.throw(400, 'Not scanned yet');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// remove the token after getting it
|
||||
await redis.del(`wx:mp:login:qrcode:${ticket}`);
|
||||
ctx.body = JSON.parse(token);
|
||||
} catch (error) {
|
||||
ctx.throw(500, 'Invalid token get');
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
|
||||
app.route({
|
||||
path: 'wx',
|
||||
key: 'login-by-ticket',
|
||||
description: '通过ticket登录微信(扫码登录回调)',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const { openid, ticket } = ctx.query || {};
|
||||
if (!openid || !ticket) {
|
||||
console.error('openid and ticket are required');
|
||||
ctx.throw(400, 'openid and ticket are required');
|
||||
return;
|
||||
}
|
||||
const wx = new WxServices();
|
||||
const result = await wx.loginByTicket({ openid, ticket });
|
||||
ctx.body = result;
|
||||
})
|
||||
.addTo(app);
|
||||
@@ -4,8 +4,9 @@ import xml2js from 'xml2js';
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { Redis } from 'ioredis';
|
||||
import http from 'node:http';
|
||||
import { Wx, parseWxMessage } from './wx/index.ts';
|
||||
import { Wx, WxMsgEvent, parseWxMessage } from './wx/index.ts';
|
||||
import { config } from './modules/config.ts';
|
||||
import { loginByTicket } from './wx/login-by-ticket.ts';
|
||||
export const simpleRouter: SimpleRouter = await useContextKey('router');
|
||||
export const redis: Redis = await useContextKey('redis');
|
||||
|
||||
@@ -74,7 +75,13 @@ simpleRouter.post('/api/wxmsg', async (req: http.IncomingMessage, res: http.Serv
|
||||
const { fromusername, msgtype } = msg;
|
||||
res.end('')
|
||||
if (msgtype === 'event') {
|
||||
console.log('Received event message');
|
||||
const wxMsgEvent = msg as WxMsgEvent;
|
||||
if (wxMsgEvent.eventkey?.includes?.('login')) {
|
||||
await loginByTicket({
|
||||
ticket: wxMsgEvent.ticket!,
|
||||
openid: wxMsgEvent.fromusername,
|
||||
});
|
||||
}
|
||||
return
|
||||
}
|
||||
if (fromusername) {
|
||||
|
||||
36
wxmsg/src/wx/get-access-token.ts
Normal file
36
wxmsg/src/wx/get-access-token.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// const accessURL = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET'
|
||||
const accessURL = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential';
|
||||
|
||||
type AccessData = {
|
||||
"access_token": string;
|
||||
"expires_in": number; // 7200, 单位秒 2小时
|
||||
}
|
||||
|
||||
type ErrorData = {
|
||||
errcode: number;
|
||||
errmsg: string;
|
||||
}
|
||||
export const getAccessURL = (appId: string, appSecret: string): string => {
|
||||
return `${accessURL}&appid=${appId}&secret=${appSecret}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (appId: string, appSecret: string): Promise<{
|
||||
code: number;
|
||||
data?: AccessData;
|
||||
message?: string;
|
||||
}> => {
|
||||
const url = getAccessURL(appId, appSecret);
|
||||
const response = await fetch(url);
|
||||
const data = await response.json() as AccessData | ErrorData;
|
||||
console.log('Access token response:', data);
|
||||
if ((data as ErrorData).errcode) {
|
||||
return {
|
||||
code: 500,
|
||||
message: (data as ErrorData).errmsg,
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: data as AccessData,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAccessToken } from './access-token';
|
||||
import { getAccessToken } from './get-access-token.ts';
|
||||
import { Redis } from 'ioredis';
|
||||
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
|
||||
|
||||
@@ -34,9 +34,9 @@ export class Wx {
|
||||
if (res.code !== 200 || !res.data) {
|
||||
throw new Error(`Failed to get access token: ${res.message || 'unknown error'}`);
|
||||
}
|
||||
const { accessToken, expires_in } = res.data;
|
||||
await this.redis?.set(`wx:access_token:${this.appId}`, accessToken, 'EX', expires_in - 200);
|
||||
return accessToken;
|
||||
const { access_token, expires_in } = res.data;
|
||||
await this.redis?.set(`wx:access_token:${this.appId}`, access_token, 'EX', expires_in - 200);
|
||||
return access_token;
|
||||
}
|
||||
public async analyzeUserMsg(msg: WxCustomServiceMsg) {
|
||||
const touser = msg.fromusername;
|
||||
@@ -59,6 +59,11 @@ export class Wx {
|
||||
}
|
||||
this.sendUserMessage(sendData);
|
||||
}
|
||||
/**
|
||||
* 发送客服消息
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public async sendUserMessage(data: any) {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`
|
||||
@@ -69,10 +74,15 @@ export class Wx {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
public async post(url: string, data: any) {
|
||||
|
||||
|
||||
async post(url: string, data: any) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
async get(url: string) {
|
||||
return fetch(url).then((res) => res.json());
|
||||
}
|
||||
}
|
||||
15
wxmsg/src/wx/login-by-ticket.ts
Normal file
15
wxmsg/src/wx/login-by-ticket.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { App } from '@kevisual/router';
|
||||
|
||||
export const loginByTicket = async (msgInfo: { openid: string, ticket: string }) => {
|
||||
const app: App = useContextKey('app');
|
||||
const res = await app.call({
|
||||
path: 'wx',
|
||||
key: 'login-by-ticket',
|
||||
payload: {
|
||||
ticket: msgInfo.ticket,
|
||||
openid: msgInfo.openid,
|
||||
},
|
||||
})
|
||||
return res;
|
||||
}
|
||||
22
wxmsg/src/wx/test/get-user-info.ts
Normal file
22
wxmsg/src/wx/test/get-user-info.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
|
||||
/**
|
||||
* 公众号获取用户信息
|
||||
* @param token
|
||||
* @param openid
|
||||
* @returns
|
||||
*/
|
||||
export const getUserInfoByMp = async (token: string, openid: string) => {
|
||||
// const phoneUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${token}&openid=${openid}`;
|
||||
const phoneUrl = `https://api.weixin.qq.com/cgi-bin/user/info?access_token=${token}&openid=${openid}&lang=zh_CN`;
|
||||
|
||||
const res = await fetch(phoneUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await res.json();
|
||||
console.log('userinfo', data);
|
||||
return data;
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export type WxMsgText = WxMsgBase<{
|
||||
export type WxMsgEvent = WxMsgBase<{
|
||||
msgtype: 'event';
|
||||
event: 'subscribe' | 'unsubscribe' | 'click' | 'location' | 'scan';
|
||||
eventkey: string;
|
||||
eventkey: string; // example: subscribe--> qrscene_login scan--> login
|
||||
/**
|
||||
*
|
||||
* 用户同意上报地理位置后,每次进入服务号会话时,都会在进入时上报地理位置,
|
||||
|
||||
Reference in New Issue
Block a user