From 51eeea3be32d1657e06f0f9e73f8a501e54e136d Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 8 Mar 2026 03:40:53 -0500 Subject: [PATCH] 's' --- app/api/join/route.ts | 2 +- app/api/pay/route.ts | 301 ++++++++++++++++++++++++++++++++++++ app/api/pay/status/route.ts | 22 +++ app/components/Tools.tsx | 172 +++++---------------- app/data/tools.json | 219 ++++++++++++++++++++++++++ app/join/page.tsx | 97 +++++++++++- app/lib/env.ts | 19 +++ app/lib/payEnv.ts | 53 +++++++ 8 files changed, 747 insertions(+), 138 deletions(-) create mode 100644 app/api/pay/route.ts create mode 100644 app/api/pay/status/route.ts create mode 100644 app/data/tools.json create mode 100644 app/lib/env.ts create mode 100644 app/lib/payEnv.ts diff --git a/app/api/join/route.ts b/app/api/join/route.ts index ebd9a8d..0bc5b60 100644 --- a/app/api/join/route.ts +++ b/app/api/join/route.ts @@ -101,7 +101,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: false, error: errMsg }, { status: 500 }); } - return NextResponse.json({ ok: true }); + return NextResponse.json({ ok: true, user_id: userId }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); console.error("Join API error:", e); diff --git a/app/api/pay/route.ts b/app/api/pay/route.ts new file mode 100644 index 0000000..10b9456 --- /dev/null +++ b/app/api/pay/route.ts @@ -0,0 +1,301 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PAYMENT_API_URL, SITE_URL } from "../../lib/env"; + +const ZPAY_SUBMIT = "https://zpayz.cn/submit.php"; + +async function createPayOrder( + user_id: string, + return_url: string, + total_fee = 80, + type = "meetup", + channel = "alipay" +) { + if (!PAYMENT_API_URL) throw new Error("未配置 PAYMENT_API_URL"); + + const payload = { + user_id, + total_fee: Number(total_fee) || 80, + type, + channel, + return_url, + }; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + + const res = await fetch(`${PAYMENT_API_URL}/payh5`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: controller.signal, + }).finally(() => clearTimeout(timeout)); + + const data = await res.json().catch(() => ({})); + if (!res.ok || data?.status !== "ok") { + throw new Error(data?.detail || data?.msg || data?.message || "支付创建失败"); + } + + let params = data.params || data.xorpay_params || {}; + const payUrl = data.pay_url || data.xorpay_url || ZPAY_SUBMIT; + if (!params || typeof params !== "object") { + throw new Error("payjsapi 未返回支付参数"); + } + params = JSON.parse(JSON.stringify(params)); + if (data.provider === "xorpay") { + throw new Error("当前 payjsapi 使用 xorpay,请设置 PAYMENT_PROVIDER=zpay 后重启 payjsapi"); + } + return { params, payUrl }; +} + +const ZPAY_REQUIRED = ["pid", "type", "out_trade_no", "notify_url", "return_url", "name", "money", "sign", "sign_type"]; + +function escapeAttr(s: string): string { + return String(s) + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">"); +} + +function buildPayHtml(payUrl: string, params: Record): string { + const flat: Record = {}; + for (const [k, v] of Object.entries(params)) { + if (v == null) continue; + const s = String(v).trim(); + if (s === "") continue; + flat[k] = s; + } + for (const key of ZPAY_REQUIRED) { + if (!flat[key]) { + console.error(`ZPAY 缺少必填参数: ${key}`, flat); + } + } + const inputs = Object.entries(flat) + .map(([k, v]) => ``) + .join("\n"); + + return ` + + + + + 跳转支付... + + + +

正在跳转到支付页面...

+
+ ${inputs} +
+ + +`; +} + +export async function POST(request: NextRequest) { + try { + let body: Record; + const contentType = request.headers.get("content-type") || ""; + if (contentType.includes("application/json")) { + body = await request.json(); + } else { + const form = await request.formData(); + body = Object.fromEntries(form.entries()) as Record; + } + + const user_id = String(body?.user_id || "").trim(); + const return_url = String(body?.return_url || "").trim(); + const total_fee = Number(body?.total_fee) || 80; + const type = String(body?.type || "meetup"); + const channel = String(body?.channel || "alipay"); + const device = String(body?.device || "pc"); // pc | h5,前端根据 UA 传入 + + if (!user_id) { + return NextResponse.json({ ok: false, error: "缺少 user_id" }, { status: 400 }); + } + + const origin = + request.headers.get("x-forwarded-host") + ? `${request.headers.get("x-forwarded-proto") || "https"}://${request.headers.get("x-forwarded-host")}` + : request.headers.get("origin") || SITE_URL; + const finalReturnUrl = return_url || `${origin}/join?paid=1`; + + const { params, payUrl } = await createPayOrder( + user_id, + finalReturnUrl, + total_fee, + type, + channel + ); + + if (!params || Object.keys(params).length === 0) { + return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 }); + } + + const accept = request.headers.get("accept") || ""; + const wantHtml = String(body?.html) === "1" || accept.includes("text/html"); + if (wantHtml) { + const clientIp = + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + request.headers.get("x-real-ip") || + ""; + // 使用 payjsapi /payh5/redirect:mapi.php API接口支付,按 device 返回 payurl/payurl2 + const redirectPayload = { + user_id, + total_fee, + type, + channel, + return_url: finalReturnUrl, + device, + ...(clientIp && { client_ip: clientIp }), + }; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15000); + const redirectRes = await fetch(`${PAYMENT_API_URL}/payh5/redirect`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(redirectPayload), + redirect: "manual", + signal: controller.signal, + }).finally(() => clearTimeout(timeout)); + if (redirectRes.status >= 301 && redirectRes.status <= 308) { + const location = redirectRes.headers.get("location"); + if (location) return NextResponse.redirect(location); + } + const resData = await redirectRes.json().catch(() => ({})); + if (redirectRes.status === 200 && resData?.qrcode_mode && resData?.img) { + const returnUrl = String(resData.return_url || finalReturnUrl).replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c + ); + const imgSrc = String(resData.img).replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c + ); + const orderId = String(resData.order_id || "").replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c + ); + const ch = String(resData.channel || "alipay").toLowerCase(); + const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat"; + const title = isWx ? "微信扫码支付" : "支付宝扫码支付"; + const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付"; + const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c); + const html = ` + +${esc(title)} + + +

${esc(title)}

+

${esc(hint)}

+支付二维码 +

支付成功后将自动跳转...

+ +`; + return new NextResponse(html, { + status: 200, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + const errData = resData; + if (redirectRes.status === 400 || redirectRes.status === 500) { + return NextResponse.json( + { ok: false, error: errData?.detail || errData?.msg || "支付跳转失败" }, + { status: redirectRes.status } + ); + } + // 降级:payjsapi 未提供 redirect 或非 zpay,使用原有表单提交逻辑 + const formParams = new URLSearchParams(); + const missing: string[] = []; + for (const key of ZPAY_REQUIRED) { + const v = params[key]; + if (v == null || String(v).trim() === "") { + missing.push(key); + } else { + formParams.append(key, String(v)); + } + } + for (const [k, v] of Object.entries(params)) { + if (!ZPAY_REQUIRED.includes(k) && v != null && String(v).trim() !== "") { + formParams.append(k, String(v)); + } + } + if (missing.length > 0) { + return NextResponse.json( + { + ok: false, + error: `payjsapi 返回参数缺少: ${missing.join(", ")}。请确认 payjsapi 已设置 PAYMENT_PROVIDER=zpay`, + }, + { status: 500 } + ); + } + const zpayRes = await fetch(ZPAY_SUBMIT, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formParams.toString(), + redirect: "manual", + }); + const location = zpayRes.headers.get("location"); + if ((zpayRes.status === 301 || zpayRes.status === 302) && location) { + return NextResponse.redirect(location); + } + const text = await zpayRes.text(); + try { + const errJson = JSON.parse(text); + if (errJson?.code === "error") { + return NextResponse.json( + { ok: false, error: errJson.msg || "ZPAY 返回错误" }, + { status: 500 } + ); + } + } catch { + /* ignore */ + } + const html = buildPayHtml(payUrl, params as Record); + return new NextResponse(html, { + status: 200, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + + return NextResponse.json({ + ok: true, + pay_url: payUrl, + params, + provider: "zpay", + submit_method: "POST", + }); + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)); + const msg = err.message || ""; + const hint = + msg.includes("fetch") || msg.includes("ECONNREFUSED") + ? "请确认 payjsapi 已启动:cd C:\\project\\payjsapi && python run.py" + : err.name === "AbortError" + ? "请求超时,请检查 payjsapi 是否正常运行" + : msg; + console.error("Pay API error:", err); + return NextResponse.json( + { ok: false, error: `支付请求失败: ${hint}` }, + { status: 500 } + ); + } +} diff --git a/app/api/pay/status/route.ts b/app/api/pay/status/route.ts new file mode 100644 index 0000000..a40f0da --- /dev/null +++ b/app/api/pay/status/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PAYMENT_API_URL } from "../../../lib/env"; + +export async function GET(request: NextRequest) { + const orderId = request.nextUrl.searchParams.get("order_id"); + if (!orderId) { + return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 }); + } + if (!PAYMENT_API_URL) { + return NextResponse.json({ ok: false, error: "未配置 PAYMENT_API_URL" }, { status: 500 }); + } + try { + const res = await fetch( + `${PAYMENT_API_URL}/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`, + { cache: "no-store" } + ); + const data = await res.json().catch(() => ({})); + return NextResponse.json({ ok: true, paid: !!data?.paid }); + } catch (e) { + return NextResponse.json({ ok: false, paid: false, error: String(e) }, { status: 500 }); + } +} diff --git a/app/components/Tools.tsx b/app/components/Tools.tsx index 1ca5a6d..8beb798 100644 --- a/app/components/Tools.tsx +++ b/app/components/Tools.tsx @@ -1,124 +1,28 @@ -const categories = [ - { - icon: "💬", - title: "远程协作", - count: 12, - tools: [ - { name: "Slack", desc: "团队沟通" }, - { name: "Notion", desc: "知识管理" }, - { name: "Linear", desc: "项目追踪" }, - { name: "Figma", desc: "协同设计" }, - ], - color: "from-sky-500 to-cyan-500", - bg: "bg-sky-50", - text: "text-sky-700", - }, - { - icon: "🎥", - title: "视频会议", - count: 8, - tools: [ - { name: "Zoom", desc: "视频会议" }, - { name: "Google Meet", desc: "在线会议" }, - { name: "Around", desc: "轻量会议" }, - { name: "Loom", desc: "异步视频" }, - ], - color: "from-violet-500 to-purple-500", - bg: "bg-violet-50", - text: "text-violet-700", - }, - { - icon: "📋", - title: "项目管理", - count: 10, - tools: [ - { name: "Trello", desc: "看板管理" }, - { name: "Asana", desc: "任务管理" }, - { name: "Monday", desc: "工作流" }, - { name: "ClickUp", desc: "全能协作" }, - ], - color: "from-emerald-500 to-teal-500", - bg: "bg-emerald-50", - text: "text-emerald-700", - }, - { - icon: "💻", - title: "开发工具", - count: 15, - tools: [ - { name: "VS Code", desc: "代码编辑" }, - { name: "GitHub", desc: "代码托管" }, - { name: "Vercel", desc: "部署平台" }, - { name: "Cursor", desc: "AI 编程" }, - ], - color: "from-blue-500 to-indigo-500", - bg: "bg-blue-50", - text: "text-blue-700", - }, - { - icon: "🎨", - title: "设计工具", - count: 9, - tools: [ - { name: "Figma", desc: "UI 设计" }, - { name: "Canva", desc: "图形设计" }, - { name: "Framer", desc: "网页设计" }, - { name: "Midjourney", desc: "AI 作图" }, - ], - color: "from-pink-500 to-rose-500", - bg: "bg-pink-50", - text: "text-pink-700", - }, - { - icon: "💳", - title: "财务工具", - count: 7, - tools: [ - { name: "Wise", desc: "跨境转账" }, - { name: "PayPal", desc: "在线支付" }, - { name: "Stripe", desc: "收款平台" }, - { name: "Deel", desc: "薪酬合规" }, - ], - color: "from-amber-500 to-orange-500", - bg: "bg-amber-50", - text: "text-amber-700", - }, - { - icon: "🔒", - title: "安全与 VPN", - count: 6, - tools: [ - { name: "NordVPN", desc: "网络安全" }, - { name: "1Password", desc: "密码管理" }, - { name: "Authy", desc: "双因素认证" }, - { name: "ProtonMail", desc: "加密邮件" }, - ], - color: "from-slate-500 to-gray-600", - bg: "bg-slate-50", - text: "text-slate-700", - }, - { - icon: "📝", - title: "生产力", - count: 11, - tools: [ - { name: "Obsidian", desc: "笔记系统" }, - { name: "Todoist", desc: "待办清单" }, - { name: "RescueTime", desc: "时间追踪" }, - { name: "Arc", desc: "浏览器" }, - ], - color: "from-teal-500 to-cyan-500", - bg: "bg-teal-50", - text: "text-teal-700", - }, -]; +import toolsData from "../data/tools.json"; -const toolStats = [ - { value: "50+", label: "精选工具" }, - { value: "8", label: "分类" }, - { value: "15", label: "开发工具" }, - { value: "12", label: "协作工具" }, -]; +const categories = toolsData.categories as Array<{ + icon: string; + title: string; + color: string; + bg: string; + text: string; + tools: Array<{ name: string; desc: string; href: string }>; +}>; + +function truncateDesc(desc: string, maxLen = 5) { + if (!desc || desc.length <= maxLen) return desc; + return desc.slice(0, maxLen) + "..."; +} + +const toolStats = (() => { + const total = categories.reduce((s, c) => s + c.tools.length, 0); + return [ + { value: `${total}+`, label: "精选工具" }, + { value: String(categories.length), label: "分类" }, + { value: "数字游民", label: "出海" }, + { value: "佣金", label: "推荐" }, + ]; +})(); export default function Tools() { return ( @@ -132,15 +36,15 @@ export default function Tools() { 工具推荐

- 数字游民必备工具精选,覆盖远程协作、开发、设计、财务等 8 大分类。 + 数字游民出海必备工具精选,含游民社区、VPS、金融收款、出海掘金等,每个链接可点击跳转。

-
+
{categories.map((cat) => (
@@ -150,20 +54,28 @@ export default function Tools() {

{cat.title}

- {cat.count} + {cat.tools.length}
{cat.tools.map((tool) => ( -
- - {tool.name} + {tool.name} + + {truncateDesc(tool.desc)} + {tool.desc && tool.desc.length > 5 && ( + + {tool.desc} + + )} - {tool.desc} -
+ ))}
diff --git a/app/data/tools.json b/app/data/tools.json new file mode 100644 index 0000000..46ffc35 --- /dev/null +++ b/app/data/tools.json @@ -0,0 +1,219 @@ +{ + "categories": [ + { + "id": "nomad-community", + "icon": "👥", + "title": "游民社区", + "color": "from-sky-500 to-cyan-500", + "bg": "bg-sky-50", + "text": "text-sky-700", + "tools": [ + { "name": "nomadro", "desc": "出海导航", "href": "https://nomadro.com" }, + { "name": "nomadlist", "desc": "海外最大的数字游民社区", "href": "https://nomads.com?ref=https://nomadro.com" }, + { "name": "nomadcna", "desc": "国内数字游民社区", "href": "https://meetup.hackrobot.cn" } + ] + }, + { + "id": "toolbox", + "icon": "🔧", + "title": "工具箱", + "color": "from-violet-500 to-purple-500", + "bg": "bg-violet-50", + "text": "text-violet-700", + "tools": [ + { "name": "ChatGPT", "desc": "OpenAI开发的人工智能聊天机器人程序", "href": "https://chat.openai.com/" }, + { "name": "Grok", "desc": "xAI 免费 AI 助手", "href": "https://grok.com/" }, + { "name": "沉浸式翻译", "desc": "双语翻译插件", "href": "https://immersivetranslate.com/?via=nomad-vps" }, + { "name": "奈飞小铺", "desc": "代充值,外网账号合租平台", "href": "https://ihezu.art/CZhn6c" } + ] + }, + { + "id": "us-phone", + "icon": "📱", + "title": "美国手机号", + "color": "from-emerald-500 to-teal-500", + "bg": "bg-emerald-50", + "text": "text-emerald-700", + "tools": [ + { "name": "1psim", "desc": "低成本:2.5$/m,支持esim和实体卡(邮寄中国)", "href": "https://1psim.com/7GARRL" }, + { "name": "yesim", "desc": "美国手机号,3$/m", "href": "https://yesim.app/?partner_id=1948" }, + { "name": "tello", "desc": "最低5$/m,支持esim", "href": "https://tello.com/" }, + { "name": "9esim", "desc": "让任何手机支持esim,可写数据的SIM实体卡", "href": "https://www.9esim.com/?coupon=XIAOSHUANGERIC2" } + ] + }, + { + "id": "finance", + "icon": "💳", + "title": "金融收款", + "color": "from-amber-500 to-orange-500", + "bg": "bg-amber-50", + "text": "text-amber-700", + "tools": [ + { "name": "wise", "desc": "线上开通境外银行账户(身份证即可)", "href": "https://wise.com/invite/amc/xiaoshuangh" }, + { "name": "OKX交易所", "desc": "加密货币交易所", "href": "https://okx.com/join/84999861" }, + { "name": "safepal (U卡)", "desc": "出海神卡,邀请码:104809", "href": "https://www.safepal.com/zh-cn/" }, + { "name": "paypal", "desc": "出海收款必备,tiktok提现", "href": "https://py.pl/1tyAwY" }, + { "name": "buy me coffee", "desc": "赞助收款平台", "href": "https://www.buymeacoffee.com/invite/xiaoshuangi" } + ] + }, + { + "id": "vps", + "icon": "🖥️", + "title": "VPS服务器", + "color": "from-blue-500 to-indigo-500", + "bg": "bg-blue-50", + "text": "text-blue-700", + "tools": [ + { "name": "vultr", "desc": "可按分钟计费的服务器 (可支付宝)", "href": "https://www.vultr.com/?ref=8985003" }, + { "name": "bandwagonhost", "desc": "针对中国线路优化的高质量vps", "href": "https://bandwagonhost.com/aff.php?aff=25463" }, + { "name": "RackNerd", "desc": "最便宜的美国vps服务器", "href": "https://my.racknerd.com/aff.php?aff=15630" }, + { "name": "腾讯云", "desc": "腾讯服务器,国内速度快", "href": "https://curl.qcloud.com/BZZ2MBTN" }, + { "name": "阿里云", "desc": "阿里巴巴旗下,vps服务器", "href": "https://www.aliyun.com/minisite/goods?userCode=dii0ofzg" } + ] + }, + { + "id": "residential-ip", + "icon": "🌐", + "title": "静态住宅IP", + "color": "from-pink-500 to-rose-500", + "bg": "bg-pink-50", + "text": "text-pink-700", + "tools": [ + { "name": "iproyal", "desc": "静态住宅ip,出海必备", "href": "https://iproyal.cn/?r=nomadro" }, + { "name": "proxy-cheap", "desc": "便宜的住宅ip", "href": "https://app.proxy-cheap.com/r/bmcMoY" }, + { "name": "smartproxy", "desc": "静态住宅ip服务商", "href": "https://www.smartproxy.org/register/?invitation_code=7RXPLM" } + ] + }, + { + "id": "residential-vps", + "icon": "🏠", + "title": "住宅VPS服务器", + "color": "from-rose-500 to-pink-500", + "bg": "bg-rose-50", + "text": "text-rose-700", + "tools": [ + { "name": "lisa", "desc": "住宅ip服务器,tiktok必备", "href": "https://lisahost.com/aff.php?aff=7566" }, + { "name": "voyracloud", "desc": "静态住宅ip服务器", "href": "https://www.voyracloud.com/?ref_code=2LJPRTE5" } + ] + }, + { + "id": "hardware", + "icon": "🖲️", + "title": "硬件", + "color": "from-slate-500 to-gray-600", + "bg": "bg-slate-50", + "text": "text-slate-700", + "tools": [ + { "name": "树莓派", "desc": "无与伦比的ARM开发板", "href": "https://uland.taobao.com/coupon/edetail?e=5hkWkiThOmClhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOWBzzpT26idJPSbzO0ArF68ertbZl9wqRUbeJevxT02sHgNIxyFsmaMOsi2OxhyQb%2BhYUTTNN7GRRSHvQe2jOLZ9pbNCYX0I%2BPP%2BWUTgK%2F%2B0I%2BtaUgbudUxA%2B536asYsLWVfKa%2BhVnND4NyB1fwjO3bJLf667Yy8Q5jB6TX2HR3Qrq8LCw4Dujjqyaqc1rGLUwdQHHM8iY%2BUDMxLRSiIntldeBz90au3REAmOgZ0RtdnfeSBK3KXa0%2FA9snq37kKL6zL4bZ3EAS%2F2oYsLHTsIOxroXBFP6oz%2BA%3D%3D&traceId=213e032e17650447230113327e0c4f&union_lens=lensId%3APUB%401765044691%400b521400_0da4_19af4dca231_da77%400290EZjuZ2xbgOlH3NTYAyI%40eyJmbG9vcklkIjoyMDg3MTcxMDEwLCJzcG1CIjoiiX3BvcnRhbF92Ml90b29sX3NlbGVjdGlvbmxpc3RfZGV0YWlsX2luZGV4X2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY2NCJ9" }, + { "name": "迷你主机", "desc": "具有性价比的X86小主机", "href": "https://s.click.taobao.com/t?e=m%3D2%26s%3DCb1Quo%2F0Wb5w4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFuhCZFSw9yq2woqx0EHqItb0JlhLk0Jl4eVWmNyHytKdTKJ80yOOA0Mtd0e5Ku7CIDk5ffgV5fRI1qrpxiwMoCNxc1AtbZGVS936JshS%2F%2FIdwgQgG5hIzyLNEPXytV9ALtCLThlbPuuZLb93Df8fOziGg7JVKZos%2BjLJJgRsfWlWtajntMnDLEHhuApO%2F8klJ62RdK%2B6%2FP7Z6k6s3rDgsC6jO9AJYjY8CXJ%2BwEVkOqHFPSJJXArlDuzso5nyPLY4NPiLqets6lIJ" }, + { "name": "GPU显卡", "desc": "AI时代的算力基础设施", "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATUJK1olXwEDUFZVCkkRC18IGloWWwALV19aAUonRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoUBWkBGFoSVAcdDRsBVXsQB29pbiZRKGMHKlclCEgVWghcGV91UQoyVW5dCUoWAmYPHl4RbTYCU24fZp-esLulkIK0yd6n5V1eOEonA2kPH1oQWQUFV19VCnsQA2Y4El0dWQ4LU1hBC00SCmcJK2slXjYFVFdJDjlWUXsOaWslXTYBZF5cCEoQAGoLH1kRQQYHV1lbAFcXBWgMGl4RXgUKUllYOEkWAmsBK2vL05J3IB9Uajl8UStqSz4dFQ9jitDJGTlnA2sPH0olImF5ECFaSU0VVAdNQDxuW09KFiw8SyljbW1jcih3JWNxNlwaS0hqWBhoX2sQbQEEVW4" } + ] + }, + { + "id": "software", + "icon": "⚙️", + "title": "软件", + "color": "from-indigo-500 to-blue-500", + "bg": "bg-indigo-50", + "text": "text-indigo-700", + "tools": [ + { "name": "3X-UI", "desc": "科学上网的正确姿势,仪表盘", "href": "https://github.com/MHSanaei/3x-ui" }, + { "name": "虚空终端mihomo", "desc": "代理软件以及配置wiki", "href": "https://wiki.metacubex.one/config/" }, + { "name": "frp", "desc": "高性能内网穿透", "href": "https://gofrp.org/zh-cn/docs/" }, + { "name": "wireguard", "desc": "P2P打洞组网", "href": "https://github.com/WireGuard/wireguard-go" }, + { "name": "lineageos (pi5)", "desc": "树莓派的安卓:linageos", "href": "https://konstakang.com/devices/rpi5/" } + ] + }, + { + "id": "digital-gear", + "icon": "📷", + "title": "数码装备", + "color": "from-cyan-500 to-teal-500", + "bg": "bg-cyan-50", + "text": "text-cyan-700", + "tools": [ + { "name": "AI眼镜", "desc": "第一视角拍摄", "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATkJK1olXw8AXVxfCU8UA18IGloVWwcKXFlZDUgnRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoXBW4AE1wRWAUdDRsBVXtwXw1pZzNKAmZgFFwlYUh8UzVBZyZlUQoyVW5dCUoWAmYPHl4RbTYCU24fZpO9hbeBtYKjxt-jwInikZ-fjV8JK1sTWgIDUVtVAEkVA2w4HFscbQ8EXFpVAUwRH2wOHlIdXDYyZF1tD0seF2l6WgkBW3QyZF5tC3sXAm8AGFsVXAEBXEJdDUMVB24UG10SWQcHUVZbAE8fCl8KGloRVDYyitPtY0JrQAYIYj0dXGZKMVc5T5Was35jYlMVXQUTZAQOch9SZTMNbC9gIGdQJi5VCzkRSCpfWDUXVAJlDQoCbw1gcG9qRwYdHnYyUW5aDEgn" }, + { "name": "运动相机", "desc": "骑行记录仪", "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eCE0UB28ME1slGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWXQABUF5ZAEsIWipURmt3CFhXIQkpbChuRy8JGDpRJUJQDwkbBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVpaDEsUM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmsNHl8UXQQeVF1ZDE0UH28OHF8UWAIDUVddCkgnAW4JH1IlbdiMwDxcaAJfBTlNTRpCBHpHLhyDht8GcR8BHFIUTDZ6NSMpYRJweWtBfAUXC2JyXBkpfTNRWi1mGV4UAlgENFpeDA13cRRcTAJLbQMyU1peOA" }, + { "name": "无人机", "desc": "天空视角", "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eAUIeB2YMH14lGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWVA8LUFdZDE4IWipURmtCHHVEABsFVChPYRJAbVhlGRhbFRgLBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVdbC04VM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmgLHlgRXgIeVFpfDU0UH28OHF8UWAIDUlteAUwnAW4JH1IlbdiMwCQvcipMBxJhfV9HGgF3FVaDht8GfxgBGVkRTDZUCjsVbzBTYRJbeRkWGG9WAx02D0JcUy5mGRNRFEJiUwgldStzfx1wTj1GbQMyU1peOA" } + ] + }, + { + "id": "tiktok", + "icon": "🎵", + "title": "Tiktok", + "color": "from-fuchsia-500 to-pink-500", + "bg": "bg-fuchsia-50", + "text": "text-fuchsia-700", + "tools": [ + { "name": "Tiktok", "desc": "全球直播平台", "href": "https://www.tiktok.com/" }, + { "name": "云手机", "desc": "用开源硬件打造云手机", "href": "https://www.hackrobot.cn/" }, + { "name": "vcam虚拟摄像头", "desc": "xposed模块,替换摄像头视频流", "href": "https://pan.baidu.com/s/1NW4GMGA3cxfUQNWZPD9vOg?pwd=live" } + ] + }, + { + "id": "youtube", + "icon": "▶️", + "title": "Youtube", + "color": "from-red-500 to-rose-500", + "bg": "bg-red-50", + "text": "text-red-700", + "tools": [ + { "name": "Youtube", "desc": "全球最大的视频网站", "href": "https://www.youtube.com/" }, + { "name": "smmfollows", "desc": "刷粉刷量平台", "href": "https://smmfollows.com/ref/dfugq" }, + { "name": "YouTube Studio Auto Dismiss", "desc": "油猴脚本,无人直播辅助", "href": "https://greasyfork.org/zh-CN/scripts/557378-youtube-studio-auto-dismiss" } + ] + }, + { + "id": "web", + "icon": "🌍", + "title": "Web网站", + "color": "from-green-500 to-emerald-500", + "bg": "bg-green-50", + "text": "text-green-700", + "tools": [ + { "name": "namesilo", "desc": "注册域名网址", "href": "https://www.namesilo.com/?rid=3bd4047mc" }, + { "name": "aapanel", "desc": "宝塔面板", "href": "https://www.aapanel.com?referral_code=wcn04vez" }, + { "name": "cloudflare", "desc": "互联网基础节点", "href": "https://www.cloudflare.com/" }, + { "name": "Google Analytics", "desc": "网站流量统计服务", "href": "https://analytics.google.com/" }, + { "name": "Google AdSense", "desc": "Google广告服务", "href": "https://www.google.com/adsense/" } + ] + }, + { + "id": "live-stream", + "icon": "📺", + "title": "无人直播", + "color": "from-orange-500 to-amber-500", + "bg": "bg-orange-50", + "text": "text-orange-700", + "tools": [ + { "name": "OBS", "desc": "免费开源的电脑客户端:录屏/推流直播", "href": "https://obsproject.com/download" }, + { "name": "ffmpeg", "desc": "命令行视频处理/推流工具", "href": "https://www.ffmpeg.org/download.html" }, + { "name": "SRS", "desc": "开源直播服务器,拉流推流中转", "href": "https://ossrs.io/" } + ] + }, + { + "id": "payment", + "icon": "💲", + "title": "支付网关", + "color": "from-lime-500 to-green-500", + "bg": "bg-lime-50", + "text": "text-lime-700", + "tools": [ + { "name": "z-pay", "desc": "支持独角数卡-易支付", "href": "https://z-pay.cn/?uid=22119" }, + { "name": "xorpay", "desc": "支持收银台支付", "href": "https://xorpay.com?r=nomadro" } + ] + }, + { + "id": "forum", + "icon": "📝", + "title": "论坛社区", + "color": "from-sky-500 to-blue-500", + "bg": "bg-sky-50", + "text": "text-sky-700", + "tools": [ + { "name": "异度世界", "desc": "独立博客,hugo搭建", "href": "https://blog.hackrobot.cn" }, + { "name": "大师兄", "desc": "AI实践", "href": "https://dsx2016.com" } + ] + } + ] +} diff --git a/app/join/page.tsx b/app/join/page.tsx index 8f27597..66352b7 100644 --- a/app/join/page.tsx +++ b/app/join/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useRef, type FormEvent, type ChangeEvent } from "react"; +import { useState, useRef, useEffect, type FormEvent, type ChangeEvent } from "react"; +import { getPayEnv, getRecommendedChannel, mustUseWxPay, type PayChannel, type PayEnv } from "../lib/payEnv"; const genderOptions = ["男", "女"]; const singleOptions = ["单身", "恋爱中", "已婚"]; @@ -43,6 +44,22 @@ export default function JoinPage() { const [submitted, setSubmitted] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const [payRedirecting, setPayRedirecting] = useState(false); + const [payEnv, setPayEnv] = useState(null); + const [payChannel, setPayChannel] = useState("alipay"); + + useEffect(() => { + if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("paid") === "1") { + setSubmitted(true); + } + }, []); + + useEffect(() => { + if (typeof window === "undefined") return; + const env = getPayEnv(); + setPayEnv(env); + setPayChannel(getRecommendedChannel(env)); + }, []); const set = (key: keyof FormData, value: string) => setForm((prev) => ({ ...prev, [key]: value })); @@ -61,7 +78,41 @@ export default function JoinPage() { if (!res.ok || !data?.ok) { throw new Error(data?.error || "提交失败,请稍后重试"); } - setSubmitted(true); + const userId = data.user_id; + if (!userId) { + setSubmitted(true); + return; + } + setSubmitting(false); + setPayRedirecting(true); + const returnUrl = `${window.location.origin}/join?paid=1`; + const payForm = document.createElement("form"); + payForm.method = "POST"; + payForm.action = "/api/pay"; + payForm.target = "_self"; + payForm.style.display = "none"; + const env = getPayEnv(); + const channel = mustUseWxPay(env) ? "wxpay" : payChannel; + const device = env === "pc" ? "pc" : "h5"; // PC 跳转 payurl,H5/微信 用 payurl/payurl2 + const fields: [string, string][] = [ + ["user_id", userId], + ["total_fee", "80"], + ["type", "meetup"], + ["channel", channel], + ["device", device], + ["return_url", returnUrl], + ["html", "1"], + ]; + fields.forEach(([k, v]) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = k; + input.value = v; + payForm.appendChild(input); + }); + document.body.appendChild(payForm); + payForm.submit(); + return; } catch (err) { setError(err instanceof Error ? err.message : "网络错误,请重试"); } finally { @@ -395,6 +446,41 @@ export default function JoinPage() { )}
+ {/* ── 支付通道选择(PC/H5 可选,微信内强制微信支付)── */} +
+

💳 支付方式

+ {payEnv === "wechat" ? ( +

+ 当前在微信内打开,将使用 微信支付 +

+ ) : ( +
+ + +
+ )} +
+ {/* ── Submit ── */}
{error && ( @@ -404,15 +490,12 @@ export default function JoinPage() { )} -

- {"🔍 提交后将进行审核\n💰 需支付基本场地/管理费"} -

diff --git a/app/lib/env.ts b/app/lib/env.ts new file mode 100644 index 0000000..9c25072 --- /dev/null +++ b/app/lib/env.ts @@ -0,0 +1,19 @@ +/** + * 环境配置:区分本地调试与生产部署 + * 生产:https://api.hackrobot.cn(payjsapi) + */ + +const isDev = process.env.NODE_ENV === "development"; + +/** 支付 API 地址(payjsapi) */ +export const PAYMENT_API_URL = + process.env.PAYMENT_API_URL || + (isDev ? "http://127.0.0.1:8700" : "https://api.hackrobot.cn"); + +/** 站点根地址(用于 return_url 等回跳,生产需配置) */ +export const SITE_URL = + process.env.NEXT_PUBLIC_SITE_URL || + (isDev ? "http://localhost:3001" : "https://hackrobot.cn"); + +/** 是否为本地开发 */ +export const IS_DEV = isDev; diff --git a/app/lib/payEnv.ts b/app/lib/payEnv.ts new file mode 100644 index 0000000..98c76df --- /dev/null +++ b/app/lib/payEnv.ts @@ -0,0 +1,53 @@ +/** + * 支付环境检测:PC / H5 / 微信浏览器 + * 用于 ZPAY 等聚合支付,需区分环境以选择正确的支付通道 + * 参考:https://juejin.cn/post/7122720360683798542 + */ + +export type PayEnv = "pc" | "h5" | "wechat"; + +export type PayChannel = "alipay" | "wxpay"; + +/** 是否在微信内置浏览器 */ +export function isWeChat(): boolean { + if (typeof navigator === "undefined") return false; + return /MicroMessenger/i.test(navigator.userAgent); +} + +/** 是否在支付宝内置浏览器 */ +export function isAlipay(): boolean { + if (typeof navigator === "undefined") return false; + return /AlipayClient|Alipay/i.test(navigator.userAgent); +} + +/** 是否移动设备(含平板) */ +export function isMobile(): boolean { + if (typeof navigator === "undefined") return false; + return /AppleWebKit.*Mobile|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); +} + +/** 当前支付环境 */ +export function getPayEnv(): PayEnv { + if (isWeChat()) return "wechat"; + if (isMobile()) return "h5"; + return "pc"; +} + +/** + * 根据环境推荐/强制支付通道 + * - 微信浏览器:只能用 wxpay(支付宝在微信内无法唤起) + * - 支付宝浏览器:优先 alipay + * - PC/H5:用户可选 + */ +export function getRecommendedChannel(env: PayEnv): PayChannel { + if (env === "wechat") return "wxpay"; + if (env === "pc" || env === "h5") return "alipay"; // 默认支付宝 + return "alipay"; +} + +/** 微信内是否只能选微信支付 */ +export function mustUseWxPay(env: PayEnv): boolean { + return env === "wechat"; +}