3965e542fc
部署到群晖 / deploy (push) Failing after 6m22s
- 新增 server/:Node + Express + SQLite + node-cron 实现登录、纪念日 CRUD 和定时订阅消息推送 - 新增 .gitea/workflows/deploy.yml:推送即触发群晖 Docker 部署,监听 15002 - utils/api.js:自动按 envVersion 切换本地/线上 BASE_URL - app.js 与 add-anniversary.js 移除 wx.cloud 调用,改走自建后端 - cloudfunctions/ 暂保留以便回滚 - 一并提交此前未入库的首页 / 设置页 / 日历 / 万年历等改造 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
1.9 KiB
JavaScript
58 lines
1.9 KiB
JavaScript
const axios = require('axios')
|
|
|
|
const APPID = process.env.WX_APPID
|
|
const APPSECRET = process.env.WX_APPSECRET
|
|
|
|
// access_token 内存缓存(微信全局唯一,2 小时有效)
|
|
let _token = { value: null, expireAt: 0 }
|
|
|
|
async function getAccessToken() {
|
|
const now = Date.now()
|
|
// 提前 5 分钟刷新,避免临界过期
|
|
if (_token.value && _token.expireAt - now > 5 * 60 * 1000) {
|
|
return _token.value
|
|
}
|
|
|
|
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
|
|
const { data } = await axios.get(url, { timeout: 8000 })
|
|
if (!data.access_token) {
|
|
throw new Error(`获取 access_token 失败: ${JSON.stringify(data)}`)
|
|
}
|
|
_token = {
|
|
value: data.access_token,
|
|
expireAt: now + (data.expires_in - 300) * 1000
|
|
}
|
|
return _token.value
|
|
}
|
|
|
|
// code2session:拿 openid(小程序登录)
|
|
async function code2session(code) {
|
|
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${APPSECRET}&js_code=${code}&grant_type=authorization_code`
|
|
const { data } = await axios.get(url, { timeout: 8000 })
|
|
if (data.errcode) {
|
|
throw new Error(`code2session 失败: ${JSON.stringify(data)}`)
|
|
}
|
|
return { openid: data.openid, unionid: data.unionid || null, session_key: data.session_key }
|
|
}
|
|
|
|
// 发送订阅消息
|
|
async function sendSubscribeMessage({ touser, page, data, templateId, miniprogramState = 'formal' }) {
|
|
const token = await getAccessToken()
|
|
const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${token}`
|
|
const body = {
|
|
touser,
|
|
template_id: templateId,
|
|
page,
|
|
miniprogram_state: miniprogramState,
|
|
lang: 'zh_CN',
|
|
data
|
|
}
|
|
const { data: res } = await axios.post(url, body, { timeout: 8000 })
|
|
if (res.errcode !== 0) {
|
|
throw new Error(`发送订阅消息失败: ${JSON.stringify(res)}`)
|
|
}
|
|
return res
|
|
}
|
|
|
|
module.exports = { getAccessToken, code2session, sendSubscribeMessage }
|