跳转到内容
123xiao | 无名键客

《Web3 中级实战:基于智能合约与钱包登录构建一套可落地的链上会员积分系统》

字数: 0 阅读时长: 1 分钟

Web3 中级实战:基于智能合约与钱包登录构建一套可落地的链上会员积分系统

很多团队一提“链上会员系统”,第一反应是:把用户积分直接写进智能合约不就完了?
但真开始做,你很快会遇到一堆现实问题:

  • 用户怎么登录?是不是必须邮箱+密码?
  • 钱包地址和会员身份怎么绑定?
  • 所有积分变化都上链,Gas 成本能不能接受?
  • 后端还能不能做风控、活动审计和补发?
  • 前端展示“我的积分”时,是读链还是读库?
  • 如果以后要接 NFT 等级、权益兑换、任务系统,架构还能不能扩?

这篇文章我不打算只讲“怎么写一个积分合约”,而是从架构落地的角度,带你搭一套能上线、可扩展、能排错的链上会员积分系统。读完后你应该能自己做出一个中级可用版本。


背景与问题

传统会员积分系统通常依赖中心化数据库:
用户注册账号,服务端记录积分,后台定时发券、做等级成长。

到了 Web3 场景,至少有三个变化:

  1. 身份入口从账号密码变成钱包 用户未必愿意注册,更多是“连接钱包即登录”。

  2. 资产与权益天然要求可验证 比如会员等级、徽章、积分变动记录,希望能公开验证,避免“平台说了算”。

  3. 链上与链下必须协同 并不是所有数据都适合上链。行为日志、任务完成明细、风控结果,更适合放在链下。

所以,一个真正可落地的方案,通常不是“全上链”,而是:

  • 钱包做身份入口
  • 智能合约做可信积分账本或结算层
  • 后端做任务计算、签名授权、风控和索引
  • 前端统一展示链上状态和链下业务态

典型业务诉求

我们先把需求压缩成一个中等复杂度版本:

  • 用户通过 MetaMask 登录
  • 后端验证钱包签名,建立会话
  • 用户完成任务后,可领取积分
  • 积分发放必须可审计,防止后端随便改
  • 支持查询当前积分余额
  • 后续可扩展会员等级、权益兑换、NFT 徽章

这个问题的本质,不是“写一个 mapping(address => uint256)”,而是设计可信边界


方案概览:为什么推荐“链下计算,链上结算”

我比较推荐中级项目采用这套思路:

  • 登录:钱包签名登录(SIWE 风格或自定义 nonce)
  • 任务判断:链下完成
  • 积分发放授权:后端签名
  • 积分记账:用户调用合约领取,合约验证签名后铸记积分
  • 展示查询:前端读链 + 后端聚合补充信息

这么做的原因很现实:

方案对比

方案优点缺点适用场景
全链上任务与积分最透明开发复杂、Gas 高、灵活性差极简游戏规则、纯链上行为
全链下积分系统成本低、实现快缺乏可信性,用户不完全信任Web2 迁移试水
链下计算 + 链上结算成本与可信度平衡增加签名与校验复杂度大多数会员/积分/任务平台

这篇文章就按第三种方案来做。


整体架构

flowchart LR
  U[用户钱包] --> F[前端 DApp]
  F --> W[钱包签名登录]
  F --> B[业务后端]
  B --> DB[(任务/订单/风控数据库)]
  B --> S[签名服务]
  F --> C[积分智能合约]
  S --> F
  C --> I[链上事件索引器]
  I --> B

这张图里最关键的是两个动作:

  1. 登录签名:证明“这个地址就是我”
  2. 领取签名:证明“后端认可你这次可以拿多少积分”

注意,这两个签名不是一回事,千万别混用。


核心原理

1. 钱包登录:用签名替代密码

传统系统靠密码证明身份,Web3 更常见的是:

  • 后端生成 nonce
  • 前端让钱包对消息签名
  • 后端验签,确认地址所有权
  • 验签通过后,签发 session/JWT

核心点有两个:

  • 消息必须带 nonce,防止重放
  • nonce 必须一次性使用

登录时序

