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 = {
|
type AccessData = {
|
||||||
"access_token": string;
|
"access_token": string;
|
||||||
"expires_in": number; // 7200, 单位秒 2小时
|
"expires_in": number; // 7200, 单位秒 2小时
|
||||||
"accessToken": string;
|
|
||||||
"expiredAt": number; // 到期时间戳,单位毫秒
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorData = {
|
type ErrorData = {
|
||||||
@@ -23,26 +21,16 @@ export const getAccessToken = async (appId: string, appSecret: string): Promise<
|
|||||||
}> => {
|
}> => {
|
||||||
const url = getAccessURL(appId, appSecret);
|
const url = getAccessURL(appId, appSecret);
|
||||||
const response = await fetch(url);
|
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) {
|
if ((data as ErrorData).errcode) {
|
||||||
return {
|
return {
|
||||||
code: 500,
|
code: 500,
|
||||||
message: (data as ErrorData).errmsg,
|
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 {
|
return {
|
||||||
code: 200,
|
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 { useContextKey } from '@kevisual/use-config/context';
|
||||||
import { UserModel } from '@kevisual/code-center-module';
|
import { UserModel } from '@kevisual/code-center-module';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
import { getCacheAccessToken } from './get-cache-access-token.ts';
|
||||||
|
import { redis } from '@/app.ts';
|
||||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
|
||||||
const User = useContextKey<typeof UserModel>('UserModel');
|
const User = useContextKey<typeof UserModel>('UserModel');
|
||||||
export class WxServices {
|
export class WxServices {
|
||||||
@@ -89,6 +91,7 @@ export class WxServices {
|
|||||||
let data = {
|
let data = {
|
||||||
...user.data,
|
...user.data,
|
||||||
wxUnionId: unionid,
|
wxUnionId: unionid,
|
||||||
|
canChangeUsername: true,
|
||||||
};
|
};
|
||||||
user.data = data;
|
user.data = data;
|
||||||
if ((type = 'mp')) {
|
if ((type = 'mp')) {
|
||||||
@@ -100,7 +103,7 @@ export class WxServices {
|
|||||||
}
|
}
|
||||||
this.user = await user.save({ fields: ['data'] });
|
this.user = await user.save({ fields: ['data'] });
|
||||||
|
|
||||||
this.getUserInfo();
|
await this.getUserInfo();
|
||||||
this.isNew = true;
|
this.isNew = true;
|
||||||
}
|
}
|
||||||
this.user = user;
|
this.user = user;
|
||||||
@@ -122,16 +125,96 @@ export class WxServices {
|
|||||||
isNew: this.isNew,
|
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() {
|
async getUserInfo() {
|
||||||
try {
|
try {
|
||||||
if (!this.wxToken) {
|
if (!this.wxToken) {
|
||||||
throw new CustomError(400, 'wxToken is not set');
|
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;
|
const { nickname, headimgurl } = userInfo;
|
||||||
this.user.nickname = nickname;
|
this.user.nickname = nickname;
|
||||||
|
console.log('User info retrieved', userInfo);
|
||||||
try {
|
try {
|
||||||
const downloadImgUrl = await this.downloadImg(headimgurl);
|
const downloadImgUrl = await this.downloadImg(headimgurl);
|
||||||
this.user.avatar = downloadImgUrl;
|
this.user.avatar = downloadImgUrl;
|
||||||
@@ -160,6 +243,23 @@ export class WxServices {
|
|||||||
return '';
|
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
|
// https://thirdwx.qlogo.cn/mmopen/vi_32/WvOPpbDwUKEVJvSst8Z91Y68m7CsBeecMqRGlqey5HejByePD89boYGaVCM8vESsYmokk1jABUDsK08IrfI6JEkibZkDIC2zsb96DGBTEF7E/132
|
||||||
@@ -185,3 +285,73 @@ export const downloadImag = async (url: string) => {
|
|||||||
throw error;
|
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;
|
appSecret = wx.appSecret;
|
||||||
}
|
}
|
||||||
if (!appId || !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);
|
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`;
|
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 token
|
||||||
* @param openid
|
* @param openid
|
||||||
* @returns
|
* @returns
|
||||||
@@ -80,10 +81,34 @@ export const getUserInfo = async (token: string, openid: string): Promise<UserIn
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log(data);
|
|
||||||
return 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)
|
// getUserInfo(token.access_token, token.openid)
|
||||||
|
|
||||||
type AuthRes = {
|
type AuthRes = {
|
||||||
@@ -123,3 +148,12 @@ export const refreshToken = async (refreshToken: string): Promise<RefreshToken>
|
|||||||
};
|
};
|
||||||
|
|
||||||
// refreshToken(token.refresh_token)
|
// 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 { WxServices } from "./modules/wx-services.ts";
|
||||||
import { app, redis } from "@/app.ts";
|
import { app, redis } from "@/app.ts";
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'wx',
|
path: 'wx',
|
||||||
key: 'checkLogin',
|
key: 'checkLogin',
|
||||||
|
description: '微信网页登录后获取登录结果(遗弃)',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const state = ctx.query.state;
|
const state = ctx.query.state;
|
||||||
@@ -28,6 +30,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'wx',
|
path: 'wx',
|
||||||
key: 'mplogin',
|
key: 'mplogin',
|
||||||
|
description: '微信网页登录后提交code去登录(遗弃)',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const state = ctx.query.state;
|
const state = ctx.query.state;
|
||||||
@@ -50,7 +53,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'wx',
|
path: 'wx',
|
||||||
key: 'mp-get-openid',
|
key: 'mp-get-openid',
|
||||||
isDebug: true,
|
description: '微信公众平台获取openid',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const code = ctx.query.code;
|
const code = ctx.query.code;
|
||||||
@@ -68,7 +71,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'wx',
|
path: 'wx',
|
||||||
key: 'open-login',
|
key: 'open-login',
|
||||||
isDebug: true,
|
description: '微信开放平台登录',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const code = ctx.query.code;
|
const code = ctx.query.code;
|
||||||
@@ -89,3 +92,71 @@ app
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addTo(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 { useContextKey } from '@kevisual/context';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import http from 'node:http';
|
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 { config } from './modules/config.ts';
|
||||||
|
import { loginByTicket } from './wx/login-by-ticket.ts';
|
||||||
export const simpleRouter: SimpleRouter = await useContextKey('router');
|
export const simpleRouter: SimpleRouter = await useContextKey('router');
|
||||||
export const redis: Redis = await useContextKey('redis');
|
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;
|
const { fromusername, msgtype } = msg;
|
||||||
res.end('')
|
res.end('')
|
||||||
if (msgtype === 'event') {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (fromusername) {
|
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 { Redis } from 'ioredis';
|
||||||
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
|
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ export class Wx {
|
|||||||
if (res.code !== 200 || !res.data) {
|
if (res.code !== 200 || !res.data) {
|
||||||
throw new Error(`Failed to get access token: ${res.message || 'unknown error'}`);
|
throw new Error(`Failed to get access token: ${res.message || 'unknown error'}`);
|
||||||
}
|
}
|
||||||
const { accessToken, expires_in } = res.data;
|
const { access_token, expires_in } = res.data;
|
||||||
await this.redis?.set(`wx:access_token:${this.appId}`, accessToken, 'EX', expires_in - 200);
|
await this.redis?.set(`wx:access_token:${this.appId}`, access_token, 'EX', expires_in - 200);
|
||||||
return accessToken;
|
return access_token;
|
||||||
}
|
}
|
||||||
public async analyzeUserMsg(msg: WxCustomServiceMsg) {
|
public async analyzeUserMsg(msg: WxCustomServiceMsg) {
|
||||||
const touser = msg.fromusername;
|
const touser = msg.fromusername;
|
||||||
@@ -59,6 +59,11 @@ export class Wx {
|
|||||||
}
|
}
|
||||||
this.sendUserMessage(sendData);
|
this.sendUserMessage(sendData);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 发送客服消息
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
public async sendUserMessage(data: any) {
|
public async sendUserMessage(data: any) {
|
||||||
const accessToken = await this.getAccessToken();
|
const accessToken = await this.getAccessToken();
|
||||||
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`
|
const url = `https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${accessToken}`
|
||||||
@@ -69,10 +74,15 @@ export class Wx {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
public async post(url: string, data: any) {
|
|
||||||
|
|
||||||
|
async post(url: string, data: any) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
}).then((res) => res.json());
|
}).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<{
|
export type WxMsgEvent = WxMsgBase<{
|
||||||
msgtype: 'event';
|
msgtype: 'event';
|
||||||
event: 'subscribe' | 'unsubscribe' | 'click' | 'location' | 'scan';
|
event: 'subscribe' | 'unsubscribe' | 'click' | 'location' | 'scan';
|
||||||
eventkey: string;
|
eventkey: string; // example: subscribe--> qrscene_login scan--> login
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* 用户同意上报地理位置后,每次进入服务号会话时,都会在进入时上报地理位置,
|
* 用户同意上报地理位置后,每次进入服务号会话时,都会在进入时上报地理位置,
|
||||||
|
|||||||
Reference in New Issue
Block a user