用Node.js构建智能电影助手:从豆瓣API到微信机器人实战
上周五晚上,朋友突然发微信问我:"最近有什么值得看的科幻片?"我正想打开豆瓣App查一查,突然意识到——为什么不把这个过程自动化?三个小时后,我的微信里多了一个能随时回答这类问题的机器人助手。本文将分享如何从零开始,用Node.js打造这样一个智能电影查询系统。
1. 项目架构设计与技术选型
整个系统可以分为三个核心模块:数据获取层、业务逻辑层和交互呈现层。我们先从技术选型开始:
graph TD A[微信客户端] -->|发送消息| B[Wechaty机器人] B -->|解析指令| C[业务逻辑处理器] C -->|调用| D[豆瓣API封装模块] D -->|返回数据| C C -->|生成回复| B B -->|返回消息| A关键技术栈选择:
- 机器人框架:Wechaty(支持微信个人号API)
- HTTP客户端:Axios(处理豆瓣API请求)
- 依赖管理:npm/yarn
- 运行环境:Node.js 14+
提示:豆瓣API有调用频率限制(40次/分钟),生产环境建议添加缓存层
安装基础依赖:
npm init -y npm install wechaty axios memory-cache2. 豆瓣API服务封装
我们需要先创建一个可靠的豆瓣电影服务模块。新建doubanService.js:
const axios = require('axios'); const cache = require('memory-cache'); const API_KEY = '0b2bdeda43b5688921839c8ecb20399b'; const BASE_URL = 'https://api.douban.com/v2/movie'; // 带缓存的请求封装 async function cachedRequest(url, ttl = 60 * 1000) { const cached = cache.get(url); if (cached) return cached; const response = await axios.get(url); cache.put(url, response.data, ttl); return response.data; } module.exports = { // 获取热映电影 getInTheaters: async (city = '北京', start = 0, count = 5) => { const url = `${BASE_URL}/in_theaters?apikey=${API_KEY}&city=${encodeURIComponent(city)}&start=${start}&count=${count}`; return cachedRequest(url); }, // 搜索电影 searchMovie: async (query, start = 0, count = 3) => { const url = `${BASE_URL}/search?apikey=${API_KEY}&q=${encodeURIComponent(query)}&start=${start}&count=${count}`; return cachedRequest(url); }, // 获取电影详情 getMovieDetail: async (id) => { const url = `${BASE_URL}/subject/${id}?apikey=${API_KEY}`; return cachedRequest(url); } };API返回数据结构示例:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| title | string | 电影中文名 | "流浪地球" |
| original_title | string | 电影原名 | "The Wandering Earth" |
| rating.average | number | 豆瓣评分 | 7.9 |
| genres | array | 电影类型 | ["科幻", "灾难"] |
| year | string | 上映年份 | "2019" |
3. 微信机器人消息处理
创建主入口文件index.js,实现消息响应逻辑:
const { Wechaty } = require('wechaty'); const douban = require('./doubanService'); // 消息处理器 async function onMessage(msg) { if (msg.self()) return; // 忽略自己发的消息 const text = msg.text(); const room = msg.room(); // 私聊或@机器人的消息才处理 if (!room || (room && text.includes('@电影助手'))) { try { let reply = await processMovieQuery(text); await msg.say(reply); } catch (err) { console.error(err); await msg.say('查询失败,请稍后再试~'); } } } // 解析电影查询指令 async function processMovieQuery(text) { // 匹配"最近有什么好电影?"类问题 if (/最近|有什么|推荐|好电影/.test(text)) { const data = await douban.getInTheaters(); return formatTheaters(data); } // 匹配"《XXX》评分"类问题 const match = text.match(/《(.+?)》|"(.+?)"/); if (match) { const name = match[1] || match[2]; const data = await douban.searchMovie(name); if (data.subjects.length) { const detail = await douban.getMovieDetail(data.subjects[0].id); return formatMovieDetail(detail); } return `没找到《${name}》的相关信息呢~`; } return `试试这样问我: • "最近有什么好电影?" • "《流浪地球》评分多少?"`; } // 格式化热映电影回复 function formatTheaters(data) { let reply = `🎬 ${data.title}(共${data.total}部)\n\n`; data.subjects.forEach(movie => { reply += `▫️ ${movie.title}(${movie.rating.average}分) 类型:${movie.genres.join('/')} 主演:${movie.casts.slice(0,3).map(c => c.name).join('、')}\n\n`; }); return reply; } // 格式化电影详情回复 function formatMovieDetail(movie) { return `🎥 ${movie.title}(${movie.original_title}) ⭐ 评分:${movie.rating.average}/10(${movie.ratings_count}人评价) 📅 年份:${movie.year} | 国家:${movie.countries.join('/')} 🎭 类型:${movie.genres.join('、')} 👨💼 导演:${movie.directors.map(d => d.name).join('、')} 👥 主演:${movie.casts.slice(0,5).map(c => c.name).join('、')} 📖 简介:${movie.summary.substring(0, 100)}...`; } // 启动机器人 const bot = new Wechaty(); bot.on('message', onMessage); bot.start() .then(() => console.log('机器人启动成功')) .catch(e => console.error('启动失败', e));4. 高级功能扩展
基础功能完成后,我们可以考虑添加以下增强特性:
4.1 分页查询优化
当用户查询结果较多时,实现交互式分页:
// 在processMovieQuery中添加 if (text === '下一页' && lastQuery) { const data = await douban.getInTheaters(lastQuery.city, lastQuery.start + 5); lastQuery.start += 5; return formatTheaters(data); }4.2 个性化推荐
基于用户历史查询记录推荐相似电影:
const userPrefs = new Map(); function updateUserPref(userId, genre) { if (!userPrefs.has(userId)) { userPrefs.set(userId, new Map()); } const prefs = userPrefs.get(userId); genre.forEach(g => prefs.set(g, (prefs.get(g) || 0) + 1)); } // 在返回推荐时优先推荐用户偏好的类型4.3 自动订阅提醒
用户可以订阅特定电影的上映提醒:
const subscriptions = new Map(); async function checkComingMovies() { const coming = await douban.getComingSoon(); coming.subjects.forEach(movie => { if (subscriptions.has(movie.id)) { subscriptions.get(movie.id).forEach(user => { bot.say(user, `您关注的《${movie.title}》已定档${movie.mainland_pubdate}!`); }); } }); } // 每6小时检查一次 setInterval(checkComingMovies, 6 * 60 * 60 * 1000);5. 部署与运维建议
将机器人部署到服务器时,需要注意:
性能优化配置:
| 项目 | 推荐值 | 说明 |
|---|---|---|
| 缓存时间 | 5-10分钟 | 平衡实时性与API限制 |
| 重试机制 | 3次 | 应对网络波动 |
| 超时设置 | 3000ms | 避免长时间等待 |
| 日志记录 | 完整记录 | 便于问题排查 |
错误处理增强:
// 在cachedRequest中添加重试逻辑 let retries = 3; while (retries--) { try { const response = await axios.get(url, { timeout: 3000 }); cache.put(url, response.data, ttl); return response.data; } catch (err) { if (retries === 0) throw err; await new Promise(resolve => setTimeout(resolve, 1000)); } }实际部署中,我发现微信机器人有时会意外断开连接。最好的解决方案是使用pm2等进程管理工具,配合自动重启脚本:
pm2 start index.js --name "movie-bot" --restart-delay=3000这个项目最有趣的部分是看到朋友们开始主动与机器人互动,甚至发展出了一些我没预料到的使用方式——有人用它来快速查询电影原声带信息,有人则用来检查某位演员的最新作品。技术真正的魅力,或许就在于它能以意想不到的方式连接人与人之间的兴趣和需求。