公开商品页(顾客扫标签二维码 / 微信分享打开)走的是 Flutter Web 深链:无论从 /product/<id> 短链还是 /app/product/<id> 进来,后端 ProductPage 都返回注入了 OG 标签的 Flutter 应用壳 index.html,浏览器随后要下完整个管理端应用才渲染出一个商品卡。
| 环节 | 大小 | 实测耗时 | 问题 |
|---|---|---|---|
| HTML(含 OG 注入) | 2.1 KB | 0.05s | 正常 |
| 公开商品 API | 830 B | 0.05s | 正常 |
| main.dart.js | 5.3 MB | 11.8s | 未压缩 无 gzip、无缓存头 |
| canvaskit.wasm | 7.2 MB | 16.4s | 未压缩 |
三因叠加:① Flutter Web 首包 ~12.5MB;② nginx 全程无压缩(响应无 content-encoding);③ ECS 出网带宽固定 3Mbps(PayByBandwidth)→ 12.5MB ÷ 3Mbps ≈ 33 秒。重复访问可靠 etag 304,但每个第一次扫码的顾客都要全量拉。
| 方案 | 解决什么 | 改动面 | 预期效果 |
|---|---|---|---|
| A′ · 传输层压缩 + 缓存(瘦身版) | 员工 Web 版首开、API JSON 响应 | 仅 nginx conf ~10 行 | Web 版首开 12.5MB → ~4.4MB,约 33s → ~12s;API 列表响应 −70% |
| B · 公开页全面 SSR(去 Flutter) | 顾客扫码 / 分享动线与 App 彻底解耦 | backend 直出商品页 + 店铺列表页两张模板 | 公开页首开 ~20KB,< 0.5s,全程零 Flutter |
公开面盘点(B 的完整范围):App 中匿名可访问的页面只有两张——商品公开页(/product/:public_id)与店铺公开商品列表页(Flutter 路由 /shop/:shop_code,现仅能从商品页点门店名进入,无独立浏览器短链:直开 /shop/S001 落营销站 404)。两张全部 SSR 化并互相跳转后,顾客动线不再触碰任何 Flutter 资源。登录/注册属 App 本体入口(员工用)不在范围;官网营销站本就是静态站。
A 仍然值得做——员工浏览器开 Web 版、以及公开页上「在 App 中打开」的兜底路径都吃到红利。
# server 块(deploy/nginx-jiu-ali.conf):低频大文件现压(2vCPU 压 5MB 约 0.1s,可接受)
gzip on;
gzip_comp_level 5;
gzip_min_length 1k;
gzip_vary on;
gzip_proxied any;
gzip_types application/javascript application/wasm application/json
text/css text/plain image/svg+xml application/octet-stream;
图片不在列:JPEG/PNG 本身是压缩格式,gzip 无收益。图片链路现状已健康——上传时后端统一缩到最长边 1200px + JPEG q85(线上实测中位数 ~91KB/张),/images/ 已配 expires 30d + immutable,不动。
Flutter Web 产物文件名无内容 hash(main.dart.js 固定名),长缓存会在发版后出现新旧混用(main.dart.js 与 canvaskit 版本错配会白屏)。因此:
| 路径 | 策略 | 说明 |
|---|---|---|
| /app/index.html | no-store(现状不变) | 入口永远最新 |
| /app/ 其余全部 | Cache-Control: no-cache(协商) | 复访全部 etag 304,零字节;Flutter 自带 service worker 二次访问另有本地缓存 |
现 backend/internal/handler/public.go 的 ProductPage:读 Flutter index.html → 注入 OG 标签 → 返回应用壳。改造为:直接渲染一张自包含的轻量商品 HTML 页(Go html/template + go:embed 内嵌模板),不再依赖 Flutter 产物。
| 项 | 设计 |
|---|---|
| 模板位置 | backend/internal/handler/templates/product_page.html(go:embed,二进制自包含,部署不变) |
| 页面内容 | 主图(多图横滑,纯 CSS scroll-snap)· 品牌/名称(系列)· 规格 · 商品编号(?code= 参数回显,与现 App 公开页口径一致,实施时核对)· 产地/保质期/储存条件/介绍(基础数据字典字段,取自现有公开查询)· 门店名 + 联系电话 · 页脚「岩美酒库 · 正品溯源」 |
| 数据来源 | 复用 GetProduct(/api/v1/public/products/:id)已有的查询/字段组装逻辑,抽出共用函数——不新增查询口径,公开字段集与 API 完全一致(成本/进价等敏感字段本就不在其中) |
| OG 兼容 | OG meta(title/description/image/url)直接写进模板 head,复用现有 buildProductOG/buildOGTitle;微信/飞书分享卡片行为不变,且不再需要 UA 分流 |
| 样式 | 内联 CSS,全页 < 20KB(gzip 后 ~6KB);配色沿用设计系统主题 A 色值(品牌蓝 #2563AC 系),移动优先单列卡片 |
| 图片加载 | 全部异步拉取(2026-07-07 用户拍板):HTML 先到先渲染,图片不阻塞——首图 decoding="async" 优先加载,其余图 loading="lazy" 滚动到位才拉;店铺列表页同规则 |
| 兜底 | 商品不存在 → 渲染同风格「商品不存在或已下架」页(HTTP 200,现状对爬虫友好的行为保持) |
| 互跳 | 门店名可点 → /shop/<shop_code>(B4 店铺 SSR 页);页脚小链接「在 App 中查看」→ /app/product/<id>(员工/深链兜底,普通顾客不需要) |
现状:Flutter 内部路由 /shop/:shop_code(从商品公开页点门店名进入),功能 = 该店公开商品列表 + 关键字搜索 + 分页,数据源 ListShopProducts(/api/v1/public/shops/:shop_code/products)。SSR 化设计:
| 项 | 设计 |
|---|---|
| 新路由 | GET /shop/:shop_code(backend,router.go 公开区)——店铺公开页从此有独立浏览器短链,可印店铺码/分享 |
| 模板 | templates/shop_page.html(同 go:embed / 同风格):店铺名+logo · 搜索框(GET 表单 ?q=,服务端过滤)· 商品卡列表(图/品牌型号版本/编号,图片 loading="lazy" 原生懒加载——20 卡×~90KB 主图只按滚动加载)· 上一页/下一页链接(?page=N,沿用 API 的 page_size=20 上限)· 每张商品卡链接 /product/<public_id> |
| 数据来源 | 复用 ListShopProducts 的查询组装(与 API 同一函数抽共用),公开字段集一致,零新增口径 |
| OG | head 输出店铺级 OG(店名 + logo + 「共 N 件商品」),微信分享店铺链接出卡片 |
| 限流/反爬 | 路由挂与 shops API 同一套闸:shopListIP 分钟限流 + dqShopList 日配额(300/IP/日);nginx 新增 location ~ ^/shop/,进 jiu_shoplist 2r/s 专属 zone + $jiu_bad_ua_lib + limit_conn 10(与 shops API location 同参数,且同样须放在通用 location 之前) |
| 兜底 | 店铺不存在 / 未开公开页 → 同风格「店铺不存在」页;无商品 → 空态文案 |
| App 内导航 | Flutter /shop/:shop_code 内部路由保留不动(员工 App 内预览用,不发 HTTP) |
| 入口 | 现状 | 改造后 |
|---|---|---|
标签二维码 / App「复制公开链接」= /product/<id> | Flutter 壳(~30s) | 轻量页 <0.5s 历史已印标签直接受益,无需换码 |
浏览器直开 / 刷新 /app/product/<id> | nginx rewrite → 同一 ProductPage | 同上轻量页(nginx 该 location 不动) |
App 内浏览公开页(Flutter 路由 /product/:public_id、/shop/:shop_code) | 客户端内部导航,不发 HTTP | 不受影响 |
浏览器直开 /shop/<shop_code> | 404(落营销站,无此入口) | 店铺 SSR 页 新增短链,可印店铺码 |
| 商品公开页点门店名 | App 内跳 Flutter 店铺页 | SSR 商品页 → SSR 店铺页,全程零 Flutter |
| 微信/飞书爬虫抓分享卡片 | OG 注入壳 | OG 在轻量页 head,行为不变(店铺页新增店铺级 OG) |
公开 API /api/v1/public/products/:id、/api/v1/public/shops/:code/products | App 数据源 | 零改动 |
路由与闸全部沿用现状:publicReadIP 分钟限流 + dqProduct 日配额(1000/IP/日)+ nginx $jiu_bad_ua_lib/limit_conn 不动。轻量页比现在还省服务器资源(不再读盘 index.html,模板编译一次常驻)。
| 批次 | 内容 | tag |
|---|---|---|
| 唯一一批 | 方案 B(商品页 + 店铺页 SSR)+ 方案 A′(nginx gzip/缓存头,含新增 /shop location)——全部 server 流水线一次带出,client 零改动 | server-v1.1.8 |
curl -H 'Accept-Encoding: gzip' --compressed -so /dev/null -w '%{size_download}' https://jiu.51yanmei.com/app/main.dart.js → 响应带 content-encoding: gzip,下载量 ~1.3MB(原 5.3MB)/product/<id> 首开 < 0.5s(total time),页面含商品名/图/门店信息/product/<id> 链接 → 分享卡片(标题/图)正常/app/product/<id> 刷新落到轻量页/shop/<code> 直开出店铺列表页,搜索(?q=)与翻页(?page=)可用;商品卡点进 /product/<id>;超日配额出 429/product/<id> 保持不变,历史标签零作废。