From 71c89232fdcab8e19478202ff07a74758e77aa83 Mon Sep 17 00:00:00 2001 From: h4erj Date: Wed, 24 Jun 2026 10:31:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=8B=AC=E7=AB=8B=20API=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=EF=BC=8C=E5=88=86=E7=A6=BB=E9=9D=99?= =?UTF-8?q?=E6=80=81=E6=96=87=E4=BB=B6=E4=B8=8E=E8=A1=A8=E5=8D=95=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api-server.js | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 api-server.js diff --git a/api-server.js b/api-server.js new file mode 100644 index 0000000..059824f --- /dev/null +++ b/api-server.js @@ -0,0 +1,153 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const DATA_DIR = path.join(__dirname, 'data'); +const MESSAGE_FILE = path.join(DATA_DIR, 'contact-messages.json'); +const PORT = Number(process.env.PORT || 8080); + +const CORS_ORIGIN = process.env.CORS_ORIGIN || 'https://potaser.github.io'; + +function ensureStore() { + fs.mkdirSync(DATA_DIR, { recursive: true }); + if (!fs.existsSync(MESSAGE_FILE)) fs.writeFileSync(MESSAGE_FILE, '[]\n', 'utf8'); +} + +function readMessages() { + ensureStore(); + try { + const data = JSON.parse(fs.readFileSync(MESSAGE_FILE, 'utf8')); + return Array.isArray(data) ? data : []; + } catch (_) { return []; } +} + +function writeMessages(messages) { + ensureStore(); + fs.writeFileSync(MESSAGE_FILE, JSON.stringify(messages, null, 2) + '\n', 'utf8'); +} + +function sendJson(res, status, payload) { + const body = JSON.stringify(payload); + res.writeHead(status, { + 'Content-Type': 'application/json; charset=utf-8', + 'Access-Control-Allow-Origin': CORS_ORIGIN, + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Content-Length': Buffer.byteLength(body) + }); + res.end(body); +} + +function collectBody(req, limit = 1024 * 1024) { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', chunk => { + body += chunk; + if (Buffer.byteLength(body) > limit) { + reject(new Error('请求内容过大')); + req.destroy(); + } + }); + req.on('end', () => resolve(body)); + req.on('error', reject); + }); +} + +function normalizeText(value, max = 2000) { + return String(value || '').trim().slice(0, max); +} + +function validEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +function validPhone(countryCode, phone) { + const raw = String(phone || '').trim(); + if (!raw) return true; + const normalized = raw.replace(/[\s()-]/g, ''); + const rules = { + '+86': /^1[3-9]\d{9}$/, + '+1': /^[2-9]\d{2}[2-9]\d{6}$/, + '+44': /^7\d{9}$/, + '+81': /^0?\d{9,10}$/, + '+65': /^[689]\d{7}$/, + other: /^\d{6,15}$/ + }; + return (rules[countryCode] || rules.other).test(normalized); +} + +function validCountryCode(code) { + return /^\+[1-9]\d{0,3}$/.test(code); +} + +function validateContact(input) { + const name = normalizeText(input.name, 100); + const email = normalizeText(input.email, 200); + const rawCountryCode = normalizeText(input.countryCode || '+86', 20); + const customCountryCode = normalizeText(input.customCountryCode || '', 20); + const countryCode = rawCountryCode === 'other' ? customCountryCode : rawCountryCode; + const phone = normalizeText(input.phone, 80); + const inquiryTypeValue = normalizeText(input.inquiryType, 100); + const customInquiryType = normalizeText(input.customInquiryType || input.otherInquiryType, 100); + const message = normalizeText(input.message, 4000); + const inquiryType = inquiryTypeValue === 'other' ? customInquiryType : inquiryTypeValue; + + const errors = []; + if (!name) errors.push('姓名必须填写'); + if (!email && !phone) errors.push('邮箱和手机至少填写一项'); + if (email && !validEmail(email)) errors.push('邮箱格式不正确'); + if (phone && rawCountryCode === 'other' && !validCountryCode(countryCode)) errors.push('国家/地区代码格式不正确'); + if (phone && !validPhone(rawCountryCode, phone)) errors.push('手机格式不正确'); + if (!inquiryType) errors.push('咨询类型必须填写'); + if (!message) errors.push('消息内容必须填写'); + + return { + errors, + data: { name, email, countryCode, phone, inquiryType, message } + }; +} + +async function handleContact(req, res) { + try { + const body = await collectBody(req); + const input = JSON.parse(body || '{}'); + const result = validateContact(input); + if (result.errors.length) return sendJson(res, 400, { ok: false, errors: result.errors }); + + const messages = readMessages(); + const now = new Date().toISOString(); + const item = { + id: crypto.randomUUID(), + createdAt: now, + notified: false, + notifiedAt: null, + ...result.data + }; + messages.push(item); + writeMessages(messages); + sendJson(res, 201, { ok: true, id: item.id }); + } catch (error) { + sendJson(res, 500, { ok: false, errors: [error.message || '提交失败'] }); + } +} + +const server = http.createServer((req, res) => { + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': CORS_ORIGIN, + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Max-Age': '86400' + }); + return res.end(); + } + if (req.method === 'POST' && req.url === '/api/contact') return handleContact(req, res); + res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ ok: false, errors: ['Not found'] })); +}); + +ensureStore(); +server.listen(PORT, () => { + console.log(`API server running on port ${PORT}`); +}); diff --git a/package.json b/package.json index 3cd41e0..6f8a9f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "scripts": { - "start": "node server.js", + "start": "node api-server.js", + "server": "node server.js", "check:contacts": "node scripts/check-contact-messages.js" }, "engines": {