From 6379bbef90a5a4ff425a84bfe04b83a9f71195a2 Mon Sep 17 00:00:00 2001
From: Abner <22141172+Silentely@users.noreply.github.com>
Date: Tue, 23 Jun 2026 21:22:16 +0800
Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20=E7=A7=BB?=
=?UTF-8?q?=E9=99=A4=E5=A4=96=E9=83=A8=E4=BA=8C=E7=BB=B4=E7=A0=81=E6=9C=8D?=
=?UTF-8?q?=E5=8A=A1=E4=BE=9D=E8=B5=96=EF=BC=8C=E6=94=B9=E7=94=A8=E6=9C=AC?=
=?UTF-8?q?=E5=9C=B0=E7=94=9F=E6=88=90=E6=96=B9=E6=A1=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将前端二维码生成从 `qrcode.show` 远程服务切换为 `qrcode-generator` 本地库(UMD 格式),消除对外部 CDN 渲染服务的依赖,降低隐私风险和第三方服务不可用时的故障点
- 重写 `generateQRCodeLocal` 函数适配 `qrcode-generator` API,使用 `qrcode(typeNumber, errorCorrectionLevel).addData().make().createDataURL()` 模式,并基于 QR 码模块数动态计算 `cellSize`,新增大尺寸预览图支持(400px)
- 移除所有页面中的 `qrcode.show` 域名引用,包括 CSP `connectSrc` 策略、preconnect 标签、resource-hints DNS 预解析配置及 Simyo/Giffgaff API 配置中的 `qrcode` 端点
- 完善二维码生成的日志与监控:在 CDN 加载成功后输出 `console.log`,本地/后端生成成功及失败时分别输出对应级别日志,并在 `trackQRCodeEvent` 中增加 `isBrowser` 守卫避免非浏览器环境报错
- 增加生成耗时和 QR 码长度(不含内容)到后端 BFF 日志,同时统一使用 `Date.now()` 计算请求耗时,避免 LPA 激活码等敏感信息进入日志
- 全面更新所有相关测试用例,适配新的 `window.qrcode` 工厂函数 mock,新增日志输出和 Sentry 上报断言,验证本地成功、本地失败降级、后端成功、后端失败四种场景的可观测性
---
docs/SECURITY.md | 2 +-
index.html | 3 +-
netlify/functions/qrcode-generate.js | 14 +-
server.js | 2 +-
src/giffgaff/giffgaff_modular.html | 3 +-
src/giffgaff/js/modules/api-config.js | 3 +-
src/js/modules/qrcode-generator.js | 75 +++++---
src/js/modules/resource-hints.js | 11 +-
src/simyo/js/modules/api-config.js | 5 +-
src/simyo/simyo_modular.html | 3 +-
tests/giffgaff/session-restore-lpa.test.js | 5 +-
tests/modules/qrcode-generator.test.js | 212 +++++++++++++++++----
tests/test_giffgaff_esim.html | 6 +-
tests/test_simyo_esim.html | 6 +-
14 files changed, 257 insertions(+), 93 deletions(-)
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 95ecb40..2b2a181 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -66,7 +66,7 @@ app.use(helmet({
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:", "http:"],
- connectSrc: ["'self'", "https://qrcode.show", "https://api.qrserver.com", "https://appapi.simyo.nl", "https://api.giffgaff.com", "https://id.giffgaff.com", "https://publicapi.giffgaff.com", "https://cdn.jsdelivr.net", "https://*.sentry.io"],
+ connectSrc: ["'self'", "https://appapi.simyo.nl", "https://api.giffgaff.com", "https://id.giffgaff.com", "https://publicapi.giffgaff.com", "https://cdn.jsdelivr.net", "https://*.sentry.io"],
fontSrc: ["'self'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://fonts.gstatic.com"]
}
}
diff --git a/index.html b/index.html
index 0f7c3f2..4ab41cb 100644
--- a/index.html
+++ b/index.html
@@ -10,7 +10,7 @@
-
+
eSIM Tools - Giffgaff & Simyo eSIM 在线管理工具 | 设备更换、激活与二维码生成
@@ -67,7 +67,6 @@
-
diff --git a/netlify/functions/qrcode-generate.js b/netlify/functions/qrcode-generate.js
index 7fb2d86..6f41509 100644
--- a/netlify/functions/qrcode-generate.js
+++ b/netlify/functions/qrcode-generate.js
@@ -48,6 +48,8 @@ function withTimeout(promise, timeoutMs) {
}
exports.handler = withAuth(async (event, context, { body }) => {
+ const startTime = Date.now();
+
// 405 优先于 Schema 验证(确保 HTTP 方法错误优先返回)
if (event.httpMethod !== 'POST') {
throw new AuthError('Method Not Allowed', 405);
@@ -58,6 +60,9 @@ exports.handler = withAuth(async (event, context, { body }) => {
const size = normalizeSize(body.size);
+ // 记录请求开始(不记录 body.data,避免 LPA 激活信息泄露)
+ console.log(`[qrcode-generate] Request received: size=${size}, dataLength=${body.data ? body.data.length : 0}`);
+
try {
const qrcode = await withTimeout(QRCode.toDataURL(body.data, {
errorCorrectionLevel: 'M',
@@ -66,6 +71,11 @@ exports.handler = withAuth(async (event, context, { body }) => {
width: size
}), QR_TIMEOUT_MS);
+ const duration = Date.now() - startTime;
+
+ // 记录成功(不记录 QR 码内容)
+ console.log(`[qrcode-generate] Success: size=${size}, duration=${duration}ms, qrcodeLength=${qrcode.length}`);
+
return {
statusCode: 200,
body: JSON.stringify({
@@ -74,8 +84,10 @@ exports.handler = withAuth(async (event, context, { body }) => {
})
};
} catch (error) {
+ const duration = Date.now() - startTime;
+
// 不记录 data,避免 LPA 激活信息进入日志或 Sentry。
- console.error('[qrcode-generate] QR code generation failed:', error.message);
+ console.error(`[qrcode-generate] Failed: error=${error.message}, duration=${duration}ms`);
// 创建一个不含敏感数据的错误对象,防止 LPA 字符串通过 Sentry 泄露
const sanitizedError = new Error(error.message);
diff --git a/server.js b/server.js
index 68bfc71..d732488 100644
--- a/server.js
+++ b/server.js
@@ -62,7 +62,7 @@ app.use(helmet({
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:", "http:"],
- connectSrc: ["'self'", "https://qrcode.show", "https://api.qrserver.com", "https://appapi.simyo.nl", "https://api.giffgaff.com", "https://id.giffgaff.com", "https://publicapi.giffgaff.com", "https://cdn.jsdelivr.net", "https://*.sentry.io"],
+ connectSrc: ["'self'", "https://appapi.simyo.nl", "https://api.giffgaff.com", "https://id.giffgaff.com", "https://publicapi.giffgaff.com", "https://cdn.jsdelivr.net", "https://*.sentry.io"],
fontSrc: ["'self'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://fonts.gstatic.com"]
}
}
diff --git a/src/giffgaff/giffgaff_modular.html b/src/giffgaff/giffgaff_modular.html
index 004d926..ef1cf38 100644
--- a/src/giffgaff/giffgaff_modular.html
+++ b/src/giffgaff/giffgaff_modular.html
@@ -15,13 +15,12 @@
-
-
+
Giffgaff eSIM 工具
diff --git a/src/giffgaff/js/modules/api-config.js b/src/giffgaff/js/modules/api-config.js
index 63f8239..c2b4e5c 100644
--- a/src/giffgaff/js/modules/api-config.js
+++ b/src/giffgaff/js/modules/api-config.js
@@ -34,8 +34,7 @@ export function getApiEndpoints() {
: "https://publicapi.giffgaff.com/gateway/graphql",
cookieVerify: "/bff/verify-cookie",
autoActivate: "/bff/auto-activate-esim",
- smsActivate: "/bff/giffgaff-sms-activate",
- qrcode: "https://qrcode.show/"
+ smsActivate: "/bff/giffgaff-sms-activate"
};
}
diff --git a/src/js/modules/qrcode-generator.js b/src/js/modules/qrcode-generator.js
index e918bbb..b054013 100644
--- a/src/js/modules/qrcode-generator.js
+++ b/src/js/modules/qrcode-generator.js
@@ -1,7 +1,5 @@
'use strict';
-import { tl } from './i18n.js';
-
// CDN 多源备用列表(全部已验证 HTTP 200 + 浏览器 UMD 全局变量)
// 统一使用 qrcode-generator 包(UMD 格式,设置 window.qrcode 全局变量)
// 注意:jsdelivr 的 qrcode@1.5.4/lib/browser.js 是 CommonJS 模块,浏览器无法使用
@@ -16,6 +14,9 @@ const MAX_QR_SIZE = 600;
const MAX_QR_DATA_LENGTH = 2048;
const BACKEND_ENDPOINT = '/bff/qrcode-generate';
const BACKEND_TIMEOUT_MS = 10000;
+const CDN_LOAD_TIMEOUT_MS = 5000;
+const QR_MARGIN_MODULES = 8;
+const LARGE_PREVIEW_SIZE = 400;
let qrCodeLibraryPromise = null;
@@ -30,8 +31,11 @@ let qrCodeLibraryPromise = null;
*/
function trackQRCodeEvent({ type, source, success, duration, error }) {
try {
+ const isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) return;
+
// 1. 上报到 Sentry(错误事件)
- if (!success && typeof window !== 'undefined' && window.Sentry) {
+ if (!success && window.Sentry) {
window.Sentry.captureMessage(`QR Code ${type} failed`, {
level: 'warning',
tags: {
@@ -47,16 +51,14 @@ function trackQRCodeEvent({ type, source, success, duration, error }) {
}
// 2. 上报到 Analytics(成功和失败都上报)
- if (typeof window !== 'undefined') {
- window.__esimAnalytics = window.__esimAnalytics || [];
- window.__esimAnalytics.push({
- event: type,
- source,
- success,
- duration,
- error: error || null
- });
- }
+ window.__esimAnalytics = window.__esimAnalytics || [];
+ window.__esimAnalytics.push({
+ event: type,
+ source,
+ success,
+ duration,
+ error: error || null
+ });
// 3. 控制台日志(开发调试)
if (!success) {
@@ -136,12 +138,14 @@ export async function loadQRCodeLibrary() {
* @returns {Promise