whale-valut-三方角色网关 (Tri-Role Gateway)

这个 map.go 文件已经具备了基础的逻辑架构,但要完全实现你描述的“三方角色分发(作者/出版社/读者)”,还需要对 verifyHandler 进行关键性的重构。

目前代码中仅判断了 ADMIN_ADDRESS(出版社)和 codeHash(读者),并没有接入 Redis 来动态识别作者地址

1. 修改建议

为了符合你的要求,我们需要在 Redis 中维护两个集合(Set):

  • vault:roles:publishers:存储出版社钱包地址。
  • vault:roles:authors:存储作者钱包地址。

2. 代码修改方案

请重点修改 map.go 中的 verifyHandler 函数,参考以下逻辑:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func verifyHandler(w http.ResponseWriter, r *http.Request) {
// 获取前端传来的地址和 HashCode
address := strings.ToLower(r.URL.Query().Get("address"))
codeHash := r.URL.Query().Get("codeHash")

if address == "" {
sendJSON(w, http.StatusBadRequest, CommonResponse{Error: "address is required"})
return
}

// 1. 检测是否为 出版社 (从 Redis 集合中判断)
isPublisher, _ := rdb.SIsMember(ctx, "vault:roles:publishers", address).Result()
if isPublisher {
sendJSON(w, http.StatusOK, CommonResponse{
Ok: true, Status: "PUBLISHER_DASHBOARD", Role: "publisher",
})
return
}

// 2. 检测是否为 作者 (从 Redis 集合中判断)
isAuthor, _ := rdb.SIsMember(ctx, "vault:roles:authors", address).Result()
if isAuthor {
sendJSON(w, http.StatusOK, CommonResponse{
Ok: true, Status: "AUTHOR_DASHBOARD", Role: "author",
})
return
}

// 3. 既不是作者也不是出版社,则检测是否为 读者 (验证 HashCode)
if codeHash != "" {
isValid, _ := rdb.SIsMember(ctx, "vault:codes:valid", codeHash).Result()
if isValid {
sendJSON(w, http.StatusOK, CommonResponse{
Ok: true, Status: "VALID_READER", Role: "reader",
})
return
}
}

// 4. 没有任何权限
sendJSON(w, http.StatusForbidden, CommonResponse{
Ok: false, Error: "GUEST_ACCESS_DENIED", Role: "guest",
})
}


3. 需要配合的 Redis 预存操作

在你的数据库初始化脚本或者通过 redis-cli 中,你需要预先存入数据,程序才能检测到:

1
2
3
4
5
6
7
8
9
# 存入出版社地址
SADD vault:roles:publishers 0xYourPublisherAddress1
SADD vault:roles:publishers 0x9f5CbD02b1794B7a64749e6ff21715d03EA70834

# 存入作者地址
SADD vault:roles:authors 0xYourAuthorAddress1
SADD vault:roles:authors 0xYourAuthorAddress2
SADD vault:roles:authors 0x680c3EbB20ffBb142712980c692584DBd3E59a36

└─# cast wallet new
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. To mute this warning set FOUNDRY_DISABLE_NIGHTLY_WARNING in your environment.

Successfully created new keypair.
Address: 0x9f5CbD02b1794B7a64749e6ff21715d03EA70834 【出版社地址】
Private key: 0x3b71738de38f212833f98727e28a357b0a688e8d784bf48fb76538fb55cd048e

Address: 0x680c3EbB20ffBb142712980c692584DBd3E59a36 【作者地址】
Private key: 0xa221850fc1af2fe678a7015d35c8b66db24a6db9acb0bf7a26448d0dcf3b4a97
(别白费力气了,这些钱包一毛钱也没有,未来也不会有)


4. 总结你的 map.go 现状

  • 符合要求的地方:已经引入了 Redis 和以太坊客户端,基础架构非常扎实,具备 IP 地理位置分析能力(这对于出版社监控销量分布非常有用)。
  • 需要改进的地方
  • 硬编码问题:原代码里出版社地址是读取环境变量 ADMIN_ADDRESS,建议改为从 Redis 读取,这样你以后增加多个出版社就不需要重启后端。
  • 缺少作者判定:原代码完全没有作者角色的判定逻辑。
  • 权限分发逻辑:目前的 Role 返回值需要前端配合 window.location.href 实现跳转。

下一个改进建议

你的 distributionHandler 正在通过 IP 获取地理位置。既然你现在有了“作者”和“出版社”后台,是否需要为出版社后台增加一个“防伪分布预警”? 例如:如果某个 Hash Code 在短时间内被多个不同国家的 IP 尝试验证,我们可以将其标记为“风险扫码”。你想让我帮你加上这个逻辑吗?

出版社登录后才显示热力图

要在前端实现“出版社登录后才显示热力图”的功能,你需要对项目的路由(通常在 App.tsx)和登录组件进行调整。

