Skip to content

Commit 461c0fa

Browse files
smallfawnclaude
andcommitted
feat(wxapp): 新增 4 个小程序脚本,从 GitHub 采集的实现改写为 smallcat 取码登录
来源:GitHub 近 3 个月更新的青龙仓库巡检(134 个仓库 -> 990 个候选脚本 -> 263 个命中小程序特征 -> 三重去重后 143 个我们没有的),从其中"有 code 登录、 可机械转换"的 30 个里先做 4 个。全部改成 wcs.js 取 code -> 业务登录 -> 任务, 统一加了 token 缓存、幂等判定、取码失败独立分支。 shefuyishou.js 社服益寿活动 wx8ac1f54b8fc39c6c /api/silenceLogin -> x-token niuyougu.js 牛油谷 wxfa2a1ee65e3c9122 /api/user/wxlogin(token 在 body) qutaoka.js 趣淘卡 wx479a4a95ec031d79 /api/login(data 直接是 token 串) fxh.js 一汽丰田丰享汇 wxdc0171c19d8ff575 /fxh-bff/app/wx/scx/login -> Authorization+sessionKey 实测(单账号,openid 为本机已绑定账号): fxh 链路通:登录回 code=200/success=true,但 hasLogin=false、token=null -> 该微信号还没在丰享汇注册。已把这种情况单独判出来给准确提示, 而不是笼统报"登录未返回 token"。 shefuyishou ylapi.luckystarpay.com 的 TLS 证书已过期(服务端问题,实测确认, 另外三个站证书都正常)。按规则不关闭证书校验,等对方续期。 niuyougu app.niuyougu.com.cn 直连与代理均 ReadTimeout,本机不可达。 qutaoka 登录恒回 {"status":0,"msg":"请求参数错误!"};form/json、只带 code、 nickname 给值四种形状都试过,需要一次真机抓包才能补齐,不猜参数。 fixture:4 个脚本 × ok/已签到/取码失败(+fxh 未注册) 共 13 组场景、114 条断言全通过, 断言覆盖 appid、UA、Referer、token 放在头还是 body、已签到必须当成成功、 取码失败必须有独立日志且不去打目标站。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b82ace8 commit 461c0fa

4 files changed

Lines changed: 878 additions & 0 deletions

File tree