sequenceDiagram
  participant U as 用户
  participant F as 前端
  participant B as 后端
  participant M as 钱包

  F->>B: 请求登录 nonce
  B-->>F: 返回 nonce
  F->>M: 发起签名
  M-->>F: 返回 signature
  F->>B: 提交 address + nonce + signature
  B->>B: 验签并销毁 nonce
  B-->>F: 返回 session/JWT

如果没有 nonce,一条旧签名可能被重复利用,这就是经典重放攻击入口。


2. 积分发放:后端授权,合约验签执行

任务逻辑通常在后端,比如:

  • 连续签到 7 天
  • 购买某商品
  • 绑定社媒账号
  • 完成邀请任务
  • 持有某 NFT 或参与某次链上交互

这些判断结果往往要结合数据库、活动规则、风控系统,适合链下完成。
但如果完全由后端改数据库积分,用户又不够放心。

所以更好的做法是:

  • 后端根据业务规则生成一份“积分领取授权”
  • 使用平台私钥对授权数据签名
  • 用户把授权和签名提交到合约
  • 合约验证签名合法后执行记账

领取积分时序

sequenceDiagram
  participant U as 用户
  participant F as 前端
  participant B as 后端
  participant C as 积分合约

  U->>F: 点击领取积分
  F->>B: 请求 claim 授权
  B->>B: 校验任务、风控、去重
  B-->>F: amount + claimId + deadline + signature
  F->>C: claim(amount, claimId, deadline, signature)
  C->>C: 验签/去重/过期检查
  C-->>F: 领取成功

为什么要有 claimIddeadline

  • claimId:防止同一份授权被重复领取
  • deadline:限制授权有效期,降低泄漏风险

3. 链上数据设计:最小可信集

很多人一开始喜欢把所有任务明细都上链,这通常没必要。
对于会员积分系统,建议只把最关键的可验证状态放上链:

  • 用户累计积分余额
  • 已使用的 claimId
  • 管理员签名验证公钥/地址
  • 必要的事件日志

而这些数据尽量保持简单,合约就会更稳、更省 Gas。

一个合理的数据边界

classDiagram
  class MembershipPoints {
    +owner : address
    +signer : address
    +balances(address) uint256
    +usedClaims(bytes32) bool
    +claim(amount, claimId, deadline, signature)
    +setSigner(address)
    +balanceOf(address) uint256
  }

实战代码(可运行)

下面给一个可运行的最小实现:

  • 合约:Solidity,基于 OpenZeppelin
  • 后端:Node.js + Express + ethers
  • 前端调用:ethers.js 示例

为了让文章聚焦,我把它做成一个“可领取积分的链上积分账本”。


合约实现:MembershipPoints.sol

说明:这里我们不直接用 ERC20,因为很多积分系统并不希望积分可自由转账。
会员积分更多是“账户状态”,不是通用代币。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

contract MembershipPoints is Ownable {
    using ECDSA for bytes32;

    mapping(address => uint256) private _balances;
    mapping(bytes32 => bool) public usedClaims;

    address public signer;

    event PointsClaimed(address indexed user, uint256 amount, bytes32 indexed claimId);
    event SignerUpdated(address indexed oldSigner, address indexed newSigner);

    constructor(address initialOwner, address initialSigner) Ownable(initialOwner) {
        require(initialSigner != address(0), "invalid signer");
        signer = initialSigner;
    }

    function setSigner(address newSigner) external onlyOwner {
        require(newSigner != address(0), "invalid signer");
        address old = signer;
        signer = newSigner;
        emit SignerUpdated(old, newSigner);
    }

    function balanceOf(address user) external view returns (uint256) {
        return _balances[user];
    }

    function claim(
        uint256 amount,
        bytes32 claimId,
        uint256 deadline,
        bytes calldata signature
    ) external {
        require(block.timestamp <= deadline, "signature expired");
        require(!usedClaims[claimId], "claim already used");

        bytes32 digest = keccak256(
            abi.encodePacked(
                block.chainid,
                address(this),
                msg.sender,
                amount,
                claimId,
                deadline
            )
        );

        bytes32 ethSigned = MessageHashUtils.toEthSignedMessageHash(digest);
        address recovered = ECDSA.recover(ethSigned, signature);
        require(recovered == signer, "invalid signature");

        usedClaims[claimId] = true;
        _balances[msg.sender] += amount;

        emit PointsClaimed(msg.sender, amount, claimId);
    }
}

