This commit is contained in:
2025-11-30 21:32:01 +08:00
parent b7f1095e4a
commit d4ff2862bd
11 changed files with 413 additions and 34 deletions

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View 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;
}

View 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;
};

View File

@@ -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
/**
*
* 用户同意上报地理位置后,每次进入服务号会话时,都会在进入时上报地理位置,