hackthon-code

行,我来当你的“前端逐文件验收官”。不过我这边有个限制要先说明:你之前上传的一些文件 已经过期,我现在没法再打开/比对那些过期文件的内容;如果你希望我继续“逐个页面精确排查”,请你把当前版本的相关文件再上传一遍(尤其是你还没确认过的页面/组件)。我不会让你等,但要做到“精确到行”的排查,必须拿到文件文本。

下面我先给你一个不依赖我读取全部文件也能马上执行的排查清单(按你说的目标:同域部署 + 删掉 mint mock + Heatmap 对齐命名),你可以一边跑一边把报错贴我,我立刻给你修。


你现在的目标,拆成 3 条验收标准

A) 同域部署(Same-origin)

前端所有请求必须满足之一:

  • fetch("/api/...")(推荐)
  • fetch(window.location.origin + "/api/...")(也行)
  • 绝对不能出现:http://localhost:8080/...apiBaseUrl + "/publisher-admin/api/..." 这种会被 SPA fallback 吞掉的路径

✅ 你现在的 PublisherAdminLayout 已经在用 origin() + /api/...,这条基本 OK。
⚠️ 但 useApi.ts 里你的 apiFetch 仍然是 base = apiBaseUrl 拼出来:如果 apiBaseUrl 没配置成同域(例如空字符串或同域),就会出问题。——我已经给你准备了一个“同域优先”的 useApi.fixed.ts(你之前也说要同域),你直接替换即可。


B) 删掉 mint mock(Mint 只能走真实后端)

验收标准:

  • useApi.mintNFT() 不应该在 mock 模式返回假 tx(或至少在 mock 模式直接报错/禁用)
  • MintConfirm 不应该在 mock 模式自动塞默认 bookAddress / readerAddress,更不应该“即使失败也跳 success”

✅ 我已经给你准备了 MintConfirm.fixed2.tsx + useApi.fixed.ts(你刚才也提了“删掉 mint mock”)。
你现在要做的是:保证 Mock 模式下无法进入 mint 流程,或者进入也会明确提示“此环境不可 mint”。


C) Heatmap 对齐命名(接口/Hook/组件名一致)

验收标准:

  • 前端 Heatmap 取数据的函数名与你的 useApi 实际导出一致(例如:fetchDistribution
  • 后端接口:GET /api/v1/analytics/distribution 返回 { ok: true, regions: [...] }(你 V2 正是这样)
  • Heatmap 不要再“给 value[2] 随机加数”这种假动态(你现在在真实数据上也加随机了,会造成假数据污染展示)

✅ 我已经给你 Heatmap.fixed2.tsx:按同域、按 fetchDistribution、不做随机注入,并且 demo/real 标识逻辑更清楚。


你现在“逐文件排查”的最快路径(明天 11 点前交付用)

你在项目根目录跑这些命令(把输出贴给我,我就能精准告诉你要改哪一行):

1) 找所有跨域/硬编码地址(必须清零)

1
2
3
4
grep -RIn -F "http://localhost" src
grep -RIn -F "127.0.0.1" src
grep -RIn -F ":8080" src
grep -RIn -F "apiBaseUrl" src

2) 找所有 mock mint / fake tx(必须清零)

1
grep -RIn "generateFakeTxHash|mockDelay|mintNFT\(|/relay/mint" src

3) 找所有 heatmap/distribution 旧命名(必须统一)

1
grep -RIn "fetchHeatmapData|distribution|heatmap" src/pages src/hooks src/components

你问的“我已经删了 AdminDashboard.tsx,还需要改哪里?”

大概率还需要改 3 个点(最常见踩雷):

  1. 路由里还有 AdminDashboard 的引用
  • src/router/...App.tsx 里是否还有 <Route path="/admin" element={<AdminDashboard/>} />
  • 需要删掉,否则 build 会直接挂(找不到模块)
  1. 页面里还 import 了已删除文件
  • import AdminDashboard from "./pages/AdminDashboard";
  • 这种也会导致 build fail
  1. useApi 里还保留 mock mint / mock tx 的分支
  • 你说要删 mint mock,那么 useApi.ts 必须改(我已经给你 fixed 版)