Hardhat 部署脚本

scripts/deploy.js

const { ethers } = require("hardhat");

async function main() {
  const [deployer] = await ethers.getSigners();

  // 演示用:实际项目请改成后端签名地址
  const signerAddress = deployer.address;

  const MembershipPoints = await ethers.getContractFactory("MembershipPoints");
  const contract = await MembershipPoints.deploy(deployer.address, signerAddress);
  await contract.waitForDeployment();

  console.log("MembershipPoints deployed to:", await contract.getAddress());
  console.log("Owner:", deployer.address);
  console.log("Signer:", signerAddress);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.20",
};

安装与运行

npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
npx hardhat
npx hardhat compile
npx hardhat run scripts/deploy.js --network hardhat

后端实现:登录验签与积分授权签名

这里用 Express 做一个最小后端,包含两个核心接口:

  • /auth/nonce:生成登录 nonce
  • /auth/verify:验签登录
  • /points/claim-signature:生成积分领取签名

server.js

const express = require("express");
const crypto = require("crypto");
const { ethers } = require("ethers");

const app = express();
app.use(express.json());

// 演示用内存存储,生产环境请换 Redis/DB
const nonces = new Map();
const sessions = new Map();
const claimedTasks = new Set();

// 用于给 claim 授权签名的服务端私钥
// 请替换成测试私钥,绝不要用真实主网热钱包私钥直接写死
const SIGNER_PRIVATE_KEY = "0x59c6995e998f97a5a0044966f094538e0d7ef4d3f8b0f5f2ff1f908db8b27c59";
const signerWallet = new ethers.Wallet(SIGNER_PRIVATE_KEY);

// 模拟合约地址,生产环境应配置真实部署地址
const CONTRACT_ADDRESS = "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC";

// 1) 获取登录 nonce
app.post("/auth/nonce", (req, res) => {
  const { address } = req.body;
  if (!address || !ethers.isAddress(address)) {
    return res.status(400).json({ error: "invalid address" });
  }

  const nonce = crypto.randomBytes(16).toString("hex");
  nonces.set(address.toLowerCase(), nonce);

  res.json({
    address,
    nonce,
    message: `Login to Membership System\nAddress: ${address}\nNonce: ${nonce}`,
  });
});

