init: 小程序后台 — 到期提醒、定时任务、Docker部署配置

This commit is contained in:
Developer
2026-05-16 23:57:30 +08:00
parent f9a4d50b09
commit 0ff5a02155
18 changed files with 1487 additions and 102 deletions
+243
View File
@@ -0,0 +1,243 @@
const axios = require('axios');
const BAIDU_OCR_API = {
token: 'https://aip.baidubce.com/oauth/2.0/token',
generalBasic: 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic',
accurateBasic: 'https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic'
};
let accessTokenCache = {
token: null,
expireAt: 0
};
async function getAccessToken() {
const now = Date.now();
if (accessTokenCache.token && accessTokenCache.expireAt > now + 60000) {
return accessTokenCache.token;
}
const apiKey = process.env.BAIDU_OCR_API_KEY;
const secretKey = process.env.BAIDU_OCR_SECRET_KEY;
if (!apiKey || !secretKey) {
throw new Error('百度OCR配置缺失:请检查 BAIDU_OCR_API_KEY 和 BAIDU_OCR_SECRET_KEY');
}
const response = await axios.post(BAIDU_OCR_API.token, null, {
params: {
grant_type: 'client_credentials',
client_id: apiKey,
client_secret: secretKey
},
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
const { access_token, expires_in } = response.data;
if (!access_token) {
throw new Error(`获取百度OCR Token失败: ${JSON.stringify(response.data)}`);
}
accessTokenCache = {
token: access_token,
expireAt: now + (expires_in * 1000)
};
return access_token;
}
async function recognizeText(imageBase64, options = {}) {
const accessToken = await getAccessToken();
const url = `${BAIDU_OCR_API.generalBasic}?access_token=${accessToken}`;
const params = new URLSearchParams();
params.append('image', imageBase64);
if (options.language_type) {
params.append('language_type', options.language_type);
}
const response = await axios.post(url, params.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return response.data;
}
async function recognizeTextAccurate(imageBase64, options = {}) {
const accessToken = await getAccessToken();
const url = `${BAIDU_OCR_API.accurateBasic}?access_token=${accessToken}`;
const params = new URLSearchParams();
params.append('image', imageBase64);
if (options.language_type) {
params.append('language_type', options.language_type);
}
const response = await axios.post(url, params.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return response.data;
}
function getDefaultPrice(type, times) {
const prices = {
year: '120',
halfYear: '60',
quarter: '30',
month: '10'
};
if (type === 'times') {
const timesCount = parseInt(times) || 1;
return String(timesCount * 10);
}
return prices[type] || '10';
}
function extractMembershipInfo(ocrResult) {
if (!ocrResult || !ocrResult.words_result || ocrResult.words_result.length === 0) {
return [];
}
const text = ocrResult.words_result.map(w => w.words).join('\n');
const lines = ocrResult.words_result.map(w => w.words);
const platformKeywords = {
'淘宝': ['淘宝', 'taobao', '88vip', '88VIP'],
'京东': ['京东', 'jd', 'JD', 'plus', 'PLUS'],
'拼多多': ['拼多多', 'pdd', 'PDD'],
'美团': ['美团', 'meituan'],
'饿了么': ['饿了么', 'eleme', 'ele.me'],
'抖音': ['抖音', 'douyin', 'tiktok'],
'快手': ['快手', 'kuaishou'],
'网易云音乐': ['网易云', 'netease', '163'],
'QQ音乐': ['QQ音乐', 'qq音乐'],
'优酷': ['优酷', 'youku'],
'爱奇艺': ['爱奇艺', 'iqiyi'],
'腾讯视频': ['腾讯视频', 'v.qq'],
'哔哩哔哩': ['哔哩哔哩', 'bilibili', 'B站'],
'喜马拉雅': ['喜马拉雅', 'ximalaya'],
'知乎': ['知乎', 'zhihu'],
'百度网盘': ['百度网盘', '百度云']
};
let platform = null;
for (const [pName, keywords] of Object.entries(platformKeywords)) {
for (const keyword of keywords) {
if (text.toLowerCase().includes(keyword.toLowerCase())) {
platform = pName;
break;
}
}
if (platform) break;
}
const typePatterns = [
{ patterns: [/年卡/, /年度会员/, /\d+年/], type: 'year' },
{ patterns: [/半年卡/, /半年会员/, /6个月/], type: 'halfYear' },
{ patterns: [/季卡/, /季度会员/, /3个月/], type: 'quarter' },
{ patterns: [/月卡/, /月度会员/, /1个月/], type: 'month' },
{ patterns: [/次卡/, /按次数/], type: 'times' }
];
let detectedType = 'month';
for (const { patterns, type } of typePatterns) {
for (const pattern of patterns) {
if (pattern.test(text)) {
detectedType = type;
break;
}
}
if (detectedType !== 'month') break;
}
const datePatterns = [
/(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})/,
/(\d{4})(\d{2})(\d{2})/,
/(\d{2})[年/-](\d{1,2})[月/-](\d{1,2})/,
/有效期[至到:]\s*(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})/,
/到期[时间日]:?\s*(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})/,
/(\d{4})\.(\d{1,2})\.(\d{1,2})/
];
let expireDate = '9999-12-31';
for (const pattern of datePatterns) {
const match = text.match(pattern);
if (match) {
let year = match[1];
const month = match[2].padStart(2, '0');
const day = match[3].padStart(2, '0');
if (year.length === 2) {
year = '20' + year;
}
expireDate = `${year}-${month}-${day}`;
break;
}
}
const benefitKeywords = [
'优酷', '网易云', 'QQ音乐', '酷狗', '酷我',
'爱奇艺', '腾讯视频', '芒果TV', '哔哩哔哩',
'饿了么', '美团', '高德打车', '滴滴',
'夸克', '百度网盘', '迅雷',
'喜马拉雅', '知乎', '微博',
'淘票票', '飞猪', '希尔顿', '万豪',
'视频会员', '超级吃货卡', '天猫超市', '天猫国际',
'阿里健康', '专属客服', '省钱卡', '网盘会员',
'打车会员', '金卡', '皮肤装扮', '每日领券',
'出行礼遇', '专享立减', '游戏特权'
];
const benefits = [];
for (const line of lines) {
for (const keyword of benefitKeywords) {
if (line.includes(keyword)) {
const existing = benefits.find(b => b.name === keyword);
if (!existing) {
benefits.push({
name: keyword,
type: detectedType,
times: detectedType === 'times' ? null : null,
price: getDefaultPrice(detectedType, null),
expireDate: expireDate
});
}
}
}
}
if (benefits.length === 0 && platform) {
benefits.push({
name: platform,
type: detectedType,
times: null,
price: getDefaultPrice(detectedType, null),
expireDate: expireDate
});
}
return benefits;
}
module.exports = {
getAccessToken,
recognizeText,
recognizeTextAccurate,
extractMembershipInfo
};