diff --git a/README.md b/README.md index 1764941..739525c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ HTTP 和 TCP 调同一套 `internal/module/*/logic`。Gin handler 只做绑定 | 用途 | 域名 | 说明 | | --- | --- | --- | +| 官网 | `https://sparkmessenger.wyrs365.com` | App 落地页(别名 `sparkmsg.wyrs365.com`) | | HTTP API | `https://sparkapi.wyrs365.com` | REST `/v1/...`,文档 `/docs` | | IM 长连接 | `sparkim.wyrs365.com:443` | TLS + 自定义 Frame,不是 WebSocket | | 对象存储 | `https://sparkr.wyrs365.com` | RustFS S3 path-style,Presigned PUT/GET | diff --git a/deploy/nginx/README.md b/deploy/nginx/README.md index 45ec3f1..de88fa9 100644 --- a/deploy/nginx/README.md +++ b/deploy/nginx/README.md @@ -8,6 +8,7 @@ Client --TLS--> :443 ssl_preread │ → proxy_pass http://127.0.0.1:18080 + X-Real-IP ├─ SNI sparkr.wyrs365.com → 127.0.0.1:8443 (HTTP TLS) │ → proxy_pass http://127.0.0.1:9000 RustFS S3 + ├─ SNI sparkmessenger / sparkmsg → 127.0.0.1:8443 (静态官网) └─ SNI sparkim.wyrs365.com → 127.0.0.1:19001 (IM TLS) → PROXY protocol → 127.0.0.1:9090 gnet ``` @@ -16,8 +17,15 @@ On this ECS, `:443` is shared with other wyrs HTTPS sites. Do **not** replace `/ - `host/sparkapi.conf` → `/etc/nginx/conf.d/` - `host/sparkr.conf` → `/etc/nginx/conf.d/` (RustFS S3; DNS A record `sparkr.wyrs365.com` → this host) +- `host/sparkmessenger.conf` → `/etc/nginx/conf.d/`(官网;`sparkmessenger.wyrs365.com` / `sparkmsg.wyrs365.com`) - `host/spark-sni.conf` → `/etc/nginx/stream.d/` +官网静态文件在 `/var/www/sparkmessenger.wyrs365.com`,部署: + +```bash +bash deploy/website/deploy.sh +``` + Existing `listen 443 ssl` vhosts move to `127.0.0.1:8443 ssl proxy_protocol` so SNI can take public 443. `00-spark-http.conf` sets `real_ip_header proxy_protocol` and `port_in_redirect off` so `$remote_addr` is the client and redirects stay on 443. spark-server (Docker): diff --git a/deploy/nginx/host/sparkmessenger.conf b/deploy/nginx/host/sparkmessenger.conf new file mode 100644 index 0000000..be3dba3 --- /dev/null +++ b/deploy/nginx/host/sparkmessenger.conf @@ -0,0 +1,53 @@ +# Spark Messenger website — sparkmessenger.wyrs365.com / sparkmsg.wyrs365.com +# Public :443 is stream ssl_preread (see stream.d/spark-sni.conf). +# TLS terminates on loopback :8443 (PROXY protocol from stream). + +server { + listen 80; + listen [::]:80; + server_name sparkmessenger.wyrs365.com sparkmsg.wyrs365.com; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 127.0.0.1:8443 ssl proxy_protocol; + listen [::1]:8443 ssl proxy_protocol; + http2 on; + server_name sparkmessenger.wyrs365.com sparkmsg.wyrs365.com; + + ssl_certificate /etc/letsencrypt/live/sparkmessenger.wyrs365.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/sparkmessenger.wyrs365.com/privkey.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:SparkSSL:10m; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + root /var/www/sparkmessenger.wyrs365.com; + index index.html; + + location = /favicon.ico { + try_files /assets/favicon.svg =404; + default_type image/svg+xml; + access_log off; + expires 7d; + } + + location /assets/ { + expires 7d; + add_header Cache-Control "public"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/deploy/nginx/install-host.sh b/deploy/nginx/install-host.sh index 3fa2732..14c170f 100755 --- a/deploy/nginx/install-host.sh +++ b/deploy/nginx/install-host.sh @@ -58,8 +58,8 @@ rewrite_http_443() { local f for f in /etc/nginx/conf.d/*.conf; do case "$(basename "$f")" in - sparkapi.conf|sparkr.conf|spark-acme.conf|00-spark-http.conf) continue ;; - spark-acme-*) continue ;; + sparkapi.conf|sparkr.conf|sparkmessenger.conf|spark-acme.conf|00-spark-http.conf) continue ;; + spark-acme-*|sparkmessenger-acme.conf) continue ;; esac if grep -qE 'listen[[:space:]]+127\.0\.0\.1:8443' "$f"; then continue @@ -77,9 +77,13 @@ install_sni() { rewrite_http_443 rm -f /etc/nginx/conf.d/spark-acme.conf rm -f /etc/nginx/conf.d/spark-acme-*.conf + rm -f /etc/nginx/conf.d/sparkmessenger-acme.conf cp "$HOST_DIR/00-spark-http.conf" /etc/nginx/conf.d/00-spark-http.conf cp "$HOST_DIR/sparkapi.conf" /etc/nginx/conf.d/sparkapi.conf cp "$HOST_DIR/sparkr.conf" /etc/nginx/conf.d/sparkr.conf + if [[ -f "$HOST_DIR/sparkmessenger.conf" ]] && cert_ok sparkmessenger.wyrs365.com; then + cp "$HOST_DIR/sparkmessenger.conf" /etc/nginx/conf.d/sparkmessenger.conf + fi cp "$HOST_DIR/spark-sni.conf" /etc/nginx/stream.d/spark-sni.conf mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'HOOK' diff --git a/deploy/website/deploy.sh b/deploy/website/deploy.sh new file mode 100644 index 0000000..c05e2d4 --- /dev/null +++ b/deploy/website/deploy.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Deploy Spark Messenger website to the Singapore ECS host. +# Layout on host: +# /var/www/sparkmessenger.wyrs365.com static site +# /etc/nginx/conf.d/sparkmessenger.conf TLS vhost (loopback :8443) +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +HOST="${SPARK_SSH_HOST:-root@47.236.66.9}" +KEY="${SPARK_SSH_KEY:-$HOME/.ssh/wyrs_shanghai_deploy.pem}" +SITE_SRC="$ROOT/website" +SITE_DST="/var/www/sparkmessenger.wyrs365.com" +DOMAINS=(sparkmessenger.wyrs365.com sparkmsg.wyrs365.com) +PRIMARY="${DOMAINS[0]}" +SSH=(ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$HOST") +SCP=(scp -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new) + +need() { + command -v "$1" >/dev/null || { + echo "need $1 on this machine" >&2 + exit 1 + } +} + +need ssh +need scp +need rsync + +if [[ ! -f "$KEY" ]]; then + echo "missing SSH key: $KEY" >&2 + exit 1 +fi + +if [[ ! -f "$SITE_SRC/index.html" ]]; then + echo "missing $SITE_SRC/index.html" >&2 + exit 1 +fi + +echo "==> upload site + nginx conf" +"${SSH[@]}" "mkdir -p '$SITE_DST' /var/www/certbot /tmp/spark-nginx-host" +rsync -az --delete -e "ssh -i $KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new" \ + "$SITE_SRC/" "$HOST:$SITE_DST/" +"${SCP[@]}" "$ROOT/deploy/nginx/host/sparkmessenger.conf" "$HOST:/tmp/spark-nginx-host/sparkmessenger.conf" + +echo "==> ensure TLS certificates" +"${SSH[@]}" bash -s -- "$PRIMARY" "${DOMAINS[@]}" <<'REMOTE' +set -euo pipefail +PRIMARY=$1 +shift +DOMAINS=("$@") + +cert_ok() { + [[ -f "/etc/letsencrypt/live/$1/fullchain.pem" ]] +} + +need_issue=0 +if ! cert_ok "$PRIMARY"; then + need_issue=1 +else + # Ensure SAN covers all website domains; re-issue if missing. + for d in "${DOMAINS[@]}"; do + if ! openssl x509 -in "/etc/letsencrypt/live/$PRIMARY/fullchain.pem" -noout -text 2>/dev/null | grep -q "$d"; then + need_issue=1 + break + fi + done +fi + +if [[ "$need_issue" -eq 1 ]]; then + mkdir -p /var/www/certbot + cat > /etc/nginx/conf.d/sparkmessenger-acme.conf <&2 + exit 1 +fi + +cp /tmp/spark-nginx-host/sparkmessenger.conf /etc/nginx/conf.d/sparkmessenger.conf +nginx -t +systemctl reload nginx +echo "website nginx ready" +REMOTE + +echo "==> verify" +for d in "${DOMAINS[@]}"; do + code=$("${SSH[@]}" "curl -fsS -o /dev/null -w '%{http_code}' --resolve ${d}:443:127.0.0.1 https://${d}/" || true) + echo "$d -> HTTP $code" +done + +echo "done" diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..107caf4 --- /dev/null +++ b/website/README.md @@ -0,0 +1,18 @@ +# Spark Messenger 官网 + +静态落地页,部署到新加坡 ECS(`47.236.66.9`)。 + +| 域名 | 说明 | +| --- | --- | +| `https://sparkmessenger.wyrs365.com` | 主站 | +| `https://sparkmsg.wyrs365.com` | 短域名,同一站点 | + +主机路径:`/var/www/sparkmessenger.wyrs365.com` +Nginx:`deploy/nginx/host/sparkmessenger.conf` → `/etc/nginx/conf.d/` + +```bash +# 默认 KEY=$HOME/.ssh/wyrs_shanghai_deploy.pem HOST=root@47.236.66.9 +bash deploy/website/deploy.sh +``` + +会签发 / 续期 Let’s Encrypt 证书(SAN 含两个域名),rsync 静态文件并 reload nginx。 diff --git a/website/assets/app.js b/website/assets/app.js new file mode 100644 index 0000000..1dfb0c2 --- /dev/null +++ b/website/assets/app.js @@ -0,0 +1,14 @@ +(() => { + const form = document.querySelector(".notify"); + const note = document.getElementById("notify-note"); + if (!form || !note) return; + + form.addEventListener("submit", (event) => { + event.preventDefault(); + const email = form.querySelector("input[type='email']"); + if (!(email instanceof HTMLInputElement) || !email.value.trim()) return; + note.hidden = false; + email.value = ""; + email.blur(); + }); +})(); diff --git a/website/assets/favicon.svg b/website/assets/favicon.svg new file mode 100644 index 0000000..5e91361 --- /dev/null +++ b/website/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/website/assets/styles.css b/website/assets/styles.css new file mode 100644 index 0000000..05b6d5a --- /dev/null +++ b/website/assets/styles.css @@ -0,0 +1,555 @@ +:root { + --ink: #1a221c; + --ink-soft: #3d4a40; + --mist: #f3efe6; + --paper: #faf7f0; + --ember: #e35a1c; + --ember-deep: #b84312; + --leaf: #2f6b4f; + --line: rgba(26, 34, 28, 0.12); + --shadow: 0 28px 60px rgba(26, 34, 28, 0.14); + --font-display: "Bricolage Grotesque", "Segoe UI", sans-serif; + --font-body: "Source Serif 4", Georgia, serif; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--ink); + font-family: var(--font-body); + font-size: 1.125rem; + line-height: 1.55; + background: + radial-gradient(1200px 700px at 12% -10%, #ffe2c4 0%, transparent 55%), + radial-gradient(900px 600px at 100% 8%, #cfe8d7 0%, transparent 50%), + linear-gradient(180deg, #f7f2e8 0%, var(--mist) 45%, #e8efe8 100%); + overflow-x: hidden; +} + +.grain { + pointer-events: none; + position: fixed; + inset: 0; + z-index: 50; + opacity: 0.035; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} + +a { + color: inherit; + text-decoration-thickness: 1px; + text-underline-offset: 0.2em; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.top { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + max-width: 1120px; + margin: 0 auto; + padding: 1.25rem 1.5rem 0; +} + +.mark { + display: inline-flex; + align-items: center; + gap: 0.55rem; + font-family: var(--font-display); + font-weight: 700; + font-size: 1rem; + letter-spacing: -0.02em; + text-decoration: none; +} + +.mark-icon { + width: 1.35rem; + height: 1.35rem; + fill: var(--ember); +} + +.top-nav { + display: flex; + gap: 1.25rem; + font-family: var(--font-display); + font-size: 0.95rem; + font-weight: 500; +} + +.top-nav a { + text-decoration: none; + opacity: 0.78; +} + +.top-nav a:hover { + opacity: 1; +} + +.hero { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); + align-items: center; + gap: 2rem 3rem; + max-width: 1120px; + min-height: calc(100vh - 4.5rem); + margin: 0 auto; + padding: 2.5rem 1.5rem 3.5rem; +} + +.hero-copy { + max-width: 34rem; +} + +.eyebrow { + margin: 0 0 0.75rem; + font-family: var(--font-display); + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--leaf); +} + +.hero h1 { + margin: 0; + font-family: var(--font-display); + font-weight: 800; + font-size: clamp(2.8rem, 7vw, 5.2rem); + line-height: 0.95; + letter-spacing: -0.045em; +} + +.lede { + margin: 1.25rem 0 0; + max-width: 28rem; + font-size: clamp(1.1rem, 2vw, 1.35rem); + color: var(--ink-soft); +} + +.cta { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.75rem; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.85rem; + padding: 0.7rem 1.25rem; + border-radius: 0.55rem; + border: 1px solid transparent; + font-family: var(--font-display); + font-size: 0.98rem; + font-weight: 600; + letter-spacing: -0.01em; + text-decoration: none; + cursor: pointer; + transition: transform 160ms ease, background-color 160ms ease, border-color 160ms ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + background: var(--ember); + color: #fffaf4; +} + +.btn-primary:hover { + background: var(--ember-deep); +} + +.btn-ghost { + background: transparent; + border-color: var(--line); + color: var(--ink); +} + +.btn-ghost:hover { + border-color: rgba(26, 34, 28, 0.35); +} + +.hero-stage { + position: relative; + min-height: 28rem; + display: grid; + place-items: center; +} + +.orb { + position: absolute; + border-radius: 50%; + filter: blur(2px); + animation: drift 9s ease-in-out infinite alternate; +} + +.orb-a { + width: 18rem; + height: 18rem; + background: radial-gradient(circle at 30% 30%, #ffb07a, #e35a1c66 55%, transparent 70%); + top: 8%; + right: 8%; +} + +.orb-b { + width: 14rem; + height: 14rem; + background: radial-gradient(circle at 40% 40%, #8fd0ad, #2f6b4f55 60%, transparent 72%); + bottom: 10%; + left: 4%; + animation-delay: -3s; +} + +.device { + position: relative; + width: min(100%, 18.5rem); + aspect-ratio: 9 / 17.5; + border-radius: 2rem; + padding: 0.7rem; + background: linear-gradient(160deg, #2a332c, #151a17); + box-shadow: var(--shadow); + transform: rotate(3deg); + animation: floaty 6.5s ease-in-out infinite; +} + +.device-bezel { + height: 100%; + border-radius: 1.45rem; + overflow: hidden; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.08), transparent 30%), + linear-gradient(180deg, #203028, #101512); + padding: 1.4rem 0.95rem 1rem; +} + +.chat { + display: flex; + flex-direction: column; + gap: 0.7rem; + height: 100%; +} + +.bubble { + max-width: 88%; + padding: 0.7rem 0.85rem; + border-radius: 1rem; + font-family: var(--font-display); + font-size: 0.92rem; + font-weight: 500; + line-height: 1.35; +} + +.bubble.in { + align-self: flex-start; + background: #eef4ef; + color: var(--ink); + border-bottom-left-radius: 0.35rem; +} + +.bubble.out { + align-self: flex-end; + background: var(--ember); + color: #fff8f1; + border-bottom-right-radius: 0.35rem; +} + +.typing { + align-self: flex-start; + display: inline-flex; + gap: 0.28rem; + padding: 0.75rem 0.9rem; + border-radius: 1rem; + background: #eef4ef; +} + +.typing span { + width: 0.38rem; + height: 0.38rem; + border-radius: 50%; + background: #6d7a70; + animation: blink 1.2s infinite ease-in-out; +} + +.typing span:nth-child(2) { + animation-delay: 0.15s; +} + +.typing span:nth-child(3) { + animation-delay: 0.3s; +} + +.rise { + opacity: 0; + transform: translateY(12px); + animation: rise-in 700ms cubic-bezier(0.22, 1, 0.36, 1) forwards; + animation-delay: var(--d, 0s); +} + +.features { + position: relative; + z-index: 1; + max-width: 1120px; + margin: 0 auto; + padding: 1rem 1.5rem 5rem; +} + +.features-intro { + max-width: 36rem; + margin-bottom: 2.5rem; +} + +.features h2, +.download h2 { + margin: 0; + font-family: var(--font-display); + font-size: clamp(1.9rem, 4vw, 2.8rem); + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1.1; +} + +.features-intro p, +.download > p { + margin: 0.9rem 0 0; + color: var(--ink-soft); + max-width: 34rem; +} + +.feature-rail { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0; + border-top: 1px solid var(--line); +} + +.feature-rail li { + padding: 1.6rem 1.4rem 0.4rem 0; + border-bottom: 1px solid var(--line); +} + +.feature-rail li + li { + padding-left: 1.4rem; + border-left: 1px solid var(--line); +} + +.feature-rail h3 { + margin: 0 0 0.55rem; + font-family: var(--font-display); + font-size: 1.15rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.feature-rail p { + margin: 0; + color: var(--ink-soft); + font-size: 1.02rem; +} + +.download { + position: relative; + z-index: 1; + max-width: 1120px; + margin: 0 auto 2rem; + padding: 3rem 1.5rem 4rem; + border-top: 1px solid var(--line); +} + +.notify { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + margin-top: 1.5rem; + max-width: 32rem; +} + +.notify input { + flex: 1 1 14rem; + min-height: 2.85rem; + padding: 0.7rem 0.95rem; + border: 1px solid var(--line); + border-radius: 0.55rem; + background: rgba(255, 255, 255, 0.55); + color: var(--ink); + font: inherit; +} + +.notify input:focus { + outline: 2px solid rgba(227, 90, 28, 0.35); + outline-offset: 1px; +} + +.notify .btn { + border: none; +} + +.notify-note { + margin: 0.85rem 0 0; + color: var(--leaf); + font-family: var(--font-display); + font-size: 0.95rem; + font-weight: 600; +} + +.store-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.store-chip { + font-family: var(--font-display); + font-size: 0.88rem; + font-weight: 600; + color: var(--ink-soft); + border-bottom: 1px solid var(--line); + padding-bottom: 0.15rem; +} + +.foot { + position: relative; + z-index: 1; + max-width: 1120px; + margin: 0 auto; + padding: 0 1.5rem 2.5rem; + color: var(--ink-soft); + font-size: 0.95rem; +} + +.foot p { + margin: 0.35rem 0; +} + +.foot-links { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + font-family: var(--font-display); + font-size: 0.85rem; +} + +.foot a { + text-decoration: none; + opacity: 0.85; +} + +.foot a:hover { + opacity: 1; +} + +@keyframes drift { + from { + transform: translate3d(0, 0, 0) scale(1); + } + to { + transform: translate3d(-12px, 16px, 0) scale(1.05); + } +} + +@keyframes floaty { + 0%, + 100% { + transform: rotate(3deg) translateY(0); + } + 50% { + transform: rotate(2deg) translateY(-10px); + } +} + +@keyframes rise-in { + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes blink { + 0%, + 80%, + 100% { + opacity: 0.35; + transform: translateY(0); + } + 40% { + opacity: 1; + transform: translateY(-2px); + } +} + +@media (max-width: 900px) { + .hero { + grid-template-columns: 1fr; + min-height: auto; + padding-top: 1.75rem; + } + + .hero-stage { + order: -1; + min-height: 22rem; + } + + .device { + width: min(100%, 15.5rem); + transform: rotate(0deg); + } + + .feature-rail { + grid-template-columns: 1fr; + } + + .feature-rail li + li { + padding-left: 0; + border-left: none; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + .orb, + .device, + .typing span, + .rise { + animation: none !important; + } + + .rise { + opacity: 1; + transform: none; + } +} diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..33f2429 --- /dev/null +++ b/website/index.html @@ -0,0 +1,107 @@ + + + + + + Spark Messenger — 即时沟通,火花相连 + + + + + + + + + + + +
+ + + Spark Messenger + + +
+ +
+
+
+

即时通讯

+

Spark Messenger

+

把对话、群聊与朋友圈收进同一束火花——安静、可靠、随处可达。

+ +
+ + +
+ +
+
+

为真实对话而造

+

少一层噪音,多一点连接。消息直达,媒体直传,跨设备保持同一条时间线。

+
+
    +
  • +

    一对一与群聊

    +

    长连接实时推送,撤回与删除按会话清晰落地。

    +
  • +
  • +

    朋友圈

    +

    图片与短文按可见范围发布,不挤占会话列表。

    +
  • +
  • +

    媒体直传

    +

    对象存储签名上传,聊天里只留引用,加载更快。

    +
  • +
+
+ +
+

准备好了就装上

+

Android 与 iOS 客户端陆续开放。留下邮箱,我们会在上架时通知你。

+
+ + + +
+ +
+ App Store · 即将推出 + Google Play · 即将推出 +
+
+
+ + + + + +