// 2) 验证登录签名
app.post("/auth/verify", async (req, res) => {
  const { address, signature } = req.body;
  const lower = String(address || "").toLowerCase();
  const nonce = nonces.get(lower);

  if (!nonce) {
    return res.status(400).json({ error: "nonce not found" });
  }

  const message = `Login to Membership System\nAddress: ${address}\nNonce: ${nonce}`;

  try {
    const recovered = ethers.verifyMessage(message, signature);
    if (recovered.toLowerCase() !== lower) {
      return res.status(401).json({ error: "signature invalid" });
    }

    nonces.delete(lower);

    const token = crypto.randomBytes(24).toString("hex");
    sessions.set(token, { address: lower, loginAt: Date.now() });

    res.json({ token, address: lower });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// 简单 session 中间件
function auth(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token || !sessions.has(token)) {
    return res.status(401).json({ error: "unauthorized" });
  }
  req.user = sessions.get(token);
  next();
}

// 3) 生成积分 claim 签名
app.post("/points/claim-signature", auth, async (req, res) => {
  const userAddress = req.user.address;
  const { taskId } = req.body;

  if (!taskId) {
    return res.status(400).json({ error: "taskId required" });
  }

  // 模拟任务去重:同一个地址同一个任务只允许一次
  const taskKey = `${userAddress}:${taskId}`;
  if (claimedTasks.has(taskKey)) {
    return res.status(400).json({ error: "task already claimed" });
  }

  // 模拟业务判断:任务积分
  const amount = 100;
  const claimId = ethers.keccak256(
    ethers.toUtf8Bytes(`${userAddress}:${taskId}:${Date.now()}`)
  );
  const deadline = Math.floor(Date.now() / 1000) + 10 * 60;

  // 与合约中的 abi.encodePacked 对齐
  const digest = ethers.solidityPackedKeccak256(
    ["uint256", "address", "address", "uint256", "bytes32", "uint256"],
    [
      31337, // 本地 hardhat chainId
      CONTRACT_ADDRESS,
      userAddress,
      amount,
      claimId,
      deadline,
    ]
  );

  const signature = await signerWallet.signMessage(ethers.getBytes(digest));

  claimedTasks.add(taskKey);

  res.json({
    userAddress,
    amount,
    claimId,
    deadline,
    signature,
    signer: signerWallet.address,
  });
});

app.listen(3001, () => {
  console.log("Server running at http://localhost:3001");
});

安装依赖

npm install express ethers
node server.js

前端调用示例

下面示例演示三个步骤:

  1. 连接钱包
  2. 签名登录
  3. 获取 claim 签名并调用合约领取积分

frontend.js

import { ethers } from "ethers";

const CONTRACT_ADDRESS = "你的合约地址";

const ABI = [
  "function claim(uint256 amount, bytes32 claimId, uint256 deadline, bytes signature) external",
  "function balanceOf(address user) external view returns (uint256)"
];

async function connectWallet() {
  if (!window.ethereum) throw new Error("MetaMask not found");
  const provider = new ethers.BrowserProvider(window.ethereum);
  await provider.send("eth_requestAccounts", []);
  const signer = await provider.getSigner();
  const address = await signer.getAddress();
  return { provider, signer, address };
}

async function login(address, signer) {
  const nonceResp = await fetch("http://localhost:3001/auth/nonce", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ address })
  });
  const nonceData = await nonceResp.json();

  const signature = await signer.signMessage(nonceData.message);

  const verifyResp = await fetch("http://localhost:3001/auth/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ address, signature })
  });

  return verifyResp.json();
}