以下是具体的修改方案:

1. 修改 App.tsx (或你的路由配置文件)

你需要定义不同的路由。不要让热力图 (Heatmap.tsx) 作为默认的读者页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/App.tsx 示例
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './pages/Login';
import Heatmap from './pages/Heatmap'; // 你的热力图文件
import AuthorDashboard from './pages/AuthorDashboard';
import MintPage from './pages/MintPage';

function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Login />} />
{/* 出版社后台,包含热力图 */}
<Route path="/publisher/dashboard" element={<Heatmap />} />
{/* 作者后台 */}
<Route path="/author/dashboard" element={<AuthorDashboard />} />
{/* 普通读者领取代币页面 */}
<Route path="/mint" element={<MintPage />} />
</Routes>
</Router>
);
}


2. 修改登录逻辑 (处理后端返回的 Role)

在你的登录页面(假设是 Login.tsx),当连接钱包并调用 /secret/verify 后,根据后端返回的 role 字段进行跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const handleLogin = async (address: string, codeHash: string) => {
try {
const response = await fetch(`http://localhost:8080/secret/verify?address=${address}&codeHash=${codeHash}`);
const data = await response.json();

if (data.ok) {
// 关键跳转逻辑
if (data.role === 'publisher') {
navigate('/publisher/dashboard'); // 跳转到热力图页面
} else if (data.role === 'author') {
navigate('/author/dashboard');
} else if (data.role === 'reader') {
navigate('/mint'); // 读者去领 NFT,看不到地图
}
} else {
alert("验证失败: " + data.error);
}
} catch (err) {
console.error(err);
}
};


3. 优化 Heatmap.tsx

既然这个页面现在是“出版社专属”,你可以微调一下标题和样式,使其更专业。

  • 修改标题:将 WHALE VAULT - 读者分布回响图 改为 出版社全球发行监控大屏
  • 增加权限保护:防止别人直接输入 URL 访问。
1
2
3
4
5
6
7
// /src/pages/Heatmap.tsx 修改建议
// 在 useEffect 的 initChart 开头增加:
const userRole = localStorage.getItem('user_role'); // 假设你登录时存了角色
if (userRole !== 'publisher') {
// navigate('/'); // 强行弹回
}


4. 创建简单的 MintPage.tsx (给读者看)

把之前放在热力图底部的 NFT 领取逻辑移动到一个干净的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/pages/MintPage.tsx
const MintPage = () => {
return (
<div className="flex flex-col items-center justify-center h-screen bg-slate-900 text-white">
<h1 className="text-3xl font-bold mb-8">NFT 领取中心</h1>
<div className="p-8 bg-slate-800 rounded-xl border border-cyan-500/30">
<p className="mb-4">检测到您的读者身份,点击下方按钮领取 NFT</p>
<button className="bg-cyan-500 px-6 py-2 rounded-lg font-bold">立即 Mint</button>
</div>
</div>
);
};

总结

  1. 后端:已经能识别 role: "publisher"
  2. 前端路由:将 /api/v1/analytics/distribution 的展示绑定到 /publisher/dashboard
  3. 分流:在登录成功回调里,根据 role 字符串决定使用 Maps() 去哪个 URL。

下一步建议:
如果你需要,我可以帮你写一个 AuthorDashboard.tsx 的代码模板,专门给作者显示“实时版税累计”和“Arweave 原始数据链接”。你想看看作者后台应该长什么样吗?

更换个人钱包地址

非常抱歉,我完全理解了。你的核心逻辑是:“钱包地址决定身份”

虽然这些地址由你预先分配且没有 Gas(钱),但它们是唯一的身份标识。用户通过扫码(Hash Code)进入系统后,系统首先根据 Code 找到绑定的预分配钱包地址,然后根据这个地址去 Redis 角色池里匹配,决定跳转到出版社后台、作者后台还是普通领取页面。

1. 完整版 App.tsx (基于地址的路由分流)

我们需要在 App.tsx 中增加一个 AuthGate 组件。它的逻辑是:

  1. 提取 URL 中的 hashCode
  2. 请求 /secret/get-binding 获取该 Code 绑定的钱包地址
  3. 拿着这个地址去请求 /secret/verify 识别身份。
  4. 根据返回的 Role 进行跳转。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import React, { useEffect } from 'react'
import { BrowserRouter, Routes, Route, useNavigate, useParams } from 'react-router-dom'
import Home from './pages/Home'
import MintConfirm from './pages/MintConfirm'
import Success from './pages/Success'
import Heatmap from './pages/Heatmap'

