This commit is contained in:
2025-12-04 14:22:04 +08:00
parent 2a55f2d3ef
commit 9e458f4a77
17 changed files with 449 additions and 143 deletions

View File

@@ -27,6 +27,7 @@
"type": "module",
"dependencies": {
"@kevisual/context": "^0.0.4",
"@kevisual/query": "^0.0.29",
"@kevisual/router": "0.0.33",
"@types/node": "^24.10.1",
"crypto-js": "^4.2.0",

3
wxmsg/readme.md Normal file
View File

@@ -0,0 +1,3 @@
# 根据 wx 的内容进行改动
如果更新了, 需要重新打包

View File

@@ -7,9 +7,14 @@ import http from 'node:http';
import { Wx, WxMsgEvent, parseWxMessage } from './wx/index.ts';
import { contextConfig as config } from './modules/config.ts';
import { loginByTicket } from './wx/login-by-ticket.ts';
import { Queue } from 'bullmq';
export const simpleRouter: SimpleRouter = await useContextKey('router');
export const redis: Redis = await useContextKey('redis');
export const wxmsgQueue = useContextKey<Queue>('wxmsgQueue', () => {
return new Queue('wxmsg', {
connection: redis
});
});
simpleRouter.get('/api/wxmsg', async (req: http.IncomingMessage, res: http.ServerResponse) => {
console.log('微信检测服务是否可用');
const query = simpleRouter.getSearch(req);

View File

@@ -1,9 +0,0 @@
import { Queue } from 'bullmq';
export const wxmsgQueue = new Queue('wxmsg', {
connection: {
host: process.env.REDIS_HOST || 'kevisual.cn',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
}
});

View File

@@ -0,0 +1,38 @@
import { Redis } from 'ioredis';
import { useConfig } from '@kevisual/use-config';
const config = useConfig();
const redis = new Redis({
password: config.REDIS_PASSWORD
});
/**
* 公众号获取用户信息
* @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;
};
// 大号的程序的服务号的openid
const openid = 'omcvy7AHC6bAA0QM4x9_bE0fGD1g'
const main = async () => {
const appId = config.WX_MP_APP_ID!;
const _accessToken = await redis.get(`wx:access_token:${appId}`);
const data = await getUserInfoByMp(_accessToken!, openid);
console.log('用户信息:', data);
}
main();

View File

@@ -3,10 +3,12 @@ import { Redis } from 'ioredis';
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
import { Queue } from 'bullmq';
import { useContextKey } from "@kevisual/context";
import { Query } from '@kevisual/query';
import { getUserInfoByMp } from './test/get-user-info.ts';
export * from './type/custom-service.ts';
export * from './type/send.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
/**
* 从
* @param str
@@ -21,10 +23,14 @@ export class Wx {
private appId: string;
private appSecret: string;
public redis: Redis | null = null;
constructor({ appId, appSecret, redis }: { appId: string; appSecret: string; redis?: Redis }) {
query: Query = new Query();
constructor({ appId, appSecret, redis, url }: { appId: string; appSecret: string; redis?: Redis; url?: string }) {
this.appId = appId;
this.appSecret = appSecret;
this.redis = redis! || null;
this.query = new Query({
url: url || 'http://localhost:3005'
});
}
public async getAccessToken(): Promise<string> {
@@ -83,4 +89,93 @@ export class Wx {
async get(url: string) {
return fetch(url).then((res) => res.json());
}
async getUnionid(openid: string) {
const cacheRedisKey = `wxmp:openid:unionid:${openid}`;
const cachedUnionid = await this.redis!.get(cacheRedisKey);
if (cachedUnionid) {
return cachedUnionid;
}
const accessToken = await this.getAccessToken();
const res = await getUserInfoByMp(accessToken, openid);
if (!res?.unionid) {
throw new Error('无法获取用户的unionid用户是否未关注公众号');
}
await this.redis!.set(cacheRedisKey, res.unionid, 'EX', 7 * 24 * 3600); // 缓存7天
return res.unionid;
}
async getCenterCodeToken(touser: string) {
const unionid = await this.getUnionid(touser);
// 第一次尝试获取token
let token = await this.redis!.get(getWxRedisKey(unionid));
if (!token) {
const msg = {
path: 'secret',
key: 'wxnotify',
payload: { unionid: unionid }
}
const res = await this.query.post(msg);
if (res.code !== 200) {
console.error('获取不到用户配置,是否用户未关注公众号?', res, 'openid', touser, "unionid", unionid);
throw new Error('获取不到用户配置,是否用户未关注公众号?');
}
await sleep(1000);
}
// 尝试更新后第二次获取token
token = await this.redis!.get(getWxRedisKey(unionid));
if (!token) {
console.error('这个uid获取不到用户配置', touser, "unionid", unionid);
throw new Error('获取不到用户配置,是否用户未关注公众号?');
}
return token;
}
async postCenter(data: any) {
await this.getAccessToken();
const { touser, msg } = data;
const msgType = msg.msgtype;
const sendUserText = async (text: string) => {
const sendData = {
touser,
msgtype: 'text',
text: {
content: text,
},
};
await this.sendUserMessage(sendData);
}
const userToken = await this.getCenterCodeToken(touser);
if (!userToken) {
await sendUserText('服务器错误:无法获取用户绑定的账号信息');
console.error('用户未绑定账号,无法自动回复', touser);
return;
}
if (msgType !== 'text') {
await sendUserText('暂不支持该类型消息的自动回复');
return;
}
const wxMsg = msg as WxMsgText;
const question = wxMsg.content;
const nocoMsg = {
path: 'noco-life',
key: "chat",
payload: {
token: userToken,
question,
}
}
const res = await this.query.post(nocoMsg);
if (res.code !== 200) {
await sendUserText('自动回复失败,请稍后再试:' + res.message);
}
const content = res.data?.content || ''
if (content) {
await sendUserText(content);
}
}
}
const getWxRedisKey = (unionid: string) => {
return `wxmp:unionid:token:${unionid}`
}

View File

@@ -1,12 +1,31 @@
type UserInfo = {
subscribe: number;
openid: string;
nickname: string;
sex: number;
language: string;
city: string;
province: string;
country: string;
headimgurl: string;
subscribe_time: number;
unionid: string;
remark: string;
groupid: number;
tagid_list: number[];
subscribe_scene: string;
qr_scene: number;
qr_scene_str: string;
};
/**
* 公众号获取用户信息
* @param token
* @param openid
* @returns
*/
export const getUserInfoByMp = async (token: string, openid: string) => {
export const getUserInfoByMp = async (token: string, openid: string): Promise<UserInfo> => {
// 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`;
@@ -17,6 +36,5 @@ export const getUserInfoByMp = async (token: string, openid: string) => {
},
});
const data = await res.json();
console.log('userinfo', data);
return data;
};

View File

@@ -1,30 +1,31 @@
import { Worker } from "bullmq";
import { redis } from './redis.ts';
import { redis, config } from './redis.ts';
import { Wx } from "../../src/wx";
const worker = new Worker('wxmsg', async job => {
const url = 'http://localhost:3005/api/router';
const wx = new Wx({
appId: process.env.WX_APPID || '',
appSecret: process.env.WX_APPSECRET || '',
redis: redis
appId: config.WX_MP_APP_ID, appSecret: config.WX_MP_APP_SECRET,
redis: redis,
url,
});
if (job.name === 'analyzeUserMsg') {
const { touser, msg } = job.data;
const accessToken = await wx.getAccessToken();
const sendData = {
touser,
msgtype: 'text',
text: {
content: 'Hello World' + new Date().toISOString(),
},
};
await wx.sendUserMessage(sendData);
await wx.postCenter(job.data);
} else {
throw new Error(`Unknown job name: ${job.name}`);
}
}, {
connection: redis
connection: redis,
removeOnComplete: {
age: 3600, // 1 hour
count: 5000, // keep last 5000 jobs
},
removeOnFail: {
age: 7200, // 2 hours
count: 5000, // keep last 5000 jobs
},
});
worker.on('completed', (job) => {

View File

@@ -3,7 +3,6 @@ import { useConfig } from '@kevisual/use-config';
import { useContextKey } from "@kevisual/context";
export const config = useConfig()
// 首先从 process.env 读取环境变量
const redisConfig = {
host: process.env.REDIS_HOST || 'kevisual.cn',
@@ -27,14 +26,14 @@ export const createRedisClient = (options = {}) => {
});
// 监听连接事件
redis.on('connect', () => {
console.log('Redis 连接成功');
// console.log('Redis 连接成功');
});
redis.on('error', (err) => {
console.error('Redis 连接错误', err);
// console.error('Redis 连接错误', err);
});
redis.on('ready', () => {
console.log('Redis 已准备好处理请求');
// console.log('Redis 已准备好处理请求');
});
return redis;
};