async function claimPoints(signer, token) {
  const claimResp = await fetch("http://localhost:3001/points/claim-signature", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${token}`
    },
    body: JSON.stringify({ taskId: "task-001" })
  });

  const claimData = await claimResp.json();
  if (claimData.error) throw new Error(claimData.error);

  const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer);

  const tx = await contract.claim(
    claimData.amount,
    claimData.claimId,
    claimData.deadline,
    claimData.signature
  );

  await tx.wait();
  console.log("claim success:", tx.hash);

  const user = await signer.getAddress();
  const balance = await contract.balanceOf(user);
  console.log("points balance:", balance.toString());
}

async function main() {
  const { signer, address } = await connectWallet();
  const loginResult = await login(address, signer);
  console.log("login result:", loginResult);

  await claimPoints(signer, loginResult.token);
}

main().catch(console.error);

关键设计取舍

到这里,你已经有一套可工作的最小系统了。
但真正做架构时,重点不是“能跑”,而是“以后不会推倒重来”。

1. 为什么不把积分做成可转账 ERC20

很多会员积分不希望具备金融属性,原因很直接:

  • 可转账后,积分会被交易和刷量
  • 会员等级逻辑容易被套利
  • 合规边界会变复杂

如果你的积分只是权益凭证,不可转账账本通常更合适。
如果以后要做兑换市场,可以再单独设计代币层,而不是一开始就上 ERC20。

2. 为什么 claim 要由用户自己提交,而不是后端代发

有两种模式:

  • 后端直接调用合约发积分
  • 后端签名,用户自己 claim

我更推荐第二种,原因是:

  • 用户自己支付 Gas,平台成本可控
  • 用户能明确感知“这次积分到账是链上操作”
  • 后端不用托管大量链上交易发送逻辑

当然,如果你要追求极致体验,也可以配合 meta transactionERC-2771 做平台代付 Gas。

3. 为什么后端还需要数据库

因为链上不是万能业务库。
你仍然需要数据库来记录:

  • 任务完成明细
  • 活动配置
  • 风控标记
  • 登录会话
  • 索引缓存
  • 审计日志

一个很常见的误区是“既然是 Web3,就不要后端”。
真实业务里,没有后端你几乎做不了完整会员系统。


容量估算与扩展思路

对于中级项目,我建议提前考虑三个规模节点:

阶段 A:冷启动期(< 1 万用户)

  • 单体后端 + Redis + PostgreSQL 足够
  • 使用链上事件回写数据库
  • 手动运营活动可接受

阶段 B:增长期(1 万 ~ 50 万用户)

  • 将签名服务独立
  • 增加消息队列处理任务结算
  • 引入链上索引器或 The Graph 类方案
  • 积分展示页尽量用缓存

阶段 C:高并发活动期

  • 批量签名或 Merkle claim
  • 多链部署或 L2 迁移
  • 热门活动任务异步化
  • 合约逻辑尽量冻结,复杂规则留在链下

如果你预计活动领取非常频繁,单笔签名 claim 也会有瓶颈。这时可以升级为:

  • Merkle Root 批次空投
  • 按活动批次上链 root
  • 用户用 proof 自助领取

这样更适合海量发放场景,但实现复杂度也会升高。


常见坑与排查

这一节我尽量讲点真会踩到的坑。

1. 合约验签总失败

常见原因

  • 前后端 chainId 不一致
  • 合约地址传错
  • abi.encodePacked 与后端 solidityPackedKeccak256 类型顺序不一致
  • 前端传的是别人的登录 token
  • 签名的是原始 digest,合约却按 toEthSignedMessageHash 验证

排查建议

先把以下内容全部打印出来对比:

  • chainId
  • contract address
  • user address
  • amount
  • claimId
  • deadline
  • digest
  • recovered signer

如果这些字段有一个不一致,验签就必然失败。


2. 登录明明签了名,后端却验不过

常见原因

  • 钱包签名的 message 和后端拼接的 message 不完全一致
  • 地址大小写处理不统一
  • nonce 已经被消费
  • 用户切换了钱包账号

排查建议

后端保存完整原始 message,前端签什么,后端就验什么。
不要在前端签完后,后端再“重新拼一个看起来一样的字符串”,很容易因为换行符或空格不同导致失败。


3. 同一任务被重复领取

常见原因

  • 只在数据库做去重,没有在链上做 claimId 防重
  • claimId 生成规则不稳定
  • 后端并发下重复发放签名

排查建议

必须双重防重:

  • 链下:任务维度唯一约束
  • 链上usedClaims[claimId]

只做一层,迟早出问题。


4. 前端显示积分和链上余额对不上

常见原因

  • 前端展示的是数据库缓存,不是链上实时值
  • 交易已发送但未确认
  • 链上事件索引延迟
  • 用户切换网络后读了错误链

排查建议

给用户明确区分:

  • “待确认”
  • “已上链”
  • “已索引同步”

别把所有状态混成一个“已到账”,否则运营和客服很难处理。


5. 签名私钥泄漏风险被低估

这是我比较想强调的一点。
很多项目 demo 阶段直接把签名私钥放在后端配置里,后来一路带到生产环境,非常危险。

建议

  • 使用专门签名服务
  • 最好接 HSM/KMS
  • 定期轮换 signer
  • 合约支持 setSigner
  • 给每个环境使用不同 signer

安全最佳实践

1. 严格区分“登录签名”和“业务签名”

这两个签名的用途不同、生命周期不同、风险不同:

  • 登录签名:证明地址控制权
  • 业务签名:证明平台授权结果

不要拿登录签名去直接发积分,也不要把 claim 授权当登录凭证。


2. 给 claim 增加过期时间和域隔离

本文示例里做了三层隔离:

  • block.chainid
  • address(this)
  • deadline

这样做可以避免:

  • 同一签名跨链复用
  • 同一签名跨合约复用
  • 长期有效签名泄漏后被滥用

如果你想做得更规范,可以进一步采用 EIP-712 typed data


3. 权限最小化

合约内建议只保留必要权限:

  • owner:管理 signer
  • signer:签发 claim 授权

不要让 owner 直接随意修改所有用户积分,除非你明确接受中心化治理。
如果业务必须支持人工补发,建议走“补发 claim”流程,而不是“后台硬改余额”。


4. 防刷与风控不要幻想交给合约

合约只能验证规则,不能理解复杂业务风险。
像这些事情,仍然要在链下做:

  • 女巫攻击检测
  • 设备/IP 异常
  • 多账号套利
  • 任务脚本刷量
  • 黑名单控制

链上可信不等于抗滥用。


5. 事件日志必须设计好

至少要有:

  • PointsClaimed(user, amount, claimId)
  • SignerUpdated(oldSigner, newSigner)

这样你后续做:

  • 区块链浏览器排查
  • 数据索引
  • 审计回放
  • 运营对账

都会轻松很多。


性能最佳实践

1. 读多写少时,前端优先走聚合接口

会员系统的典型特点是:

  • 查询多
  • 发放相对少
  • 页面经常要同时展示积分、等级、任务进度、权益状态

如果每次都让前端直接发十几个 RPC 请求,体验会很差。
更实际的方案是:

  • 关键余额读链
  • 复杂页面走后端聚合接口
  • 后端用索引缓存提升响应速度

2. 大规模发积分考虑批量证明,而不是逐条签名

当活动规模很大,比如一次给 10 万用户发积分时:

  • 逐个生成 claim 签名,成本高
  • 用户逐个领取,体验也一般

这时应考虑:

  • Merkle 树批量分发
  • 活动 root 上链
  • 用户携 proof 领取

它不一定适合所有项目,但一旦活动频次高、名单大,这条路几乎是必走的。


3. 选链要贴合积分业务,而不是追热点

如果积分系统交互频繁,主网通常不是首选。
更适合考虑:

  • Polygon
  • Arbitrum
  • Base
  • Optimism
  • 其他低 Gas EVM 链

选择标准很简单:

  • 钱包兼容性
  • Gas 成本
  • 基础设施成熟度
  • 你现有用户在哪条链

进一步扩展:会员等级、权益兑换、NFT 勋章

当积分账本稳定后,通常会继续长出三类能力:

1. 会员等级

可根据积分区间或成长值计算:

  • Bronze
  • Silver
  • Gold
  • Platinum

实现方式有两种:

  • 链上实时计算等级
  • 链下计算后上链记录结果

前者透明,后者更灵活。

2. 权益兑换

比如 1000 积分兑换一次优惠券。
这时你需要在合约里引入“扣减积分”能力,或者把兑换动作留在链下再做链上登记。

3. NFT 勋章

满足条件时铸造 NFT,作为会员成就或身份展示。
这是链上会员系统很常见的增强层,因为 NFT 非常适合做“可展示的荣誉凭证”。


一个更稳妥的落地建议

如果你准备真的上线,我建议按这个顺序推进:

  1. 先做钱包登录 + nonce 验签
  2. 再做不可转账积分账本
  3. 接入后端 claim 授权签名
  4. 补齐事件索引和运营后台
  5. 最后再考虑等级、兑换、NFT

不要一上来就想把积分、等级、商城、徽章、邀请裂变全做了。
Web3 项目的复杂度,往往不是卡在合约,而是卡在身份、风控、状态同步和运营排错


总结

一套可落地的链上会员积分系统,核心不是“把积分放到链上”,而是设计清楚这三件事:

  • 谁在证明用户身份:钱包签名登录
  • 谁在决定积分是否应该发:链下业务与风控
  • 谁在保证积分发放可验证:链上合约验签结算

如果你是中级开发者,我建议你先采用本文这套基线方案:

  • 钱包登录:nonce + 验签
  • 积分发放:后端签名授权 + 合约 claim
  • 数据协同:链上账本 + 链下索引与业务库
  • 风险控制:claimId 防重 + deadline 过期 + signer 可轮换

它的边界也很明确:

  • 适合会员、任务、成长值、权益体系
  • 不适合一开始就做高频金融化积分交易
  • 用户量暴涨后,需要升级为批量证明与更强索引架构

如果你现在正准备做一个 Web3 会员系统,不妨先把最小闭环跑通:
连接钱包 → 签名登录 → 完成任务 → 获取授权 → 链上领取积分 → 前端展示余额。

这条链路一旦打通,后面的等级、兑换、勋章,都会顺很多。


分享到:

上一篇
《自动化测试稳定性治理实战:从脆弱用例定位到持续集成中的误报率优化》
下一篇
《分布式架构下的幂等性设计实战:从接口重试到消息消费防重的完整方案》