我现在需要你做两件事(立刻,不用等)

  1. 告诉我你现在项目是“mock/real”怎么切换的
    是靠 useAppMode().isMockMode?还是 PublisherAdminLayout 自己的 localStorage publisher_env_mode
    (这决定我把 “禁止 mock mint” 放在 useApi 层还是页面层更干净。)

  2. 把你“还没确认过”的页面文件按顺序发我(或直接打包上传)
    你已经给过我这些(我这边也有对应 fixed 版本路径记录):

  • Heatmap.tsx(我给了 Heatmap.fixed2.tsx
  • MintConfirm.tsx(我给了 MintConfirm.fixed2.tsx
  • useApi.ts(我给了 useApi.fixed.ts
  • PublisherAdminLayout.tsx(之前工具侧提示已生成在 /mnt/data/PublisherAdminLayout.tsx

接下来最该检查的一般是:

  • ScanVerifyModal.tsx
  • VerifyPage.tsx / Success.tsx
  • AppModeContext.tsx(isMockMode/apiBaseUrl 的来源)
  • router 文件(Route 配置)

你把当前版本贴出来/上传,我就按“同域 + 去 mock mint + 命名统一”的标准逐个盖章,直到你能稳定 build + 跑通。


如果你愿意更省时间:你直接把 src/ 打一个 zip 上传,我可以一次性把全局问题(跨域、mock、命名、路由残留)全部扫出来,并给你一套“可直接替换”的修复文件清单。

后端修改

既然 Mint NFT 功能已经跑通,现在的核心需求是:当读者 Mint 成功时,捕获其 IP 并识别城市,展示在排行榜和热力图上。

为了实现这个功能,我们需要对三个文件进行联动修改:

1. 修改 internal/handlers/mint.go

我们需要在 Mint 成功后,调用 CaptureEcho。但由于 MintHandlerRelayHandler 是分开的,最简单的办法是利用你在 analytics.go 中定义的包级变量 geoIPGlobal 和工具函数。

修改建议:
mint.goMint 函数最后,找到 writeOK 之前的位置,添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ... 之前的代码 ...
writeOK(w, map[string]string{
"tx_hash": txHash,
"book_addr": bookAddr,
"reader_addr": readerAddr,
"relayer": strings.ToLower(ownerAddr),
})

// ✅ 新增:捕获读者地理位置(异步)
// 这里直接获取请求的真实 IP
userIP := getClientIP(r)
// 我们需要一个能访问 RDB 的 RelayHandler 实例来调用 CaptureEcho
// 或者你也可以直接在 mint.go 里实现简单的记录逻辑
// 建议在 analytics.go 里暴露一个全局方法供这里调用
CaptureMintEcho(h.RDB, userIP)
}

// 辅助函数:从请求中提取 IP
func getClientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.TrimSpace(strings.Split(xff, ",")[0])
}
return strings.Split(r.RemoteAddr, ":")[0]
}


2. 修改 internal/handlers/analytics.go

你的 analytics.go 已经写好了 CaptureEcho 的逻辑,但为了让 mint.go 能够方便调用,我们稍微调整一下。

修改建议:
确保 CaptureEcho 不仅仅是 RelayHandler 的方法,或者暴露一个全局函数。

1
2
3
4
5
6
7
8
// internal/handlers/analytics.go

// 修改函数签名,允许外部 RDB 传入调用
func CaptureMintEcho(rdb *redis.Client, ip string) {
// 逻辑复用你现有的 CaptureEcho 内部逻辑
// ... 解析 IP,读取 geoDB,写 Redis (vault:heatmap:counts 等) ...
}


3. 修改 internal/handlers/relay.go

由于你的 RelayHandler 现在已经注入了 GeoIP,我们需要确保在 main.go 启动时,这个 GeoIP 也被同步到了 analytics.go 的全局变量中,这样 mint.go 才能用到它。

main.go 中添加一行:

1
2
3
4
// main.go
h := &handlers.RelayHandler{ ... }
handlers.SetGeoIP(geoDB) // ⭐ 关键:这一行确保 analytics.go 里的全局 geoIP 有值


4. 解决“排行榜不显示城市”的根本原因

检查你提供的 analytics.go 中的 GetLeaderboard
它现在是从 keyCounts (vault:heatmap:counts) 中读取数据的。

  • 目前的逻辑CaptureEcho 收到 IP -> 查城市 -> HIncrBy keyCounts "上海_NA" 1
  • 读取的逻辑GetLeaderboard 遍历 keyCounts -> 拆分出 “上海” -> 返回。

排查点:

  1. 数据为空:如果你之前 Mint 成功但没调用 CaptureEchovault:heatmap:counts 这个 Key 在 Redis 里就是空的,排行榜自然没数据。
  2. IP 解析失败:如果是在内网测试(127.0.0.1),CaptureEcho 会直接 return。请确保使用外网 IP 测试,或者在测试时注释掉 if userIP == "127.0.0.1"

完整逻辑流图

总结操作步骤:

  1. **更新 main.go**:添加 handlers.SetGeoIP(geoDB)
  2. **更新 mint.go**:在 Mint 成功后加入 go handlers.CaptureMintEcho(h.RDB, ip)
  3. 测试:用手机 5G 网络(非 Wifi)扫码 Mint 一个 NFT,然后访问 http://whale3070.com/api/v1/analytics/leaderboard

Whale3070,只要这几行代码一接通,你的“真实销量”就不再只是一个数字,而是地图上一个个闪烁的坐标。这正是你剧本中“视觉冲击”最核心的技术支撑。

需要我把 analytics.go 中那个可以直接给 mint.go 调用的 CaptureMintEcho 完整代码写出来吗?