// 核心:身份分流中转站
function AuthGate() {
const { hashCode } = useParams();
const navigate = useNavigate();

useEffect(() => {
const identifyAndRedirect = async () => {
try {
// 第一步:根据 Code 获取绑定的预分配钱包地址
const bindRes = await fetch(`http://localhost:8080/secret/get-binding?codeHash=${hashCode}`);
const bindData = await bindRes.json();
const userAddress = bindData.address;

if (!userAddress) {
navigate('/?error=no_binding');
return;
}

// 第二步:根据钱包地址验证角色
const verifyRes = await fetch(`http://localhost:8080/secret/verify?address=${userAddress}&codeHash=${hashCode}`);
const verifyData = await verifyRes.json();

if (verifyData.ok) {
// 根据后端 verifyHandler 返回的 Role 字段跳转
if (verifyData.role === 'publisher') {
navigate('/heatmap');
} else if (verifyData.role === 'author') {
navigate('/author/dashboard');
} else {
// 读者身份,进入领取确认页
navigate(`/valut_mint_nft/${hashCode}`);
}
} else {
navigate('/?error=unauthorized');
}
} catch (err) {
console.error("Auth Error:", err);
}
};

if (hashCode) identifyAndRedirect();
}, [hashCode, navigate]);

return (
<div className="min-h-screen bg-[#0f172a] flex items-center justify-center text-cyan-500">
<div className="animate-pulse">正在识别预分配钱包身份...</div>
</div>
);
}

export default function App() {
return (
<BrowserRouter>
<div className="min-h-screen bg-[#0f172a]">
<main>
<Routes>
{/* 扫码后的统一入口 */}
<Route path="/verify/:hashCode" element={<AuthGate />} />

{/* 基础页面 */}
<Route path="/" element={<Home />} />
<Route path="/valut_mint_nft/:hashCode" element={<MintConfirm />} />
<Route path="/success" element={<Success />} />
<Route path="/heatmap" element={<Heatmap />} />

{/* 预留作者页面 */}
{/* <Route path="/author/dashboard" element={<AuthorDashboard />} /> */}
</Routes>
</main>

<footer className="mx-auto max-w-7xl px-4 py-8 text-center text-white/30 text-xs tracking-widest uppercase">
Whale Vault • Decentralized Identity System © {new Date().getFullYear()}
</footer>
</div>
</BrowserRouter>
)
}

2. 后端 verifyHandler 的配合修改

在你的 tree-role.go 中,verifyHandler 已经实现了“只认地址”的逻辑:

  • 它首先检查地址是否在 vault:roles:publishers 集合中。
  • 接着检查是否在 vault:roles:authors 集合中。
  • 如果都不是,才视为普通读者验证 codeHash

3. Redis 预存方案 (操作指南)

为了让这个系统转起来,你需要通过 redis-cli 完成以下绑定:

  1. 绑定 Code 和地址
    HSET vault:bind:CODE_123 address "0x预分配地址_A"
  2. 设置角色权限
  • 如果 Address_A 是出版社:SADD vault:roles:publishers "0x预分配地址_A"
  • 如果 Address_A 是作者:SADD vault:roles:authors "0x预分配地址_A"

4. 关于“重新填写钱包地址”

既然你提到用户想提现或需要有钱的钱包时可以重新填写地址,这通常发生在 Success.tsx 页面之后。

  • 当前逻辑Success.tsx 显示的是你分配的“未知持有人”或预设地址。
  • 建议功能:在 Success.tsx 底部增加一个“绑定个人钱包(有钱钱包)”的按钮。点击后,用户输入新地址,后端将 Redis 中的 vault:bind:hashCode 里的 address 字段更新为新地址。

下一步建议:
你现在的 Success.tsx 已经有核验逻辑了。需要我帮你给它增加一个**“更换个人钱包地址”**的表单功能吗?这样用户就可以把 NFT 从你分配的无钱钱包“转移”到他们自己的钱包里。

validate.go

这是一个非常精妙的架构。你已经成功地从“基于 Code 的认证”转型为 “基于预分配钱包地址的身份确权”

通过检查你上传的 tree-role.goSuccess.tsxApp.tsx,我发现了几个逻辑上的关键点和潜在风险,如果不修正,出版社扫码时确实会卡在“激活码无效”这一步。

1. 逻辑诊断:为什么会报错?

tree-role.go 中,你的 verifyHandler 逻辑是:

  • 第一步:检查 userAddr 是否在 vault:roles:publishers
  • 第二步:如果不是,再检查 codeHash 是否在 vault:codes:valid

问题出在这里:
虽然 verifyHandler 能正确识别出版社角色,但你的前端流程通常是先经过 MintConfirm 或在 Success 页面触发了 mintHandler。在 tree-role.gomintHandler 中,它第一件事就是尝试从 vault:codes:valid 移除 codeHash
由于你的出版社 codeHash(如 pub_001)不在这个集合里,后端会立刻返回 激活码无效或已被使用

2. 修正方案:后端 tree-role.go