‎wxapp/fxh.js‎

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
------------------------------------------
3+
@Description: 一汽丰田丰享汇 - 微信小程序静默登录 + 会员签到
4+
cron: 28 8 * * *
5+
------------------------------------------
6+
变量名:fxh
7+
变量值:wx_server 里的 openid/账号标识,多账号用 & 或换行分隔(可加 #备注)
8+
9+
依赖变量:
10+
wx_server_url 默认 http://192.168.31.196:8787
11+
wx_auth 必填,wx_server 鉴权值
12+
------------------------------------------
13+
契约(appid wxdc0171c19d8ff575,host fxh.ftms.com.cn):
14+
登录 POST /fxh-bff/app/wx/scx/login
15+
{jsCode:<code>, nickName:"微信用户", gender:0, language/city/province/country:"",
16+
avatarUrl:"", iv:"", signature:"", headImg:""}
17+
-> code==200, data.token;同时把 token 放进请求头 Authorization 和 sessionKey
18+
昵称在 data.customerInfoCacheDto.name
19+
积分 POST /fxh-bff/app/membership/pointsSum
20+
签到 POST /fxh-bff/app/membership/signIn -> code==200 成功
21+
注:原始实现里 signature 是一串写死的 sha1(别人抓包留下的用户资料签名),
22+
这里留空——那个值对本账号没有意义,也不该照抄别人的。
23+
------------------------------------------
24+
*/
25+
26+
const { Env } = require("../tools/env.js");
27+
const $ = new Env("一汽丰田丰享汇");
28+
const axios = require("axios");
29+
const fs = require("fs");
30+
const path = require("path");
31+
const WeChatServer = require("./wcs.js");
32+
33+
const ckName = "fxh";
34+
const MINI_APP_ID = "wxdc0171c19d8ff575";
35+
const BASE = "https://fxh.ftms.com.cn";
36+
const TOKEN_CACHE_FILE = path.join(__dirname, "fxh_token_cache.json");
37+
const USER_AGENT =
38+
"Mozilla/5.0 (Linux; Android 12; M2012K11AC Build/SKQ1.220303.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) " +
39+
"Version/4.0 Chrome/134.0.6998.136 Mobile Safari/537.36 MicroMessenger/8.0.48.2580(0x28003036) MiniProgramEnv/android";
40+
41+
const EP_LOGIN = "/fxh-bff/app/wx/scx/login";
42+
const EP_POINTS = "/fxh-bff/app/membership/pointsSum";
43+
const EP_SIGN = "/fxh-bff/app/membership/signIn";
44+
45+
const wechat = new WeChatServer({
46+
url: process.env.wx_server_url || "http://192.168.31.196:8787",
47+
appid: MINI_APP_ID,
48+
auth: process.env.wx_auth || "",
49+
});
50+
51+
function readCache() {
52+
try {
53+
if (!fs.existsSync(TOKEN_CACHE_FILE)) return {};
54+
return JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, "utf8")) || {};
55+
} catch (e) {
56+
return {};
57+
}
58+
}
59+
60+
function writeCache(cache) {
61+
try {
62+
fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
63+
} catch (e) {
64+
$.log(`写入token缓存失败: ${e.message || e}`);
65+
}
66+
}
67+
68+
function parseAccount(raw = "") {
69+
const [id, remark] = String(raw).split("#").map((s) => (s || "").trim());
70+
return { openid: id, remark: remark || "" };
71+
}
72+
73+
function short(v, n = 220) {
74+
const t = typeof v === "string" ? v : JSON.stringify(v);
75+
return !t ? "" : t.length > n ? `${t.slice(0, n)}...` : t;
76+
}
77+
78+
const isOk = (res) => Number(res?.code) === 200 || res?.success === true;
79+
const msgOf = (res) => res?.message || res?.msg || short(res);
80+
const isAlreadyDone = (t) => /已签|已经签|签到过|重复|already/i.test(String(t || ""));
81+
const isAuthError = (t) => /登录|token|未授权|失效|过期|重新|401/i.test(String(t || ""));
82+
83+
class Task {
84+
constructor(raw) {
85+
this.index = $.userIdx++;
86+
this.account = parseAccount(raw);
87+
this.token = "";
88+
this.name = "";
89+
}
90+
91+
log(text) {
92+
$.log(`账号[${this.index}]${this.account.remark ? `[${this.account.remark}]` : ""} ${text}`);
93+
}
94+
95+
async request(apiPath, data = {}) {
96+
const res = await axios.request({
97+
method: "POST",
98+
url: `${BASE}${apiPath}`,
99+
data,
100+
headers: {
101+
"Content-Type": "application/json",
102+
"User-Agent": USER_AGENT,
103+
Referer: `https://servicewechat.com/${MINI_APP_ID}/0/page-frame.html`,
104+
...(this.token ? { Authorization: this.token, sessionKey: this.token } : {}),
105+
},
106+
timeout: 20000,
107+
validateStatus: () => true,
108+
});
109+
if (res.status !== 200) throw new Error(`${apiPath} HTTP ${res.status}: ${short(res.data)}`);
110+
return res.data;
111+
}
112+
113+
/** wcs.getCode 在 status:false 时也 resolve,必须自己判失败 */
114+
async getCode() {
115+
const { data } = await wechat.getCode(this.account.openid);
116+
if (data && data.status === false) {
117+
throw new Error(`wx_server 取code失败: ${data.message || short(data)}`);
118+
}
119+
const code = data?.data?.code || data?.code;
120+
if (!code || typeof code !== "string") throw new Error(`wx_server 未返回 code: ${short(data)}`);
121+
return code;
122+
}
123+
124+
async login() {
125+
const jsCode = await this.getCode();
126+
const res = await this.request(EP_LOGIN, {
127+
nickName: "微信用户",
128+
gender: 0,
129+
language: "",
130+
city: "",
131+
province: "",
132+
country: "",
133+
avatarUrl: "",
134+
iv: "",
135+
signature: "",
136+
jsCode,
137+
headImg: "",
138+
});
139+
if (!isOk(res)) throw new Error(`登录失败: ${msgOf(res)}`);
140+
const d = res.data || {};
141+
this.token = d.token || "";
142+
this.name = (d.customerInfoCacheDto || {}).name || "";
143+
if (!this.token) {
144+
// 服务端会用 code=200 + hasLogin:false + token:null 表示「这个微信号还没注册」,
145+
// 不是脚本坏了,也不是 code 无效(customerId 是正常返回的)。
146+
if (d.hasLogin === false) {
147+
throw new Error(
148+
"该微信号还没在丰享汇注册/绑定(服务端返回 hasLogin:false)," +
149+
"需先在小程序「一汽丰田丰享汇」里手动完成注册后再跑"
150+
);
151+
}
152+
throw new Error(`登录未返回 token: ${short(res)}`);
153+
}
154+
const cache = readCache();
155+
cache[this.account.openid] = { token: this.token, name: this.name, updatedAt: new Date().toISOString() };
156+
writeCache(cache);
157+
this.log(`登录成功${this.name ? `: ${this.name}` : ""}`);
158+
}
159+
160+
async ensureLogin() {
161+
const cached = readCache()[this.account.openid] || {};
162+
if (!this.token && cached.token) {
163+
this.token = cached.token;
164+
this.name = cached.name || "";
165+
if (await this.queryPoints(false)) {
166+
this.log("使用缓存token");
167+
return;
168+
}
169+
this.log("缓存token失效,重新登录");
170+
this.token = "";
171+
}
172+
if (!this.token) await this.login();
173+
}
174+
175+
async queryPoints(needLog = true) {
176+
const res = await this.request(EP_POINTS, {});
177+
if (!isOk(res)) {
178+
if (needLog) this.log(`读取积分失败: ${msgOf(res)}`);
179+
return false;
180+
}
181+
const d = res.data;
182+
const points = d && typeof d === "object" ? d.pointsSum ?? d.points ?? d.total ?? short(d, 80) : d;
183+
if (needLog) this.log(`会员: ${this.name || "未知"} 积分: ${points ?? "未知"}`);
184+
return true;
185+
}
186+
187+
async sign(retry = true) {
188+
const res = await this.request(EP_SIGN, {});
189+
if (isOk(res)) return this.log("✅ 签到成功");
190+
if (isAlreadyDone(msgOf(res))) return this.log(`✅ 今日已签到(${msgOf(res)})`);
191+
if (retry && isAuthError(msgOf(res))) {
192+
this.log("会话失效,重新登录后重试");
193+
this.token = "";
194+
await this.login();
195+
return this.sign(false);
196+
}
197+
this.log(`❌ 签到失败: ${msgOf(res)}`);
198+
}
199+
200+
async run() {
201+
if (!this.account.openid) {
202+
this.log("跳过:变量值里没有 openid");
203+
return;
204+
}
205+
try {
206+
await this.ensureLogin();
207+
await this.queryPoints();
208+
await this.sign();
209+
await this.queryPoints();
210+
} catch (e) {
211+
this.log(`执行失败: ${e.message || e}`);
212+
}
213+
}
214+
}
215+
216+
!(async () => {
217+
$.checkEnv(ckName);
218+
if (!$.userCount) {
219+
$.log(`未找到变量 ${ckName}`);
220+
return;
221+
}
222+
for (let i = 0; i < $.userList.length; i++) {
223+
await new Task($.userList[i]).run();
224+
if (i < $.userList.length - 1) await $.wait(1500, 3000);
225+
}
226+
})()
227+
.catch((e) => $.log(e.message || e))
228+
.finally(() => $.done());

0 commit comments

Comments
 (0)