你需要让 mintHandler 具备“身份识别”能力。如果是出版社的地址,直接放行,不校验激活码。

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
26
27
28
29
// tree-role.go 中的 mintHandler 建议修改如下:

func mintHandler(w http.ResponseWriter, r *http.Request) {
var req struct { Dest string; CodeHash string }
json.NewDecoder(r.Body).Decode(&req)

// --- 新增:出版社地址“特权”校验 ---
destAddr := strings.ToLower(req.Dest)
isPub, _ := rdb.SIsMember(ctx, "vault:roles:publishers", destAddr).Result()
if isPub {
// 出版社不需要消耗激活码,直接返回成功并告知前端角色
sendJSON(w, http.StatusOK, CommonResponse{
Ok: true,
Status: "WELCOME_PUBLISHER",
Role: "publisher",
})
return
}
// --------------------------------

// 原有逻辑:普通读者必须消耗激活码
removed, _ := rdb.SRem(ctx, "vault:codes:valid", req.CodeHash).Result()
if removed == 0 {
sendJSON(w, http.StatusForbidden, CommonResponse{Error: "激活码无效或已被使用"})
return
}
// ... 剩下的执行 executeMintLegacy 逻辑
}

3. 修正方案:前端 Success.tsx

你的 Success.tsx 目前在验证成功后只是显示了勋章。为了实现出版社“扫码即看图”,需要增加自动跳转。

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
26
// Success.tsx 中的 useEffect 修改建议:

useEffect(() => {
const verifyAndRedirect = async () => {
// ... 原有代码 ...
try {
const response = await fetch(`http://192.168.1.9:8080/secret/verify?codeHash=${codeHash}&address=${userAddress}`);
const data = await response.json();

if (!response.ok) throw new Error('身份核验失败');

// --- 新增:如果是出版社,直接瞬移到热力图 ---
if (data.role === 'publisher') {
navigate('/heatmap');
return;
}
// ---------------------------------------

setTimeout(() => setIsLoading(false), 1500);
} catch (err) {
// ... 错误处理 ...
}
};
verifyAndRedirect();
}, [codeHash, userAddress, navigate]);

4. 修正方案:前端 App.tsx (完整版)

你的 App.tsx 已经包含了路由定义,但建议增加一个 /verify/:hashCode 的入口,作为二维码的直接跳转点,这样可以省去用户点击“领取”的动作,实现真正的“扫码即身份识别”。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React from 'react'
import { BrowserRouter, Routes, Route, useNavigate, useParams } from 'react-router-dom'
import Home from './pages/Home'
import MintConfirm from './pages/MintConfirm'
import Success from './pages/Success'
import Heatmap from './pages/Heatmap'

// 扫码中转逻辑:自动识别并分流
function AutoRedirect() {
const { hashCode } = useParams();
const navigate = useNavigate();

React.useEffect(() => {
const start = async () => {
// 1. 获取绑定地址
const res = await fetch(`http://localhost:8080/secret/get-binding?codeHash=${hashCode}`);
const bind = await res.json();

if (bind.address) {
// 2. 预检身份
const vRes = await fetch(`http://localhost:8080/secret/verify?address=${bind.address}&codeHash=${hashCode}`);
const vData = await vRes.json();

if (vData.role === 'publisher') navigate('/heatmap');
else if (vData.role === 'author') navigate('/author_dashboard');
else navigate(`/valut_mint_nft/${hashCode}`); // 读者去领 NFT
}
};
start();
}, [hashCode]);

return <div className="text-white text-center mt-20">身份确权中...</div>;
}

export default function App() {
return (
<BrowserRouter>
<div className="min-h-screen bg-[#0f172a]">
<main>
<Routes>
<Route path="/" element={<Home />} />
{/* 新增自动分流入口 */}
<Route path="/v/:hashCode" element={<AutoRedirect />} />

<Route path="/valut_mint_nft/:hashCode" element={<MintConfirm />} />
<Route path="/success" element={<Success />} />
<Route path="/heatmap" element={<Heatmap />} />
</Routes>
</main>
<footer className="mx-auto max-w-7xl px-4 py-8 text-center text-white/30 text-xs">
Whale Vault • Decentralized Identity System © {new Date().getFullYear()}
</footer>
</div>
</BrowserRouter>
)
}

总结

  1. 后端:必须在 mintHandlerverifyHandler 里放行出版社地址,不要求其 codeHash 必须在 vault:codes:valid 集合里。
  2. 前端:在接收到 role: "publisher" 后,直接进行 Maps('/heatmap')
  3. Redis:你的 HSET vault:bind:pub_001 address 0x9f5... 是正确的,只需要后端逻辑放行即可。

如果你完成了上述 tree-role.gomintHandler 的修改,报错就会消失,出版社就能顺利进入地图。