Web3 大学面试手册
# Web3 大学面试手册
从项目介绍到系统设计 从技术原理到压力追问
这份手册以仓库中的真实实现、部署记录、测试与已知限制为依据。目标不是背术语,而是让你能从业务目标讲到架构边界,再深入到合约安全、DeFi、钱包认证、链上链下数据一致性、事件驱动自动化和 AWS 部署。
| 适用面试 | 建议用法 |
|---|---|
| Web3 全栈 | 优先掌握项目叙事、Solidity、viem、钱包认证和链上链下边界 |
| 智能合约 | 重点复习 ERC20、ERC721、ERC5192、CEI、重入、授权和 Foundry |
| 后端与云 | 重点复习 Hono、tRPC、PostgreSQL、KMS Relayer、ECS、RDS 和 S3 |
| 系统设计 | 重点复习真相源、幂等性、一致性、密钥边界、故障恢复和扩展方案 |
版本 2026 年 9 月
# 使用方法
先读 项目介绍,理解购课和发证两条主线,再按问题查阅实现与取舍。技术基础复用 Aladdin 和站内专题笔记,这里重点说明 Web3 大学怎样使用这些技术、遇到了什么问题,以及怎样处理失败。
# 面试前优先掌握的核心问题
下面是可以直接口述的短答案。后文保留详细原理、代码落点和压力追问,需要时再下钻。
1. 为什么称为半中心化?参考答案
用户可以在链上查到购买记录和证书,不必只相信平台页面。但课程内容、学习进度和是否结业仍由平台维护,管理员还能管理发证权限,所以它是链上可验证、链下仍需信任平台的半中心化系统。
2. 为什么不把所有数据都放到链上?参考答案
链上存储成本高、公开且更新慢,只适合保存需要公开验证的资产和权限事实。课程价格、购买关系和证书放链上;视频、评论和学习进度体积大、写入频繁,放 PostgreSQL 或对象存储更合适。两边通过 courseId 和 tokenURI 关联。
3. 用户购买一门课程会经过哪些步骤?参考答案
前端先检查网络、YD 余额和 allowance,也就是课程合约目前可以扣取多少 YD。余额不足时可通过 Uniswap V2 用 ETH 或 USDC 兑换 YD,额度不足时先调用 approve 完成授权,再调用 CoursePlatform.purchase。最后必须等交易回执确认成功,钱包给出的交易哈希只代表已经广播,不能提前当成购买成功。
4. 钱包签名登录为什么不需要私钥上传?参考答案
后端先发一个只能使用一次的 nonce,钱包在本地用私钥签名,服务端通过签名恢复地址并核对 domain、uri、nonce 和时间。私钥始终留在钱包中;验证成功后由 better-auth 签发 HttpOnly Session。nonce 必须原子消费,否则同一签名可能被重复使用。
5. 结业证书为什么需要 Relayer 和 KMS?参考答案
合约无法直接读取数据库里的学习进度,所以需要链下中继服务 Relayer 监听申领事件,再向后端确认用户确实完成课程。确认后生成 NFT metadata,并使用 AWS KMS 中不可导出的签名密钥发送 mint 交易。KMS 降低了私钥文件泄露风险,但调用权限、交易内容和 issuer 权限仍然需要限制。
6. 怎样避免事件漏处理或重复发证?参考答案
生产方案用 RDS 保存扫描进度,服务重启后从已记录区块附近补扫,处理成功才推进进度。重复扫描不可怕,合约中的 hasClaimed 会阻止同一钱包为同一课程再领一张证书。进度记录防止漏发,合约检查防止重复发,二者缺一不可。
7. Chainlink CRE 和当前 Relayer 是什么关系?参考答案
Chainlink CRE 是另一套结业自动化执行方案,与 Relayer 解决的是同一条业务链路,但当前生产路径仍是 AWS KMS Relayer。CRE 已经过隔离验证,但生产部署权限尚未开通,还没有替代 KMS Relayer。两者是同一业务的两条执行路径,不是每次发证都要一起运行。
8. 这套系统怎样部署到生产环境?参考答案
生产方案让 Web、API 和 Relayer 分开运行在 ECS Fargate;CloudFront 缓存、WAF 过滤异常请求,ALB 把请求交给健康实例。RDS 保存数据,S3 保存头像,KMS 保护发证私钥。上线还要验证监控、回滚和备份恢复,不能只看页面是否打开。
# 事实边界
本文按生产项目的设计标准组织说明:不只讲功能能跑,还要讲权限、重复请求、监控和故障恢复。正文给出完整方案与可口述答案,不在每节反复讨论是否适合 Demo。
实际经历与设计方案要区分:本机 Relayer 代码尚未持久化扫描进度,文中的 RDS checkpoint、租约和停机补扫按生产方案说明;云端高可用配置与恢复演练以部署记录为准。合约业务在 Sepolia 测试网,CRE 尚未接管发证,不等于已上主网。
# 技术栈与项目分工
| 模块 | 主要技术 | 在本项目中负责什么 |
|---|---|---|
| 前端 | TypeScript、Next.js 与 React、Tailwind CSS | 课程页面、购买与兑换交互,以及钱包切换后的状态清理 |
| 钱包与身份 | viem、Privy、better-auth | 连接当前钱包、签名登录、建立会话、发送交易并等待确认 |
| API 与数据 | Hono、tRPC、Zod、PostgreSQL 与 Drizzle | 校验身份与输入,保存课程内容和学习进度,合并链上课程状态 |
| 智能合约 | Solidity、Foundry、OpenZeppelin | 实现并测试代币支付、购买记录、不可转让证书与发行权限 |
| 代币兑换 | Uniswap V2 | 将 ETH 或 USDC 换成 YD,处理报价、授权、滑点保护与交易确认 |
| 自动发证 | KMS Relayer、Chainlink CRE、Pinata/IPFS | 核验结业条件、保存证书内容并提交铸造交易;CRE 是已隔离验证的备选路径 |
| 云部署 | AWS CDK、Docker、ECS Fargate、RDS | 分别运行 Web、API 和 Relayer,持久化业务数据与消费进度,配置入口、权限和监控 |
# 第一部分 项目介绍与架构叙事
# 项目介绍
这是一个让课程购买记录和结业证书可以在链上验证的在线教育平台。学生连接钱包并签名登录后,可以用 ETH 或 USDC 兑换平台代币 YD,再用 YD 购买课程;完成学习后,可以申领一张绑定当前钱包、不能转让的 NFT 结业证书。
我把系统分成链上和链下两部分:合约保存课程价格、购买关系和证书,数据库保存课程资料、评论与学习进度,两边通过课程 ID 关联。前端使用 Next.js,后端通过 Hono、tRPC 和 viem 组合数据库与链上数据。这样既能公开验证付款和证书,又不用把大量内容和频繁更新的数据都放到链上。
最关键的是把链下的学习结果可靠地变成链上的证书。Relayer 收到申领事件后,向后端核验结业条件,生成证书资料,再调用 AWS KMS 签名并发送发证交易。生产可靠性方案用数据库保存扫描进度,重启后补扫,再由合约阻止重复领取。
生产部署采用独立的 Web、API 和 Relayer 服务,业务数据与扫描进度使用 RDS。合约运行在 Sepolia 测试网;Chainlink CRE 作为替代发证方案验证,尚未接管现有发行路径。
# 业务角色和主流程
| 角色 | 在项目里做什么 | 权限从哪里来 |
|---|---|---|
| 学生 | 换币、买课、学习、评论、领取证书 | 钱包签名登录,付费权限再查链上购买记录 |
| 老师 | 提交课程和资料,等待审核 | 数据库 isTeacher 教师标记,后端每次检查 |
| Owner(管理员) | 审核教师和课程、上架或下架课程 | 与 CoursePlatform.owner 返回的管理员钱包比对 |
| Relayer(发证服务) | 检查申领资格,生成资料并提交发证交易 | 使用证书合约认可的 issuer 发证地址 |
| CRE Workflow(备选工作流) | 由 Chainlink 网络执行同一套发证流程 | 报告来源验证通过后,由 Receiver 接收合约发证 |
# 链上链下数据边界
| 数据 | 保存位置 | 为什么这样分 |
|---|---|---|
| 课程价格、上架状态、购买记录 | CoursePlatform 合约 | 影响付款和付费访问,后端核验时以合约为准 |
| YD 余额与扣款授权额度 | YDToken 合约 | 属于用户资产状态,交易前读取确认 |
| 证书归属与资料地址 | NFT 合约;图片和说明放 IPFS | 合约公开证明归属,文件不必全部写到链上 |
| 标题、简介、视频地址、大纲 | PostgreSQL | 经常更新,按同一 courseId 与链上课程对应 |
| 评论和学习进度 | PostgreSQL | 写入频繁,由会话身份和数据库约束控制 |
| 头像文件 | S3 | 适合保存图片,后端限制上传路径与大小 |
# 为什么叫半中心化,而不是去中心化教育平台?
建议回答:因为付费与证书等关键状态由链上合约公开验证,但课程内容、评论、完成状态接口、Relayer 和云基础设施仍由平台运营。把它说成完全去中心化会掩盖后端、issuer 和 owner 的信任假设。
# 为什么这个项目需要区块链?
建议回答:区块链真正增加价值的地方是付款关系和证书可验证性,而不是把普通 CRUD 搬上链。用户可以独立验证自己是否购买、证书是否由指定合约和 issuer 发行;如果这些价值不足以抵消 gas、延迟和复杂度,就应该保留 Web2 方案。
学生 老师 Owner
│
▼
Next.js 前端 ── tRPC ── Hono 后端 ── Drizzle ── PostgreSQL
│ │
│ viem ├── S3 头像
▼ └── Pinata IPFS
Ethereum Sepolia
├── YDToken
├── CoursePlatform
├── CompletionRequest ── 事件 ── KMS Relayer ── CourseCompletionNFT
└── Uniswap V2 Router ── YD WETH 与 YD USDC Pair
2
3
4
5
6
7
8
9
10
11
12
图 1 项目架构总览 当前结业证书功能主路径为 AWS KMS Relayer
# 读图讲解顺序
先从角色和入口讲起。学生、老师和 owner 都通过 Next.js 与钱包进入系统。
再讲身份。Privy 只负责连接钱包,后端通过签名挑战证明地址所有权,better-auth 负责会话。
接着讲数据。Hono 和 tRPC 访问 PostgreSQL,也用 viem 批量读取 Sepolia 合约。
然后讲资产。YDToken、CoursePlatform、Uniswap V2 和 CourseCompletionNFT 负责支付、兑换、购买与证书。
最后讲自动化。CompletionRequest 事件把用户请求交给 Relayer 或 CRE,当前 issuer 是 KMS Relayer 地址。
学生 钱包登录 → 兑换 YD → approve → purchase → 学习与进度 → 申领证书 → NFT
老师 钱包登录 → 申请教师 → 提交课程资料 → 等待审核 → 课程上架
Owner 钱包登录 → 校验 owner → 审核申请 → listCourse → 停用或恢复课程
2
3
图 2 学生 老师 Owner 的端到端业务流程
# 我的职责怎样回答
职责应按自己实际负责的范围说明,不必把工具名称全背一遍。以下按负责完整链路组织:
我主要负责三条链路:让用户安全地换币和买课,通过钱包签名建立登录身份,以及核验学习完成情况后自动发证。对应实现包括合约权限、前后端接口、KMS 签名适配和部署排障。标准代币、钱包连接等能力复用成熟库,我重点处理授权、重复请求、异步状态和失败恢复这些容易出错的地方。
# 第二部分 区块链基础与智能合约
这一部分先记住
合约保存的是会影响资产和权限的事实。Solidity 负责写规则,OpenZeppelin 提供标准实现,Foundry 负责测试和部署;使用成熟库能够减少重复造轮子,但业务权限、金额和状态机仍要由项目自己保证。
# 必须先掌握的以太坊基础
| 概念 | 用项目理解 |
|---|---|
| EOA(Externally Owned Account,外部账户) | 由私钥控制的钱包,例如学生钱包和 KMS 发证地址;合约账户则执行部署好的代码 |
| 交易 | 向链上提交一次操作,例如授权或购课;需要签名、手续费,并等待执行结果 |
| eth_call(只读调用) | 向节点查询当前状态,例如余额和是否买过课程;不发链上交易,不支付链上 gas |
| ABI(Application Binary Interface,应用二进制接口) | 合约的调用说明:有哪些函数、参数是什么。viem 用它编码请求、解码结果 |
| 事件日志(event log) | 合约留下的操作记录,例如谁买了哪门课;后端可以查询,但合约不能直接拿过去的日志做权限判断 |
| 回执(receipt)与回滚(revert) | 回执记录交易是否成功;回滚会撤销本次交易的状态修改,但已经消耗的手续费不退 |
| 确认与重组(reorg) | 最新区块可能被替换,所以重要操作要等后续区块确认,不能把刚查到的事件当成绝不会变化 |
# readContract 和 writeContract 的区别是什么?
建议回答:readContract 像查账,例如查询是否买过课程,不发送链上交易;writeContract 像提交一笔操作,例如授权或购课,需要签名并支付手续费。拿到交易哈希只说明已提交,必须等回执才能知道这次操作是否成功。
# 为什么事件适合做历史和触发,不适合做合约内权限判断?
建议回答:事件像操作日志,后端可以用它展示购买历史或触发发证,但合约不能直接查询自己过去的日志。所以判断某人是否买过课,要读 purchased 购买记录,不能说 “以前发过购买事件就行” 。
# Solidity
本项目用 Solidity 0.8.24 编写代币、购课、结业证书、申领入口和 CRE Receiver 合约。基础能力见 Aladdin:Solidity。
- 购买记录:用 mapping 查询用户是否买过课程,同时维护 purchasedCourseIds 和 userCourses 数组,支持列出已购课程。
- 金额与事件:课程价格按 YD 最小单位存成 uint256;购买事件按用户和课程 ID 建索引,方便链下查询。
- 外部调用:扣款、NFT 安全铸造和 Receiver 调用都可能失败或触发回调,因此要检查权限、先更新状态,再调用外部合约。
合约没有使用升级代理,结构更简单,但重大逻辑修复需要重新部署和迁移;owner 与 issuer 仍是需要保护的管理和发行权限。
# mapping 为什么不能返回所有 key?
建议回答:mapping 像一本只能按名字查找的字典:给出钱包和课程 ID 能查到购买状态,但它不额外保存所有键的清单。因此要展示用户买过哪些课程,项目另外维护课程 ID 数组;大量历史查询也可以由链下事件索引来做。
# 为什么 price 用 uint256?
建议回答:代币金额按最小单位存整数,不用小数。比如 1 YD 有 18 位精度,链上保存的是 10 的 18 次方;前端用 bigint 计算,展示时再转换。uint256 与 ERC20 金额接口一致,关键是避免用 JavaScript 浮点数算钱,而不是所有 ABI 类型都只能是 uint256。
# Foundry
本项目用 Foundry 测试合约权限、购买状态、事件、失败回滚和证书发行。工具分工与选型见 Aladdin:Foundry 与 Hardhat。
- 用 vm.prank 模拟普通用户、owner 和 issuer,分别验证允许与拒绝的操作;用 expectRevert、expectEmit 检查错误和事件。
- 项目采用 OpenZeppelin v5,授权不足等场景按实际自定义错误断言,不能沿用 v4 的错误字符串。
- 部署前校验 chainId;Deploy 脚本只负责广播,Record 脚本只从成功回执生成 deployments JSON,防止把模拟地址当成真实部署。
本地 Anvil 和单元测试验证合约逻辑,Sepolia 验证真实广播、RPC 和第三方合约集成,两类结果分别记录。
# 为什么部署脚本和记录脚本要分开?
建议回答:部署会先模拟、再真实广播。若模拟时就写地址,即使后面广播失败,文件里也会留下 “已部署” 的假象。拆开后,部署脚本只提交交易,记录脚本只读取成功回执,文件才能反映真实结果。
# 如何测试 onlyOwner?
建议回答:用管理员地址调用,确认操作成功;换成普通用户再调用,确认报的是无管理员权限错误,并且状态没有改变。Foundry 的 vm.prank 用来模拟调用者,expectRevert 用来检查应该失败的路径。
# OpenZeppelin v5
本项目复用 OpenZeppelin v5 的 ERC20、ERC721、Ownable 和 ReentrancyGuard,分别提供代币、NFT、管理权限与防重入组件。库的定位和安全边界见 Aladdin:OpenZeppelin。
- YDToken:基于 ERC20 实现支付代币,增发入口受 owner 权限控制。
- CoursePlatform:用 Ownable 限制课程管理,用 ReentrancyGuard 保护购买入口;课程是否有效、是否已购买仍由业务代码检查。
- CourseCompletionNFT:基于 ERC721 实现证书,在 v5 的统一状态更新入口 _update 拦截转让和销毁,确保铸造后不可转让。
v5 的 Ownable 构造函数需要显式传入 initialOwner,错误断言也应匹配 v5 的自定义 error。依赖版本与合约代码、测试一起锁定,避免混用不同版本的示例。
# 核心合约与权限
| 合约 | 负责什么 | 关键权限与记录 |
|---|---|---|
| YDToken | 发行与转移课程支付币 YD | owner 是管理员,可增发;balances 记余额,allowances 记扣款授权额度 |
| CoursePlatform | 上架课程、收款、记录谁买过课程 | owner 管课程;purchased 记购买关系;treasury 是收款钱包 |
| CourseCompletionNFT | 给完成课程的钱包发不可转让证书 | issuer 是发证地址;hasClaimed 阻止同一钱包重复领取同一课程证书 |
| CompletionRequest | 接收用户的发证申请并留下事件 | 不直接判断是否结业;由链下服务向后端核验 |
| CRE Receiver | 接收 CRE 的执行结果,再调用证书合约 | 只接受指定转发合约和工作流的报告,并检查目标链与接收地址 |
# ERC20 与 approve purchase
approve 是授权扣款,purchase 才是实际买课。例如课程价格为 100 YD:用户先允许 CoursePlatform 最多使用 100 YD,再调用 purchase。课程合约检查上架状态和是否重复购买,通过 transferFrom 把 100 YD 从用户钱包转到 treasury 收款地址,同时记录购买关系。
这里 spender 指获准扣款的合约,allowance 指剩余授权额度。额度已经足够时,不必重复授权。
用户 → YDToken.approve:允许课程合约扣取指定数量的 YD
用户 → CoursePlatform.purchase:请求购买指定课程
课程合约 → YDToken.transferFrom:把课程费用转到收款地址
交易成功 → 链上留下购买记录;任何必要步骤失败 → 本次购买回滚
2
3
4
# 为什么不能让购买合约直接扣用户代币?
建议回答:ERC20 不允许任意合约划走用户余额。用户必须通过 approve 明确授予 spender 和额度,之后 spender 才能 transferFrom。这是资产控制权的基本边界。
# 精确授权和无限授权怎么选?
建议回答:只授权本次费用,合约最多只能扣这笔额度,风险小,但以后买课可能要再授权;无限授权省去重复操作,但合约一旦出问题,更多余额会暴露。项目支持授权选项,界面应明确告诉用户授权给谁、多少额度,不能把无限授权当成无风险便利。
# CoursePlatform 的安全与完整性
购买时要同时守住三件事:课程可以买、用户没重复买、钱确实扣成功。
- 先检查 active 上架标记和 purchased 购买记录,再写入本次购买状态,最后调用代币合约扣款。扣款失败时,购买记录也一起回滚。
- 这个顺序叫 CEI(Checks–Effects–Interactions,先检查、再更新状态、最后调用外部合约)。配合 nonReentrant 防重入锁,防止外部调用又绕回购买入口,利用旧状态重复操作。
- treasury 收款地址允许管理员更新,便于换成多签钱包;ydToken 支付币地址部署后固定,不能悄悄把支付币种换掉。下架课程不会删除用户已经买过的记录。
已知问题:setCourseActive 没检查课程是否存在。管理员若误把陌生课程 ID 设为上架,它的默认价格是零,可能出现本来没创建却能购买的课程。应单独增加 exists 存在标记;只检查价格非零会误伤合法免费课程。
# 先写 purchased 再 transferFrom 会不会钱没扣就记录购买?
建议回答:不会。EVM 交易是原子的,如果 transferFrom 返回 false 或 revert,后续 require 失败会让整笔交易回滚,前面写入的 purchased 和数组也一起恢复。先写状态还能减少外部调用重入时读到旧状态的风险。
# CoursePlatform 有什么你现在会改的地方?
建议回答:先补课程存在性检查,防止管理员误把不存在的课程上架成零价;再根据是否要支持其他代币决定使用 SafeERC20;主网上线前用多签保护管理员和收款地址。优先修能造成错误购买的具体问题,不是先堆治理组件。
# ERC721 与 ERC5192 Soulbound
这里的 NFT 是结业证书,不是可以买卖的收藏品。ERC721 负责给每张证书唯一编号并记录持有人;ERC5192 让钱包知道这张证书处于锁定状态,不能转让。Soulbound(灵魂绑定)就是绑定钱包、不能转走的凭证。
- issuer 发证地址才能 mint(铸造证书);owner 管理员只能更换 issuer,日常发证和管理权限分开。
- hasClaimed 按钱包和课程记录是否领过,防止重复发证。
- 在 OpenZeppelin v5 的统一更新入口 _update 阻止转让和销毁,也拒绝授权别人转走证书;只有首次铸造能通过。
- tokenCourse 记录证书对应哪门课;tokenURI 是证书资料的 IPFS 地址,不是把图片和说明全文存到链上。
这种绑定只能证明某个钱包持有证书,不能阻止持有人私下出售整个钱包的私钥。
# 为什么不用普通 ERC721?
建议回答:证书证明的是当前钱包完成课程。若可转让,买来的证书和学来的证书在链上无法区分,会破坏凭证语义。ERC5192 用最小接口告诉钱包和市场该 token 已锁定。
# 为什么要在 _update 拦截,而不是重写 transferFrom?
建议回答:OZ v5 所有 transferFrom 和 safeTransferFrom 路径最终都收敛到 _update。拦截底层统一入口能覆盖市场合约和所有上层调用,减少遗漏。
# 合约安全问题清单
| 风险 | 处理重点 |
|---|---|
| 外部调用绕回入口重复操作 | 先检查并更新状态,再调用外部合约,购买入口加防重入锁 |
| 重复购买或领取证书 | 合约按钱包与课程检查记录,不能只靠前端禁用按钮 |
| 非法发证或管理权限泄露 | 分开发证与管理员权限;生产用多签管理关键权限,并监控异常发证 |
| 部署到错误网络、记录假地址 | 广播前检查链 ID,记录地址时读取成功回执 |
| 管理员无限增发 YD | 明确代币总量规则,按业务需要增加上限、多签或延迟执行 |
| 不存在的课程被误上架 | 增加 exists 存在标记,不能把默认零价当成课程存在 |
权限检查回答 “谁能操作” ,业务检查回答 “这次操作是否合理” ,两者缺一不可。
# 第三部分 Uniswap V2 与课程购买
这一部分先记住
Uniswap V2 负责换币,课程合约负责卖课。用户已有足够的 YD 就直接进入购课流程;不足时,才需要先把 ETH 或 USDC 换成 YD。换币和购课是不同的交易,都要等链上确认成功,不能把钱包返回交易哈希当成已经完成。
# Uniswap V2
项目用 Uniswap V2 (opens new window) 解决用户没有 YD、却想购买课程的问题:用户交出 ETH 或 USDC,从预先存入两种代币的资金池中换出 YD,不需要等另一个用户刚好挂单出售。用资金池和公式完成兑换,就是自动做市(AMM);基础概念见 Uniswap、AMM 和 LP 的关系。
项目在 Sepolia 测试网上建立了 YD/WETH 和 YD/USDC 两个池子。WETH 是包装成 ERC20 标准的 ETH,方便池子用同一套规则处理代币;用户用 ETH 兑换时,Router 会完成这一步包装。
# Pair、Router 和 Factory 分别做什么?
| 合约 | 用本项目理解 |
|---|---|
| Pair(交易对资金池) | 真正存放两种代币、执行兑换。例如 YD/USDC 池收到 USDC,再按规则付出 YD。提供资金的人拿到 LP 代币,作为池中份额的凭证 |
| Router(兑换入口) | 前端主要调用它:先查询能换多少 YD,再提交兑换。它负责把用户的资金送入相应池子,也能串联多个池子完成兑换 |
| Factory(资金池工厂) | 创建和查找 Pair,例如找到 YD/USDC 对应的资金池地址 |
# 项目为什么接入 Uniswap V2,具体怎么用?
建议回答:课程只接受 YD,但用户可能只有 ETH 或 USDC,所以我接入 Uniswap V2,让用户先换币再买课。这样不用自己开发一套撮合买卖的交易系统。前端先查询预计能换到多少 YD,让用户确认最低接收数量,再发起兑换;等兑换成功后,才继续授权课程合约扣取 YD 并购买。选择 V2 是因为它的资金池和定价机制比较简单,适合验证这条业务链路;并不是说它一定能提供市场最优价格。
对应调用:getAmountsOut 查询预计输出;swapExactETHForTokens 用 ETH 换 YD;swapExactTokensForTokens 用 USDC 换 YD。
# 为什么 USDC 要先 approve Router?
建议回答:Router 不能随意动用用户钱包里的 USDC,必须先获得用户授权的扣款额度,也就是 allowance;额度不足时才需要调用 approve。用 ETH 兑换则是把 ETH 随交易一起交给 Router,不需要这一步。还要区分两次授权:换币时授权 Router 使用 USDC,买课时授权 CoursePlatform 使用 YD,用途和收款合约都不同。
# 报价成功为什么交易仍可能失败?
建议回答:报价只是查询当时能换多少,不会锁定价格。比如页面预计能换到 100 YD,用户要求至少收到 99 YD,但等待交易确认时别人先换了币,导致现在只能换到 98 YD,合约就会拒绝兑换。除此之外,余额或授权不足、交易过期、手续费不足也可能导致失败。
# 为什么不用前端自己算报价?
建议回答:不是不能自己算,而是直接调用 Router 的报价方法更省事,也不容易算错手续费或兑换路径。它会根据池中最新读取到的代币数量计算预计输出;前端负责展示和设置保护条件,但这个报价仍不保证最终成交结果。
相关代码:apps/web/src/hooks/use-swap-quote.ts、use-eth-to-yd-swap.ts、use-usdc-to-yd-swap.ts;建池和添加资金的脚本位于 packages/contracts/script/AddUniswapV2*.s.sol。
# 恒定乘积举例
池子不是按固定单价卖 YD:换走的 YD 越多,剩下的越少,继续购买就越贵。V2 用 x × y = k 表达这个规则,x、y 是池中两种代币的数量;忽略手续费时,兑换前后两者的乘积保持不变。
例如池里有 10,000 YD 和 100 USDC,乘积是 1,000,000。投入 1 USDC 后:
- USDC 变成 101,池里需要留下
1,000,000 ÷ 101 ≈ 9,900.99 YD。 - 因此用户拿到的是
10,000 − 9,900.99 ≈ 99.01 YD,不是按最初比例换到整整 100 YD。
这就是价格影响(price impact):自己的交易改变了池内比例,买得越多、池子越小,平均成交价越差。实际 V2 兑换还收取输入金额的 0.3% 作为手续费,预计收到的 YD 会更少;手续费留在池中,因此实际乘积会增长,并非永远不变。
# 滑点容忍度 1% 是什么意思?
建议回答:如果页面报价是 100 YD,设置 1% 就代表至少接受 99 YD,前端把 99 写入最低接收数量 amountOutMin。实际只能换到 98 YD 时,合约会让兑换失败,而不是让用户吃下这个差价;链上失败的交易仍需付 gas。价格影响已经体现在报价里,滑点容忍度限制的是成交结果还能比这个报价差多少,不是额外手续费。
# 什么是夹子攻击,这个项目如何降低风险?
建议回答:攻击者看到用户准备买 YD,就抢先买入推高价格,等用户买完再卖出获利,相当于把用户的交易夹在中间。最低接收数量能限制用户接受多差的结果;deadline 是交易有效期,防止旧交易隔很久才执行,但它不能阻止有效期内的夹子攻击。更深的资金池和受保护的交易提交渠道能进一步降低风险;项目的 Sepolia 小额测试只能验证兑换流程,不能据此证明主网交易安全。
# 完整购课状态机
这里的状态机就是把购买拆成几个明确阶段,而不是点击后一直转圈。下表从用户已有足够 YD 开始;不足时先完成前面的换币流程。
| 阶段 | 要做什么 | 失败或等待时怎么处理 |
|---|---|---|
| 购买前检查 | 读取价格、余额和扣款额度 | 网络或余额不满足时先提示,不急着发送交易 |
| 授权 | 额度不足才调用 approve | 用户拒签就停止,不自动重复弹出钱包 |
| 等待授权确认 | 查授权交易回执,再继续购买 | 保留交易哈希;暂时查不到结果不等于失败 |
| 提交购买 | 调用 CoursePlatform.purchase | 区分用户取消、网络错误和合约拒绝 |
| 确认购买成功 | 检查回执,并刷新购买状态 | 结果未知时先核对原交易,不盲目再付一次 |
# 为什么不能把 approve 和 purchase 当成一个 loading?
建议回答:授权和购买是两笔交易。用户可能已经授权成功,却拒绝了购买;也可能已提交购买,只是还没确认。界面分开显示 “等钱包签名” 和 “等链上确认” ,用户才知道卡在哪一步,也不容易误点重发。
# 用户切换钱包时旧请求怎么办?
建议回答:给请求记下发起时的钱包、课程和版本号。用户切换后,旧结果返回时版本不匹配,就不能覆盖新界面。已经广播的交易无法靠切换钱包取消,仍应按原钱包查询结果,不能把它算到新钱包头上。
# 第四部分 前端、后端、认证与数据库
这一部分先记住
前端负责钱包交互和状态展示,Hono 与 tRPC 负责服务端接口,PostgreSQL 保存高频业务数据。钱包地址只是身份标识,只有完成 nonce 签名验证并建立服务端 Session 后,才能作为已登录用户访问受保护资源。
# TypeScript
Web、API、Relayer、CRE Workflow、CDK 和共享包统一使用 TypeScript。基础原理见 Aladdin:TypeScript 与 Zod,本项目重点处理三类数据边界:
- 接口类型:后端导出 AppRouter 类型,前端通过 tRPC 获得输入输出提示,接口变更能在编译时暴露。
- 链上金额:uint256 用 bigint 计算,通过 API 返回时转成十进制字符串,避免浮点误差和 JSON 序列化失败。
- 外部输入:请求、环境变量和 Workflow 配置仍需运行时校验;Address、Hex 等类型不能代替地址合法性和签名验证。
# Next.js 16 与 React 19
本项目使用 Next.js 16 与 React 19 构建课程、购买、兑换、证书和后台审核页面。组件与渲染原理见 Aladdin:React 和 Next.js;两个项目的 Next.js 主版本不同,不能照搬具体配置。
- 页面与交互分开:不依赖浏览器状态的内容可在服务端渲染,钱包连接、签名和交易操作放在 Client Component。
- 交易按阶段展示:swap、purchase、claim 分别由 Hook 管理,区分钱包确认、链上确认、成功、失败和结果暂时未知。
- 钱包切换隔离:用 generation 计数让旧异步结果失效,防止 A 钱包的余额或交易结果覆盖 B 钱包的界面。
当前交易历史仍依赖 RPC 查询;访问量增加后,需要引入事件索引或服务端缓存,而不是让每个页面重复扫描链上日志。
# 早期 Vercel 阶段为什么需要同源代理?
建议回答:当时 Vercel 页面是 HTTPS,而早期 ALB 只有 HTTP。浏览器直连会触发 mixed content,Secure cookie 也无法正确建立。Next rewrite 让浏览器只访问同源 HTTPS,再由服务端转发到 ALB,这是迁移前的过渡方案。
# 最终怎样解决这条明文链路?
建议回答:给公网入口和 ALB 源站都配置 HTTPS,保证用户到入口、入口到源站的传输加密;数据库连接也单独启用 TLS 和证书验证。同源代理只解决浏览器访问方式,不会自动把代理到后端的 HTTP 变成 HTTPS。
# Tailwind CSS v4
项目用 Tailwind 组织课程卡片、购买按钮、表单和手机端布局。好处是样式和交互状态放在组件旁边,改一个按钮不用来回查找多份 CSS。颜色、字号和间距集中在 globals.css 的主题变量中,重复界面抽成组件。
两个容易踩的点:动态颜色要映射成完整类名,否则构建时可能找不到对应样式;类名只能控制外观,按钮是否禁用、键盘能否操作仍要由组件逻辑保证。代价是 JSX 会变长,所以不能到处复制几十个类名。
本项目使用 v4 的 @import "tailwindcss" 与 @theme 配置,相关代码在 apps/web/src/app/globals.css。
# Tailwind 和 CSS Modules 怎么选?
建议回答:Tailwind 适合大量组件共享统一 token、状态和响应式规则;CSS Modules 更适合复杂且高度定制的局部样式。两者并不互斥,但当前项目以 utility class 为主,避免同时维护两套主要样式范式。
# Tailwind 是否会让最终 CSS 很大?
建议回答:生产构建只生成扫描到的 utility,通常不会把全部类输出。真正需要警惕的是动态 class 无法被扫描导致样式缺失,以及无约束复制 class 导致维护成本上升。
# viem
本项目用 viem 统一前端交易、后端读链和 Relayer 发证。客户端与钱包层的区别见 Aladdin:wagmi、viem 与 SIWE。
- 前端从 Privy 当前钱包创建 WalletClient,用于授权、兑换和购课;PublicClient 读取余额、额度,并等待交易回执。
- 后端用 readContract 和 multicall 查询课程与购买权限,不把浏览器传来的状态当作事实。
- Relayer 按区块范围读取申领事件,用自定义 KMS Account 签署 mint 交易;日志区间过大时分片查询。
项目没有再叠加 wagmi,而是用 Privy 管理钱包连接、用自定义 Hook 管理交易流程。这样控制直接,但缓存、去重和 React 状态需要自己维护。
# 为什么交易历史不用数据库?
建议回答:当前先直接查询链上事件,减少维护一份同步表的工作。缺点是历史越多、查询越复杂,RPC 越慢;需要扩展时,再由后台统一同步事件到数据库供页面查询。数据库能当索引,但必须记录确认进度并处理链重组,不能另造一套购买事实。
# Privy
项目用 Privy 统一处理连接钱包、选择账户和账户切换,避免分别适配每一种浏览器钱包。当前只开放钱包登录,不使用它的其他登录方式。
Privy 解决 “网页连接哪个钱包” ,后端签名验证解决 “请求者是否真的控制这个钱包” 。前端从 useActiveWallet 取得当前钱包,再交给 viem 发送交易;不能随便取钱包列表第一项,否则用户切换账户后可能操作错钱包。
断开时还要退出后端会话。只断开钱包连接,不会自动删除服务器发出的登录 Cookie。相关代码在 use-wallet-client.ts、use-wallet-sign-in.ts 和 wallet-auth-provider.tsx。
# 为什么连接钱包后还要再签名?
建议回答:连接钱包只让网页知道地址,后端不能凭一串地址认定你就是本人。后端发一次性消息,让钱包签名并验证,才能确认请求者控制这个地址;之后再建立登录会话。
# 钱包签名认证完整链路
登录不是把地址发给后端,而是签一条后端能验证、只能使用一次的消息。nonce 是本次登录的随机验证码,和链上交易的 nonce 序号不是同一种用途。
1. 后端生成 nonce,保存对应地址和过期时间
2. 前端把站点域名、地址、nonce、签发和过期时间组成登录消息
3. 用户用当前钱包签名;私钥始终留在钱包里
4. 后端核验签名、域名和时间,确认消息属于本站的这次登录
5. 数据库将 nonce 标记为已使用,只有第一次成功消费的请求可以继续
6. 查找或创建用户,交给 better-auth 建立会话
7. 浏览器保存 HttpOnly Cookie,后续请求通过它携带登录身份
2
3
4
5
6
7
# better-auth
钱包签名解决一次身份验证,better-auth 负责记住登录状态。否则用户每次发表评论、保存学习进度都得重新签名。
本项目的 wallet-sign-in 插件验证钱包签名后,让 better-auth 创建用户和 Session(服务端会话),并写入登录 Cookie。退出时销毁会话;教师身份、昵称等可能变化的字段仍实时查数据库,不依赖缓存中的旧值。
Cookie 的 HttpOnly 禁止网页脚本读取它,Secure 要求经 HTTPS 发送,SameSite=Lax 限制部分跨站携带场景。这些配置各管一层,不能代替后端权限判断。
实现细节:Cookie 会话缓存为五分钟;Drizzle 适配器的表名映射要和 modelName 一致,否则会找不到表。本项目是 SIWE(Sign-In with Ethereum,以太坊签名登录)风格的自定义流程,不等于完整实现了 EIP-4361 的全部要求。
相关代码:apps/server/src/lib/auth.ts、auth-plugins/wallet-sign-in.ts、siwe-message.ts。
# HttpOnly 能防什么?
建议回答:主要防止前端 JavaScript 读取 session cookie,从而降低 XSS 直接窃取会话的风险;它不能防止 XSS 代替用户发请求,也不能单独解决 CSRF。
# SameSite Lax 的作用?
建议回答:它让浏览器在不少跨站请求中不自动携带登录 Cookie,降低恶意网站借用户身份发请求的风险。但它不是所有跨站请求都拦,也不能单独防住 CSRF 跨站请求伪造;写操作仍要检查请求来源或使用防伪令牌。
# nonce 防重放为什么要原子消费
假设同一份登录签名同时发来两次:如果两个请求都先查到 nonce 还没用,再分别标记为已用,就会全部登录成功。
解决办法是把 “检查没用过” 和 “标记已使用” 放在同一次数据库更新里,只有一个请求能抢到。这就叫原子消费。签名验证和过期检查应先通过,下面只展示阻止重复使用的关键 SQL:
-- $1 是本次已验证登录消息对应的 nonce 记录 ID。
UPDATE auth_nonce
SET used = true
WHERE id = $1 AND used = false
RETURNING id; -- 返回一行才允许创建会话;没有返回说明已被消费。
2
3
4
5
# 签名正确但 nonce 消费失败怎么办?
建议回答:拒绝这次登录,让客户端获取新 nonce 重新签名。签名正确说明消息由该钱包签过,但这个一次性凭证已经被别的请求用掉,不能再创建会话。
# 为什么 nonce 还要过期?
建议回答:一次性只能防止 “用过再用” ,过期时间还能防止一份没用过的旧签名长期有效。两者配合,既限制使用次数,也限制能登录的时间窗口。
# Hono
本项目把 Hono 作为 HTTP 入口,在常驻 Node 服务中挂载健康检查、better-auth、tRPC 和供 Relayer、CRE 使用的 REST 接口。框架与中间件原理见 Aladdin:Hono。
Web 前端通过 tRPC 调用类型化业务接口;Relayer 和 CRE 通过标准 REST 查询结业状态与课程信息,不依赖前端的 AppRouter 类型。Hono 负责接收请求和组织中间件,身份校验、数据库事务和业务权限由对应服务处理。
错误响应保留可识别的状态码,但不暴露 RPC URL、数据库错误详情或完整堆栈。
# 为什么 Hono 和 tRPC 同时用?
建议回答:它们不是重复的两个后端。Hono 负责接收 HTTP 请求、挂载路由和中间件;tRPC 在其中提供前后端共享类型的业务接口。页面用 tRPC,Relayer 和 CRE 用普通 REST,最终都由 Hono 接收。
# tRPC v11
tRPC 让前后端共享接口类型,减少 “后端改了字段,前端还按旧格式调用” 的问题。前端获得的是调用提示与类型检查,网络请求仍然会发送到后端,不能靠 TypeScript 阻止恶意请求。
项目按课程、评论、学习进度等业务拆分接口。公开接口不要求登录;protectedProcedure 要求登录;teacherProcedure 重新查教师资格;ownerProcedure 核对链上管理员地址。用户身份从服务端会话取得,不能相信请求里自行填写的 userId。
输入格式由 Zod 校验。课程列表用 multicall 一次批量读取多门课的链上状态,再按 courseId 拼上数据库中的标题和简介;金额转成字符串返回,避免 JSON 无法表示 bigint。
Web 前后端都是 TypeScript,适合共享类型;Relayer 和 CRE 则使用普通 REST 接口,不必依赖整个前端类型体系。相关代码在 apps/server/src/trpc。
# tRPC 是否等于没有 API 文档?
建议回答:不是。它能告诉前端字段类型,却不能完整说明 “谁能审核课程” “失败后能否重试” 。这些业务规则仍要写清楚。若接口开放给其他语言的客户端,再提供 REST/OpenAPI 会更通用。
# 前端隐藏按钮能否代替 ownerProcedure?
建议回答:不能。别人可以不经过页面,直接发审核请求。后端必须拿会话里的钱包地址与链上 owner 比较,一致才允许操作;隐藏按钮只是避免普通用户误操作。
# Zod
本项目用 Zod 校验 tRPC 输入、Web/API 环境变量、Relayer 和 CRE 配置。Schema、parse 与 safeParse 的原理见 Aladdin:Zod。
- 服务启动:提前检查 RPC 地址等必要配置,避免运行到 new URL() 才因错误配置返回 500。
- 请求入口:tRPC 的 input 校验字段形状;用户身份和资源归属仍由 Session 与权限中间件判断。
- CRE 运行时:不能直接复用依赖 Node 或浏览器 URL 全局对象的校验,需使用经过 WASM 模拟验证的规则。
结构校验通过不等于业务合法。例如 courseId 类型正确,还必须确认课程存在、已上架且用户有相应权限。
# Zod 校验通过是否就能直接写数据库?
建议回答:不能。它能确认 courseId 是合法格式,不能证明这门课存在,更不能证明用户有权修改。还要查身份、课程归属与业务状态,最后用数据库约束防止并发写入破坏数据。
# PostgreSQL 与 Drizzle ORM
本项目用 PostgreSQL 保存用户、会话、nonce、课程资料、评论、学习进度和审核申请。事务、约束与并发基础见 Aladdin:PostgreSQL。Drizzle 用 TypeScript 声明表结构和构建 SQL,drizzle-kit 管理迁移。
- 链上链下关联:courseId 直接使用链上课程 ID;价格、余额和购买关系以链上状态为准,不在数据库再维护一套权威记录。
- 唯一性与状态:course_progress 用 userId 与 courseId 组成复合主键;申请状态限制在合法取值内,避免并发产生重复行或非法状态。
- 迁移与环境隔离:已执行的迁移不改历史;跨环境迁移业务关系时按 walletAddress 重建 userId,不直接复制会话、nonce 或随机主键。
直接读链减少了购买状态同步问题,但会增加 RPC 延迟;规模扩大后可引入索引与缓存,同时明确确认区块和重组处理规则。
# 为什么 courseId 用自然键?
建议回答:链上已经有课程 ID,数据库直接沿用,就能把链上价格与链下标题对应起来,不用再维护一套 ID 映射。如果以后支持多条链或多份课程合约,就要把链和合约地址也加入标识,不能假定 courseId 在所有地方都唯一。
# 为什么不用数据库缓存购买状态?
建议回答:当前直接读合约判断是否购买,少一套同步逻辑,也避免用户刚买完、缓存还没更新的问题。缓存并非不能用,但引入后要明确更新时间、确认区块和失败处理,不能用一个旧值无条件决定付费访问权限。
# 什么时候需要数据库事务?
建议回答:例如批准教师申请既要更新申请状态,又要赋予教师身份,两步必须一起成功或一起撤销,适合数据库事务。但链上交易和数据库不能装进同一个普通事务,必须分别确认结果,并给失败的一边提供重试或补偿。
# S3 头像预签名 POST
后端发一张短时有效、限制了上传位置和大小的 “上传许可” ,浏览器拿它直接把头像传给 S3。这样图片不必经过 API 服务器转发,减少带宽和内存占用。
这张许可就是预签名 POST:后端签署上传条件,S3 检查对象路径、有效期、文件大小和声明的内容类型。不符合条件就拒绝上传;声明是图片不代表文件内容一定安全,必要时仍需检查实际内容。
# 为什么不用预签名 PUT?
建议回答:项目需要限制头像大小和上传路径。预签名 POST 能把允许的大小区间、路径等写进上传条件,让 S3 执行检查,更贴合这次需求。不是 PUT 不能上传,而是不能照搬 POST 的条件策略。
# 上传后为什么还要校验 URL?
建议回答:上传许可限制了往哪里传文件,却不限制客户端更新头像资料时随便填一个地址。所以后端还要确认提交的 URL 确实属于规定存储位置和该用户,不能让用户冒用别人的文件或写入恶意地址。
# 第五部分 结业证书自动化 Chainlink CRE 与 AWS KMS Relayer
这一部分先记住
合约不能直接读取数据库中的学习进度,所以需要链下执行者把 “申领事件、完成状态、metadata 和 mint 交易” 串起来。当前生产路径是 KMS Relayer,CRE 是已经完成隔离验证、但尚未正式接管生产发行的候选路径。
# 为什么需要链下执行层
数据库显示学完了,不会自动让链上发证。合约既不能主动查业务数据库,也不会自己定时运行,因此需要 Relayer 这个后台服务串起两边:
学生申领 → CompletionRequest 合约留下事件
Relayer 读到事件 → 向后端核验是否完成课程
完成 → 生成证书名称、图片和课程信息 → 保存到 IPFS
调用 KMS 签名 → 提交 mint 发证交易 → 等回执确认成功
2
3
4
这里 metadata 指证书的名称、图片地址等资料,mint 指在链上创建证书。后端决定是否学完,issuer 决定谁有权发证,两者都需要保护,不能因为用了区块链就省掉核验。
# AWS KMS Relayer
Relayer 负责干活,KMS 负责保管私钥并签名。Relayer 长期运行,查询申领事件、核验完成状态、生成证书资料,再调用固定 NFT 合约发证。KMS 中的私钥不能导出到应用,避免服务器配置文件泄露时连私钥一起丢失。
需要掌握两个不同的问题:
- 怎样发出合法交易:KMS 返回的是通用密码学签名,不是完整以太坊交易。适配器解析公钥得到发证地址,把签名转换成以太坊格式,再交给 viem 编码和广播。DER 是 KMS 使用的二进制编码;r、s 是签名的两个数;yParity 是恢复签名地址时需要的标记。
- 怎样避免漏发和重复发:按 fromBlock 到 toBlock 查询事件,记录处理到哪里;重启后补扫遗漏区间。重复领取错误正常跳过,合约 hasClaimed 保证不能多发证书。
生产方案是把 checkpoint(扫描进度)存到 RDS:例如已经完成到第 1,000 个区块,重启后从这里附近补扫,而不是直接跳到最新区块。只扫描达到设定确认数的区块;一批处理完成后才推进进度,失败项要可重试。多实例再用 lease(有过期时间的处理资格)选出当前消费者,减少重复提交和交易序号冲突。
允许重复扫描,但同一课程只能发一张证书。持久进度解决漏处理,hasClaimed 解决重复效果,不能把两者混为一谈。KMS 也不检查业务内容,应用仍要限定目标合约和 mint 方法,并用 IAM 限制哪些服务能请求签名。
相关代码:apps/relayer/src/kms-account.ts、listener.ts、completion-handler.ts、mint-executor.ts。
# KMS 为什么比环境变量私钥安全?
建议回答:环境变量里的私钥一旦被复制,攻击者离开服务器也能继续使用;KMS 私钥不能导出,应用只能请求签名,还能按角色限制和审计调用。但应用若被攻破,仍可能滥用它已有的签名权限,所以交易范围和权限限制也必须做。
# KMS 是否让系统去中心化?
建议回答:不会。它让私钥更难泄露,但发证仍由平台服务和指定地址控制。密钥安全和去中心化是两件事,换成 KMS 不会让学习完成判定自动可信。
# checkpoint 具体存在哪里?
建议回答:生产方案把 checkpoint 放在 RDS,记录哪个消费者、哪条链、哪个合约已经处理到哪个区块。容器重启后从这条记录继续,而不是依赖本地内存。多实例通过条件更新和租约协调,避免同时推进同一份进度。
# 为什么不用 S3 保存 checkpoint?
建议回答:可以设计成存 S3,但多实例同时更新时还得处理竞争和版本冲突。项目已经使用 PostgreSQL,扫描进度和实例租约放在那里更容易通过条件更新协调,所以优先复用 RDS,而不是说 S3 完全做不到。
# 什么时候更新 checkpoint?
建议回答:一批事件都处理完成,或失败项已可靠保存并会继续重试时,才推进进度。不能先记 “完成” 再发证,否则中途崩溃就可能永久跳过申请。重复扫到已发证记录时,核对合约后正常跳过。
# 为什么要低 s?
建议回答:s 是签名中的一个数,同一份签名可能有两种等效表示。以太坊交易要求采用较小的那一种,称为低 s。KMS 不保证直接返回这种形式,所以适配器要规范化,否则签名在数学上有效,交易仍可能被拒绝。
# 为什么 recovery bit 可以试出来?
建议回答:KMS 给了签名,但没给恢复地址需要的那一位标记。适配器分别尝试 0 和 1,恢复出地址后与 KMS 公钥对应的已知地址比较;匹配才接受,都不匹配就报错,不能随便选一个。
# 事件监听为什么不用 watchContractEvent filter
项目遇到过服务还活着、却收不到申领事件的情况,日志提示 filter not found。filter 是 RPC 节点临时保存的订阅编号;节点切换或编号失效后,继续拿旧编号查询就会失败。
因此改成每次明确询问 “第几块到第几块有哪些事件” ,用 getContractEvents 查询区块范围,不依赖节点替我们保存订阅状态。这个修改解决监听失效,不会自动解决重启漏事件;停机补扫还需要上一节的持久化进度方案。
# 无状态轮询还有什么问题?
建议回答:不依赖 RPC 保存订阅编号,不代表服务自己不用保存进度。还要解决查询区间过大、请求限流、停机补扫和链重组。生产方案是分批查询、失败退避重试、持久化进度,并留出确认与回退扫描窗口。
# 为什么链上 hasClaimed 仍然必要?
建议回答:即使后台去过重,重启、并发或其他调用入口仍可能再次请求发证。合约按钱包和课程检查 hasClaimed,能让所有入口都遵守 “只能领一次” 。后台去重节省费用,合约检查守住最终规则。
# Chainlink CRE
CRE(Chainlink Runtime Environment,Chainlink 运行环境)是另一种自动发证方式:把 “监听事件、请求后端、提交发证结果” 的工作流交给 Chainlink 执行网络,而不是只由自己的 Relayer 进程运行。
本项目的流程仍然是同一条:收到申领事件 → 查询后端是否学完 → 上传证书资料 → 生成报告 → 交给链上 Receiver 接收合约 → 调用 NFT 发证。两套方案共用 completion-nft-shared,避免对是否结业给出不同判断。
- DON:Decentralized Oracle Network,去中心化预言机网络,参与工作流执行和报告生成。
- report:报告里写明给哪个钱包、哪门课发证,以及证书资料地址。
- Forwarder 与 Receiver:前者是报告转发合约,后者是本项目接收报告并发证的合约。Receiver 还检查 workflowId 工作流编号、目标链和自身地址,不能谁提交报告都照单执行。
CRE 已有隔离模拟和测试网验证记录,但生产部署权限 Deploy Access 尚未开通,未替代现有 KMS Relayer。多个节点执行并不代表数据源自动真实:是否学完仍来自平台后端。
相关代码:apps/cre-workflow、packages/completion-nft-shared、CourseCompletionNFTReceiver.sol。
# CRE 和 Chainlink Functions 有什么区别?
建议回答:Functions 主要是合约发起请求,让链下执行计算后把结果回传;CRE 更适合描述一整条由事件触发、调用外部接口、再写回链上的工作流。本项目需要申领到发证的多步流程,因此采用 CRE 方案做验证。
# 为什么 Receiver 不能直接信任任意 onReport?
建议回答:否则任何人都能提交一份 “给我发证” 的报告。接收合约先检查报告是否由指定转发合约送来,再检查工作流编号、目标链和接收地址。验证来源和用途之后,才能执行发证。
# CRE 已跑 simulate 为什么不直接切换?
建议回答:模拟环境里的临时合约和模拟转发器跑通,只证明流程可以工作,不证明生产工作流已获准部署并能执行。如果先把发证权限切过去,真正的工作流却没运行,用户就会领不到证书,所以先完成生产部署验证,再切权限。
# CRE WASM 兼容性难点
本地 TypeScript 测试通过,不代表 CRE 中能运行,因为执行环境不同。CRE 将代码编译为 WASM(WebAssembly,一种可移植执行格式),不能假定拥有 Node.js 的全部 API。
本项目排查过三个差异:URL 全局对象不可用时,原来的 URL 校验会误报;HTTP 超时时长要写成 10s 这种格式,GET 请求必须省略 body 字段;事件 topics 的第一项是事件类型标识,真正的索引参数从后一项开始。
处理方式是把这些转换集中到适配层,并用真实 cre workflow simulate 验证完整流程。单元测试检查逻辑,实际模拟检查运行环境,二者不能替代。
# Pinata 与 IPFS
证书合约保存资料地址,图片和文字放在链外。IPFS 按内容生成 CID(Content Identifier,内容标识),Pinata 则帮项目持续保存这些文件并提供访问服务,避免把大量图片和文字写入昂贵的链上存储。
发证时,项目把名称、图片和课程信息组成 metadata,上传后把 ipfs://... 地址写入 tokenURI。内容变了,CID 通常也会变;当前合约没有修改 tokenURI 的入口,因此已发证书一直引用同一份资料。
需要分清:CID 能验证拿到的文件有没有变,不能证明其中的成绩是真的,也不保证文件永远有人保存。持续保留文件叫 pin;访问网关坏了可以换网关,但若所有节点都丢失文件,单凭 CID 找不回内容。因此要备份原文件,必要时由多个服务保存;Pinata 上传凭据只放后端。
相关代码:packages/completion-nft-shared/src/ipfs/pinata.ts、metadata.ts。
# IPFS 是不是把文件存到链上?
建议回答:不是。CID 上链,文件存储在 IPFS 网络节点或 Pinata pinning 服务。链上只保存可验证的内容地址。
# 如果 Pinata 挂了怎么办?
建议回答:先区分是访问网关故障,还是文件没有任何可用副本。前者可以换网关;后者必须用保留的原文件重新提供存储,CID 本身不能变回文件。所以应备份资料,重要证书由多个节点或服务持续保存。
# 两条发行路径对比
| 维度 | KMS Relayer | Chainlink CRE |
|---|---|---|
| 谁执行流程 | 自己运营的 Node 后台服务 | Chainlink 执行网络中的工作流 |
| 谁有权发证 | KMS 私钥对应的 issuer 地址 | 验证报告后的 Receiver 合约 |
| 优点 | 调试直接,能控制部署和重试,私钥不可导出 | 减少对单个自建执行进程的依赖,报告来源可验证 |
| 代价 | 自己负责运行、进度恢复和签名权限保护 | 需要部署权限,适配运行环境与报告校验 |
| 共同限制 | 是否结业仍由平台后端判断 | 多节点执行也不能自动证明后端数据真实 |
两条路径共用结业规则和证书资料生成逻辑,合约统一阻止重复发证;切换前验证新路径,不同时启用两套发行权限。
# 第六部分 AWS 基础设施与部署
这一部分先记住
AWS 部署不是把所有代码塞进一个服务,而是按运行特点选择载体:Next.js、Hono 和长期运行的 Relayer 使用 ECS Fargate,RDS 保存业务数据和 checkpoint,KMS 管签名密钥,CloudFront、WAF 和 ALB 负责入口与安全。核心是让请求进得来、数据不乱、失败能发现并恢复。
# 生产部署选型结论 AWS 还是 Cloudflare
选择 AWS 的主要原因不是产品更多,而是 API、数据库和 KMS 发证服务可以放在同一套网络与权限体系里。当前 Node 服务能直接打包成容器,不必为了迁移平台重写长期运行的 Relayer。
| 真正影响选型的因素 | 本项目的取舍 |
|---|---|
| 运行方式 | Web 和 API 接收请求,Relayer 持续扫描事件;ECS 可以分别运行三个常驻服务 |
| 数据与密钥 | PostgreSQL、对象存储和 KMS 都在 AWS,放在一起更容易控制访问权限和排查故障 |
| 改造成本 | 迁到 Workers 需要验证 Next.js 适配、数据库连接和 Node 依赖,Relayer 也要改成适合该平台的执行方式 |
| 费用 | ECS、ALB、NAT 和 RDS 有持续费用,低流量时未必划算;开发环境应缩小规格并设置预算 |
Cloudflare 可以用于静态站点、边缘接口或接入 AWS 源站。只有访问速度或成本确实有收益时才增加跨云部署,不能只为多用一个平台而拆分系统。
# Cloudflare Pages 与 Workers 应该怎样区分
先看要不要在收到请求时执行服务端代码。能完整静态导出的页面可放 Pages;需要动态渲染和后端逻辑时,评估 Workers 的框架适配,不能把静态文件托管当成完整 Node 服务器。
本项目即使把页面迁走,PostgreSQL 和 KMS 仍可能留在 AWS。这样要额外处理跨云连接、凭据、延迟和日志,所以重点不是 “能不能跑” ,而是迁移是否值得。具体 Next.js 适配方式以文末官方指南为准,不把某条适配路径当成唯一方案。
# 生产部署拓扑
生产部署按入口、应用、数据和恢复能力分工。每个组件都对应具体问题,不是把 AWS 产品名称堆在一张图上。
| 环节 | 组件 | 在本项目中解决什么 |
|---|---|---|
| 域名与入口 | Route 53、ACM、CloudFront、WAF | 找到网站、提供 HTTPS、缓存静态资源、拦截异常请求 |
| 请求分发 | ALB | 把页面请求交给 Web,把接口请求交给 API;不给不健康实例继续分配请求 |
| 应用运行 | 三个 ECS Fargate Service | 分别运行 Next.js、Hono 和 Relayer,便于独立发布与排障 |
| 业务数据 | RDS PostgreSQL | 保存用户、课程资料和进度;生产补上多可用区、备份与恢复能力 |
| 图片资料 | S3、IPFS/Pinata | S3 保存头像;IPFS 保存证书资料,链上只引用资料地址 |
| 凭据与签名 | Secrets Manager、KMS | 前者保存数据库等凭据,后者让发证私钥不进入应用文件 |
| 发证恢复 | 持久化扫描进度、失败重试、合约防重复 | 进程重启后能补处理申请,又不会为同一课程发两张证书 |
| 发布与监控 | ECR、ECS 回滚、CloudWatch | 保存可回退的镜像,发现异常并追查是哪一段失败 |
Web、API 的生产高可用目标是跨可用区运行多个实例;Relayer 多实例则必须先解决谁处理事件、谁使用哪个交易序号,不能直接加副本就认为更可靠。
# 前端到底部署在哪里
浏览器执行交互代码,ECS 中的 Next.js 执行服务端代码;CloudFront 负责缓存和转发,不代替 Next.js 运行。纯静态站点可以放 S3,但需要收到请求后再生成页面或处理服务端操作时,仍要有应用运行环境。
生产请求链路可以这样理解:先通过 Route 53 做域名解析,再访问 CloudFront;静态资源命中缓存直接返回,动态请求经 ALB 转到 Next.js 或 Hono。DNS 只负责解析地址,不是 HTTPS 请求流经的一台代理服务器。
浏览器里的钱包签名仍在用户设备上完成,不会搬到 ECS。Relayer 是后台发证服务,也不需要向浏览器开放公网入口。
# 前端页面部署在 ECS Fargate 合理吗?
建议回答:合理,因为这里运行的不只是静态页面,还包括 Next.js 的服务端渲染和服务端操作。容器能保留现有 Node 运行方式,并与 API 统一部署。如果页面完全能在构建时生成,放静态托管通常更便宜,不必继续用 ECS。
# 既然 CloudFront 能返回页面,为什么还需要 ECS?
建议回答:CloudFront 可以直接返回缓存里已有的文件,但用户这次请求需要现算内容时,它要向源站请求。ECS 里的 Next.js 才是执行这段服务端逻辑的地方,缓存层与应用运行层不是替代关系。
# 为什么不直接把 Next.js 放在 S3?
建议回答:S3 只能托管静态对象,适合 Next.js 的静态导出结果,不能执行 Node.js、SSR、Server Actions 或服务端会话校验。当前项目存在请求时服务端逻辑,所以 S3 只负责头像和对象文件,不负责运行动态 Web 应用。
# 为什么不使用 Cloudflare Pages?
建议回答:Pages 最适合静态站点和静态导出的 Next.js。如果完整应用需要 SSR、React Server Components 或 Server Actions,应评估 Cloudflare Workers,而不是把 Pages 当作完整 Node 服务器。本项目没有选择 Pages,是因为应用不是纯静态导出。
# 为什么不使用 Cloudflare Workers?
建议回答:不是不能用,而是迁移收益暂时不明确。现有服务按 Node 容器运行,数据库和 KMS 又在 AWS;迁到 Workers 后,要重新验证运行兼容性、跨云连接与凭据管理。当前选择先把现有链路做好,若实测全球访问延迟明显受益,再做小范围试运行。
# 能不能前端放 Cloudflare,后端继续放 AWS?
建议回答:可以,静态前端配 AWS API 很常见。但要处理域名、登录 Cookie、跨域配置和两边日志;如果服务端渲染还频繁跨云查数据,也要测延迟。是否拆开由实际收益决定,不是混用云平台本身有什么不合理。
# 如果从零设计,什么情况下你会选择 Cloudflare?
建议回答:如果主要是内容展示、全球读请求和轻量无状态接口,我会优先评估 Cloudflare。若已经依赖 AWS 私网数据库、KMS 签名和常驻后台服务,统一部署 AWS 更容易维护。先看工作方式和数据位置,再选产品。
# 从 Vercel 迁移到 AWS 的实施顺序
迁移应按下面的顺序执行,避免一边切正式流量一边排基础配置问题:
- 保留旧环境和可回退版本,在 staging(预发布环境)部署网络、数据库和应用镜像。
- 先检查服务启动、数据库连接、登录、上传和 KMS 权限,再用测试域名跑通购课到发证。
- 配好 HTTPS、监控和备份,验证新版本失败时能回滚、备份能恢复。
- 切换域名流量,观察错误率和发证情况;稳定后再下线旧环境。
迁移不只搬代码,还要同步 Cookie 域名、钱包平台允许的域名和 S3 跨域配置。
# AWS 上线验收依据
页面能打开,只证明一条访问路径通了。完整验收要回答四个问题:
- 能否使用:HTTPS、登录、头像上传、购课和发证能否走完,并有交易回执或业务记录。
- 是否安全:数据库不公开暴露,凭据不写进镜像,应用只拥有必要权限,TLS 正确校验证书。
- 能否发现故障:服务启动失败、RPC 限流、发证失败是否有日志,并能通知到人。
- 能否恢复:旧镜像可回滚,数据库可从备份恢复,事件进度持久化后能补扫。
CloudFormation 资源记录、ECS 发布记录和恢复演练结果分别提供证据;配置里写了某个选项,不等于功能已在真实环境验收。
# AWS CDK
本项目用 TypeScript 声明网络、数据库、存储和应用资源,再由 CDK 生成 CloudFormation 模板交给 AWS 创建。好处是配置可以审查和复用,不必靠记忆在控制台重复点选。命令和示例见 IaC 笔记。
代码按 NetworkStack、DatabaseStack、StorageStack、BackendStack 拆分,分别管理网络、数据库、对象存储和后端。生产与开发配置应显式区分,例如副本数、数据库多可用区和删除保护,避免把开发省钱配置直接带入生产。
实际排查过的关键问题是循环依赖:数据库要引用应用的安全组,应用又要引用数据库凭据。把共享安全组放到 NetworkStack 后,两边只依赖共同的上游,不再互相等待。
cdk synth 只是生成模板,不会证明 AWS 权限、镜像启动和业务链路正常。相关代码在 packages/infra。
# 为什么拆四个 Stack?
建议回答:网络、数据库、文件存储和应用的改动频率不同。比如只升级 API,不应该连带重建数据库;分开后职责清楚,变更也容易审查。但 Stack 之间不能互相引用成环,共享安全组应放在共同依赖的网络层。
# Docker
Docker 把运行所需的代码、Node 版本和依赖打包,减少 “本机能跑,服务器不能跑” 的差异。项目的 Web、API 和 Relayer 可以各自构建镜像,再由 ECS 启动;镜像只是运行材料,不负责自动扩容和故障切换。
构建时安装依赖,运行时直接启动应用,不再临时联网装包。多阶段构建只保留必要文件;生产应固定版本、使用非 root 用户,并让程序收到 SIGTERM 停止信号后关闭连接。
项目曾遇到镜像清单与目标运行环境不兼容,以及启动时执行 pnpm 意外安装依赖导致内存不足。前者要核对 CPU 架构与发布格式,后者把依赖安装留在构建阶段;不能据此断言 Fargate 一律不支持多架构镜像。
# 镜像和容器有什么区别?
建议回答:镜像是只读的运行模板,容器是镜像的一次运行实例。ECR 保存镜像,ECS task 启动容器;多个 task 可以由同一镜像创建。
# 为什么不用 latest tag 部署?
建议回答:latest 会变化,无法证明某次部署究竟用了哪份代码,也不利于回滚。生产用版本 tag 并最终锁定 image digest,使部署产物可追踪且不可变。
# ECR
基础原理见 ECR。
CI 分别构建 Web、API 和 Relayer 镜像,推送到独立 ECR repository;CDK/ECS task definition 使用镜像 digest 发布,旧 digest 保留用于快速回滚。
ECR 与 ECS、IAM 和漏洞扫描原生集成,不需要给生产任务配置外部镜像仓库的长期密码,也减少跨平台拉取依赖。
发布约束:三类服务都锁定镜像 digest;清理仓库时保留当前及回滚版本,避免 ECS 回退时找不到镜像。
# ECR 和 ECS 有什么区别?
建议回答:ECR 存镜像,ECS 负责把镜像运行起来并维持实例数量。发布时先把构建产物推到 ECR,再让 ECS 使用这个版本;保留旧镜像是为了失败时能回退。
# CloudFront
基础原理见 CloudFront。
CloudFront 是统一公网入口,默认行为回源 ALB,静态资源使用长缓存;SSR、tRPC、better-auth 和携带 cookie 或 Authorization 的动态请求不做共享缓存,并转发必需的 header、cookie 和 query string。
它为用户提供就近连接、TLS、静态缓存、源站隐藏和 WAF 接入点,同时让 Web 与 API 仍由现有 ECS Node 运行时承载。
配置重点:带内容哈希的静态资源使用长缓存;购买状态、用户信息和 session 响应不共享缓存。CloudFront 到 ALB 使用 HTTPS,并限制绕过 CloudFront 直连源站的路径,避免跳过边缘防护。
# CloudFront 和 ALB 有什么区别?
建议回答:CloudFront 尽量在离用户近的地方返回缓存,减少到源站的请求;ALB 则把到达源站的请求分给健康的 Web 或 API 实例。一个偏全球分发,一个偏应用入口分流,项目可以把它们串起来。
# 为什么 API 不全部缓存?
建议回答:购买状态、用户信息和 session 相关响应具有身份和实时性。错误共享缓存可能把一个用户的数据返回给另一个用户,因此只对明确公开、可接受陈旧的接口设置缓存策略。
# AWS WAF
基础原理见 AWS WAF。
Web ACL 绑定 CloudFront,启用托管基础规则和 rate-based rule,对登录 nonce、公开写接口和异常扫描进行限速;先以 count 模式观察误伤,再逐步切换 block。
在请求到达 ALB 与 ECS 前过滤明显恶意流量,可以降低应用资源消耗并提供统一的边缘日志和限流控制。
nonce 接口的限速要观察真实客户端 IP 和共享出口,避免误伤正常用户;规则调整后检查日志与误拦截。WAF 不知道谁拥有钱包或课程,项目的会话、购课权限与合约校验不能省略。
# WAF 和安全组有什么区别?
建议回答:安全组管哪些来源能连哪些端口,例如只让应用连接数据库。WAF 则检查 HTTP 请求,例如有人短时间大量调用登录接口。前者管网络通路,后者管请求模式,都不替代 “谁有权审核课程” 的业务检查。
# ACM
基础原理见 ACM。
正式域名使用 ACM 证书:CloudFront 的 viewer certificate 负责用户到边缘的 HTTPS,ALB 443 listener 的区域证书负责 CloudFront 到源站的 HTTPS;ALB 80 只重定向到 443。
证书可以由 AWS 自动续期并通过 CDK 绑定,避免在容器里保存私钥、手工更新 Nginx 证书或因过期造成停机。
配置重点:项目分别配置 CloudFront 的用户侧证书和 ALB 的源站证书,保留 DNS 验证记录;RDS 的 TLS 则单独配置,不能因为公网 HTTPS 正常就认为所有内部链路都已加密。
# 为什么不把证书放进 Docker 镜像?
建议回答:镜像包含私钥会扩大泄露面并让续期必须重建镜像。使用 ACM 在托管入口终止 TLS,容器只接受来自 ALB 安全组的流量,证书轮换与应用发布解耦。
# ECS Fargate 与 ALB
ECS 管应用实例,Fargate 提供运行容器的计算资源,ALB 把请求分给正常工作的实例。原理见 ECS/Fargate 和 ALB。
本项目适合把 Web、API、Relayer 分成独立服务:页面发布不应中断发证,发证失败也不应拖垮课程浏览。Web 和 API 只接受受控入口请求;Relayer 不对公网提供接口,只主动访问节点、后端、Pinata 和 KMS。
生产 Web/API 可至少运行两个实例,发布时先启动新实例,健康检查通过再停旧实例;新实例持续失败就自动回滚。这里 task 指运行中的一组容器,Service 负责维持期望的 task 数量。
Relayer 扩容前需补持久化进度和实例协调;Region 内多可用区也不等于跨 Region 灾备。相关配置在 packages/infra/lib/backend-stack.ts,发布细节见 滚动发布。
# 为什么不用 Lambda?
建议回答:不是 Lambda 不能运行 Web 或 API,而是当前服务已经按常驻 Node 程序设计,尤其 Relayer 要持续轮询。改成 Lambda 后,需要把工作拆成有时间上限的批次,另存进度并处理重复触发;直接使用 ECS 改造更少。图片处理、定时清理等短任务则适合单独用 Lambda。
# 健康检查通过说明什么?
建议回答:只说明检查的那个接口能正常响应。比如 health 返回成功,可能根本没测 RPC 或 Pinata,学生仍可能领不到证书。因此要另查关键依赖,并用完整业务流程定期验证,不能把 “进程活着” 当成 “所有功能正常” 。
# RDS PostgreSQL
RDS 保存用户、课程、评论和学习进度。数据库只允许应用连接,不需要直接向互联网开放。基础见 RDS,网络与 TLS 配置见 数据库访问。
生产重点不是背配置名,而是分清三个问题:
- 谁能连接:放隔离子网,安全组只放行应用来源,凭据交给 Secrets Manager 管理。
- 连接是否可信:启用 TLS 加密,还要用可信 CA 证书校验数据库身份;走内网不代表可以关闭证书验证。rds.force_ssl 只要求加密,不替客户端检查证书。
- 出事怎么恢复:Multi-AZ 在实例或可用区故障时切换;备份与 PITR(Point-in-Time Recovery,时间点恢复)用于误删后恢复到之前的时间。两者不能互相替代。
CDK 已有生产级数据库配置开关;是否已启用、恢复需要多久,应检查实际部署并演练。若补充 Relayer 持久进度,可复用 RDS,但要建立相应表和处理逻辑,不是连上数据库就自动拥有恢复能力。
# 为什么数据库不放公网?
建议回答:应用与数据库同 VPC,没必要暴露 Internet。私有隔离子网加安全组把攻击面限制到 ECS 来源。
# 备份和 Multi AZ 有什么区别?
建议回答:Multi AZ 主要解决实例或可用区故障的快速切换;备份和 PITR 解决误删、逻辑损坏和时间点恢复,二者不能互相替代。
# S3
头像放 S3,不放数据库字段或应用容器磁盘。图片由浏览器按后端签发的条件直接上传,应用重启也不会丢失。基本能力见 S3。
项目只允许头像目录公开读取,上传路径、大小和有效期由预签名 POST 限制;CORS 配置允许实际前端域名使用上传接口。公开头像不意味着整个存储桶都公开,更不能把付费视频照同一策略存放。
生产可启用版本保留、防误删和生命周期清理。付费视频应单独使用私有对象及短时访问授权。相关代码在 packages/infra/lib/storage-stack.ts 和 apps/server/src/lib/s3.ts。
# CORS 是安全权限吗?
建议回答:不是。CORS 约束浏览器脚本是否能读取响应,不阻止 curl 或服务端请求。真正写权限由签名 policy、IAM 和 bucket policy 决定。
# AWS KMS
本项目不是用 KMS 保存一串可取出的密码,而是让它代为签署发证交易。Relayer 取得公钥算出发证地址,发送交易摘要请求签名,但拿不到私钥。它与 Secrets Manager 的区别见 密钥与凭据管理。
关键配置是 ECC_SECG_P256K1 密钥类型,配合 ECDSA_SHA_256 和 MessageType=DIGEST。这里 DIGEST 表示应用已生成待签名摘要,不能因为算法名里有 SHA_256 就把以太坊交易摘要再随意换一种哈希。
格式转换在 KMS Relayer 一节说明。权限上,IAM 只给运行角色使用指定密钥的必要操作;但KMS 不理解 “这笔交易是不是在发证” ,目标合约、方法和业务资格仍由应用与合约约束。
# KMS 能不能限制只能调用某个 Solidity 函数?
建议回答:KMS 只看签名请求,不理解 EVM calldata。项目在 mint-executor 把能力收窄到固定合约和 mint 方法,合约再限制 issuer;更强控制可加入策略服务、交易模拟和 allowlist 审计。
# Secrets Manager
基础原理见 Aladdin:KMS 与 Secrets Manager,凭据注入与轮换见 Secret 怎样交给应用。
RDS 自动生成的用户名和密码、Pinata 等第三方凭据以 secret 管理;ECS task definition 只把具体需要的字段注入对应容器,代码仓库和镜像不保存生产 secret。
相比 .env 文件或 CI 明文变量,Secrets Manager 提供集中权限、版本、轮换和审计能力,便于把凭据限定给真正需要它的服务。
配置重点:启动时注入凭据的权限授予 Execution Role,应用主动调用 AWS 的权限授予 Task Role。Web、API、Relayer 分别只拿到必需字段;凭据轮换后滚动更新对应 Task,日志不输出环境变量。
# 为什么最终还是把 secret 注入环境变量?
建议回答:许多 Node 库通过环境变量读取配置,注入是交付方式,不代表 secret 存在源码里。关键是只向需要它的 task 注入、限制 IAM、避免日志泄露,并在轮换后触发安全滚动更新。
# CloudWatch
重点不是收集了多少指标,而是学生说 “没拿到证书” 时能找到卡在哪一步。CloudWatch 集中查看运行日志和告警,基础见 可观测性笔记。
用钱包地址、课程 ID 和交易哈希关联一次申请:是否读到申领事件、后端是否确认学完、IPFS 上传是否成功、KMS 是否签名、交易回执是否成功。错误日志不能包含私钥、凭据或原始登录签名。
Web/API 看错误率和响应时间;数据库看连接数与存储;Relayer 看最后处理区块、失败数和发证成功率。持久化进度补齐后,还应监控与最新确认区块的差距:进程活着但进度不走,同样需要告警。
# CloudWatch 和 CloudTrail 有什么区别?
建议回答:CloudWatch 主要观察应用和资源的运行日志、指标与告警;CloudTrail 记录谁在什么时间调用了哪些 AWS API,偏审计。排查 KMS 签名异常时两者可能同时使用。
# Relayer 应该监控哪些指标?
建议回答:我会先看事件是否跟得上、发证是否成功、失败卡在哪一步。对应指标是最后扫描区块与最新确认区块的差距、失败与重试数、发证成功率,再用课程 ID、钱包和交易哈希查日志。扫描进度停止增长时,即使进程没退出也应告警。
# VPC 网络怎样讲
入口对外、应用可出网、数据库不出网。网络原理见 VPC、子网、NAT 与安全组。
| 位置 | 放什么 | 访问规则 |
|---|---|---|
| 公有子网(Public) | ALB、NAT Gateway | ALB 接收受控的入口请求;NAT 供私网服务主动访问外部 |
| 可出网私有子网(Private with egress) | Web、API、Relayer 容器 | 应用端口仅允许必要来源;通过 NAT 访问 RPC、Pinata 等服务 |
| 隔离子网(Private isolated) | RDS 数据库 | 只允许应用安全组连接数据库端口,没有主动公网出口 |
Relayer 不需要对外开放接口。每个服务按自己需要开权限,不能因为都在同一个 VPC 就互相放开所有端口。
# 为什么生产使用每个可用区一个 NAT Gateway?
建议回答:ECS task 需要访问 RPC、Pinata、ECR 和其他外部服务。若两个可用区共享单 NAT,该 NAT 所在可用区故障会让另一可用区也失去出网能力。生产按可用区配置 NAT 避免跨区单点;开发环境可以用单 NAT 控制固定成本。
# 为什么 Security Group 放 NetworkStack?
建议回答:数据库需要知道允许哪个应用安全组连接,应用又需要数据库地址和凭据。若安全组放在应用 Stack,就会出现两边互相依赖。把它放到共同的网络 Stack 后,两边都从上游取得配置,部署顺序就能确定。
# 第七部分 测试、安全、可观测性与扩展
这一部分先记住
测试证明的是已覆盖场景,不等于系统绝对安全。合约、API、链上事件和云部署要分别验证正常路径、失败路径与恢复路径,并通过日志、指标和告警判断问题发生在哪一层。
# 测试策略
按失败会发生在哪一层来安排测试,不用每种工具都测一遍相同内容。
| 层次 | 工具与验证方式 | 本项目重点 |
|---|---|---|
| 合约规则 | Foundry | 非管理员操作被拒绝、余额不足时回滚、重复购买和重复发证失败、证书不能转走 |
| TypeScript 逻辑 | Vitest | 钱包切换后旧结果不写回、KMS 签名格式转换、错误分类和配置校验 |
| 编译与静态检查 | tsc、ESLint、next/forge build | 提前找类型错误、代码问题和无法构建的产物;不能代替运行测试 |
| 真实依赖集成 | Sepolia 交易、CRE simulate | 钱包签名、第三方合约、RPC 和 CRE 运行时能否配合完成任务 |
| 部署验收 | 测试环境端到端操作与故障演练 | 登录到发证能否走完,重启、回滚和恢复是否符合预期 |
本地测试使用替身(mock)控制输入,真实集成测试检查替身没有覆盖的网络、权限和运行环境问题。
# 为什么测试通过仍不能说功能上线?
建议回答:测试通常使用 mock、本地链或隔离环境。涉及凭证、真实广播、IAM、CORS、第三方审批和链上 issuer 的验收,必须有真实环境证据。CRE 生产切换就是典型例子。
# 合约测试最重要的失败路径有哪些?
建议回答:我重点测三类:无权限的人能不能改课程或发证;钱和状态是否一起成功或一起回滚;同一用户能不能重复买、重复领或转走证书。接入 CRE 后再测伪造报告、错误工作流和错误目标链,确保异常输入不会变成有效发证。
# 威胁模型速查
| 可能出什么事 | 怎么防 |
|---|---|
| 冒用钱包、重复使用登录签名 | 验证签名、域名和过期时间;nonce 只能成功消费一次 |
| 普通用户直接调用审核接口 | 服务端核验管理员或教师身份,不依赖页面是否有按钮 |
| 链上查询失败却错误放行付费内容 | 区分查询失败与未购买,无法确认时不放宽权限;RPC 返回数据也需要可信来源 |
| 重复扣款、重复发证 | 合约检查购买和领取记录,前端保留待确认交易哈希 |
| 发证私钥被复制或签名权限被滥用 | KMS 防止私钥导出,IAM 限权;应用限制交易用途并监控异常 |
| 伪造 CRE 报告发证 | 校验转发者、工作流、目标链和接收地址 |
| 上传超大文件或冒用他人头像 | S3 验证上传条件,后端校验对象路径和用户归属 |
| 重启漏掉申请、并发重复写入 | 持久化进度与失败任务,数据库约束和合约规则共同防重复 |
# 可观测性和故障恢复
以 “学生申请后没收到证书” 为例:
- 先查申领交易是否成功,链上有没有事件;没有事件,就不是 Relayer 发证的问题。
- 有事件但没处理,查 RPC 和扫描进度;处理过却没发证,查学习完成判定、上传与 KMS 错误。
- 已广播交易但结果未知,先查回执和 hasClaimed,不能立刻盲目再发一笔。
- 确认可重试时再补处理。生产方案应保存进度和失败任务,让恢复不依赖用户重新点按钮。
应用发布失败则回滚镜像;数据库变更要兼容仍在运行的旧版本。代码回滚不能自动撤销已经完成的链上交易或数据库修改。
# Exactly once 能做到吗?
建议回答:我会区分事件只处理一次与证书只发一次。跨数据库、IPFS 和链上交易,很难保证整条流程恰好执行一次;更实际的生产方案是允许重复投递和重试,再由合约保证同一课程只发一张证书。持久化进度防漏,合约幂等防重,两者共同完成可靠处理。
# 规模扩大后的演进
先观察哪个问题真的出现,再增加组件:
| 出现的问题 | 对应改进 |
|---|---|
| 页面频繁查链,响应慢且被 RPC 限流 | 后台集中同步事件形成查询索引,记录区块高度并处理重组;关键权限不能盲信旧缓存 |
| Relayer 停机漏申请,失败只能重新申领 | 先持久化进度并补扫,再按需要加入任务队列;多次失败的任务放 DLQ(死信队列)等待检查 |
| 数据库连接过多或读请求变慢 | 先查慢 SQL、索引和连接池,再评估连接代理或只读副本 |
| 流量上涨,单个实例忙不过来 | 根据负载扩容 Web/API;Relayer 扩容前先做好交易序号和消费协调 |
| 单个管理员权限过大 | 使用多签与重要操作延迟执行,并记录审计日志 |
| 已购视频链接被转发 | 短时签名播放地址配合水印;更高保护需求再评估 DRM 数字版权管理 |
跨地区灾备应由恢复目标和预算决定,不是小项目默认要加的一套系统。
# 第八部分 难点与最难问题的标准回答
下面按 “现象 → 原因 → 处理 → 验证” 组织,可以直接口述。技术细节在对应章节展开,不再重复背一长串接口名。
# 最难问题首选 KMS 签名适配与可靠铸造
建议回答:最难的是让后台安全地自动发证。服务必须能签链上交易,但我不希望把私钥直接放在服务器配置里,所以使用 AWS KMS。问题是 KMS 返回的是通用签名格式,viem 不能直接拿来发送以太坊交易。
我做了一层适配:先从 KMS 公钥确定发证地址,再转换签名格式,并恢复签名地址与预期地址比对,确认没转换错,最后交给 viem 广播。业务入口只允许调用指定证书合约的发证方法,不暴露任意交易能力。
验证时不能只看签名函数没报错,还要检查恢复地址、真实交易回执,以及证书确实归目标钱包。生产可靠性则用持久化扫描进度解决停机补扫,用合约 hasClaimed 阻止重复发证:签名正确与任务可靠,是两层不同的保证。
# 如果面试官问你是不是自己实现了 ECDSA?
建议回答:我没有从零实现 ECDSA,而是用 noble curves 完成签名解析、低 s 归一化和公钥恢复。我实现的是 KMS DER 签名与 viem Account 之间的适配,以及地址恢复和真实交易验证。
# 难点二 RPC Filter 失效与事件不漏不重
建议回答:当时 Relayer 还在运行,但学生申请后没收到证书,日志不断报 filter not found。我先确认链上事件存在,把问题缩小到监听层;发现监听依赖 RPC 节点临时保存的 filter 编号,编号失效或请求切到别的节点后就查不到。
我改成按明确区块范围查询事件,不再依赖节点保存订阅状态,并避免前一次查询没结束又启动下一次。这个修改解决了监听不稳定;但不等于停机不漏事件。要保证补扫,还得保存最后处理区块,重启后回退查询,处理完成才更新进度,重复事件由合约 hasClaimed 拦截。
# 难点三 钱包切换导致异步登录和交易状态串扰
建议回答:用户用 A 钱包发起请求后切到 B 钱包,A 的结果可能更晚返回,把页面误更新成 A 的余额或购买结果。
我的处理是给每轮钱包和课程上下文一个版本号。切换时增加版本号,异步结果返回后先比对,版本过期就不写回页面;交易还记录发起时的钱包和课程。断开连接时同时退出后端登录。这样已广播交易可以继续上链,但旧请求不能污染新钱包界面;登录请求还需处理后端会话的清理,不能只忽略前端结果。
# 难点四 CRE 在 WASM 中与 Node 行为不同
建议回答:Workflow 在本地测试通过,放到 CRE 的真实模拟里却失败。根因是它运行在 WASM 环境,不是完整 Node.js;例如原来依赖 URL 全局对象的校验不能照搬,HTTP 请求格式也有明确要求。
我把环境相关逻辑集中到适配层,按 CRE 要求处理 URL、超时时长和事件参数,再运行真实模拟核对日志和输出。这让我把 “业务逻辑测试” 和 “目标运行环境验证” 分开:编译成功不代表部署后能跑。
# 难点五 部署脚本产生虚假记录
建议回答:部署脚本曾经在模拟阶段就把预测合约地址写入文件,导致交易没成功,记录看起来却像已部署。
我把流程拆成 Deploy 和 Record:前者负责校验网络并广播,后者只从成功交易回执提取地址。这样地址记录依赖真实上链结果,而不是模拟时的预测值。检查时同时看回执状态和对应合约,不能只看 JSON 文件生成了没有。
# 难点六 ECS 镜像启动失败
建议回答:ECS 任务反复启动失败,应用日志却是空的。我先看任务停止原因,而不是直接改业务代码,再用相同镜像检查启动命令和 CPU 架构。
一次故障出在构建产物的镜像清单与拉取环境不兼容,调整目标架构和附加证明文件后恢复;另一次是运行时通过 pnpm 启动触发了额外依赖安装,耗尽内存,改为直接执行已安装的启动工具。这两次的共同点是先分清 “镜像没拉下来” “进程没起来” 和 “业务运行后出错” ,再处理对应层。
# 难点七 CAA 导致自定义域名证书失败
建议回答:域名解析已经生效,但 HTTPS 仍不可用。我把 DNS 解析和证书签发分开检查,发现 CAA 记录限制了哪些证书机构可以签发,而部署平台使用的机构不在允许名单里。
处理时检查实际域名和 CNAME 别名目标上的 CAA,再在合法位置调整授权并重新验证证书;不能在同一个名称下随意同时加 CNAME 和 CAA。网站恢复后,还要同步钱包平台的允许域名和 S3 跨域配置,否则页面打开了,登录或上传仍可能失败。
# 行为面试如何收尾
用实际结果收尾:签名地址是否一致、回执是否成功、切换钱包后是否还会串状态。再说明一个真实限制及对应改进,例如监听稳定之后仍需补持久化进度,不把解决一个故障说成系统再也不会出错。
# 第九部分 高频问题库与压力追问
这一部分先记住
这一部分是追问资料,不需要逐题背诵。每道题先说第一句结论,面试官继续追问时再补项目落点和取舍;遇到陌生术语时先解释它解决什么问题,不要直接堆缩写。
下面的答案以口语为目标。先回答第一句,再根据面试官兴趣补项目证据和取舍。
# 项目与架构
# 这个项目最大的技术亮点是什么?
建议回答:把购课和自动发证串成了完整流程,并处理了容易出错的衔接。比如前端不能把交易提交当成功,后端不能只凭用户填的地址就放行,发证服务不能因为重复申请就再发一张。技术价值是这些具体规则和失败处理,不是把很多库放在一起。
# 为什么不是所有数据都上链?
建议回答:链上 storage 贵、更新慢且公开。只把需要不可篡改和公开验证的支付、购买与证书上链,视频、评论、进度放数据库或对象存储。
# 链上和链下怎样关联?
建议回答:都用同一个 courseId。例如数据库里课程 10 的标题和视频,对应链上课程 10 的价格与购买记录;证书也记录它属于课程 10。这样各自保存擅长的数据,又能拼成一个完整页面。
# 怎样避免两边数据不一致?
建议回答:先明确谁说了算:是否购买由合约决定,标题和学习进度由数据库保存,不两边都维护一份权威购买记录。跨两边的操作则先确认链上成功,再更新数据库;若第二步失败,要凭交易哈希重试或核对,先后顺序本身不等于自动一致。
# 为什么称链上为 single source of truth?
建议回答:这个词就是 “最终以谁为准” 。在本项目里,价格、购买关系和证书归属以合约为准;数据库标题和学习进度仍以数据库为准。不是所有数据都以链为准,而是每一种数据只设一个最终裁定来源。
# 如果 RPC 挂了平台是否不可用?
建议回答:课程介绍等链下内容可以继续展示,但购买权限和链上交易会受影响。生产可以配置备用 RPC,查询失败时切换;仍无法确认购买权限,就提示稍后重试,不能把网络故障当成用户没买过,也不能直接放行付费内容。
# 项目是不是过度设计?
建议回答:如果只是看视频和记进度,用普通网站就够了。这个项目引入区块链,是为了让付款记录和证书发行能由用户独立核验;因此只把这些关键记录放链上,普通内容仍放数据库。代价是手续费、交易等待和密钥管理,是否值得取决于业务是否真的需要公开验证。
# 你如何划分模块?
建议回答:合约负责付款与证书规则,Web 负责交互,API 负责身份和课程数据,Relayer 或 CRE 负责跨链上链下的发证流程。两条发证路径共用完成判定和资料生成代码,避免同一学生在两套流程里得出不同结果。
# 为什么 monorepo?
建议回答:前后端放同一仓库,改接口时能同步更新调用方,Relayer 和 CRE 也能复用同一套发证逻辑。代价是镜像构建必须带上依赖的共享包,不能只复制某个应用目录。适合当前紧密协作的代码,不代表所有服务都必须放一个仓库。
# 如果重新做最先改什么?
建议回答:优先修能造成实际漏发或错误购买的问题:保存 Relayer 进度并支持失败重试,补课程存在性检查,再完善测试。队列、索引器和多地区灾备要等流量或恢复目标需要时再引入,不能用增加组件代替解决当前缺口。
# Solidity 与合约
# ERC20 的 allowance 有什么风险?
建议回答:它是合约能动用多少钱的授权。无限授权下,合约若被利用,用户更多余额都可能受影响;改变已有额度时还要考虑旧额度被抢先使用。按需要授权和清晰展示扣款对象更稳妥;permit 只是另一种授权方式,不会自动消除这些风险。
# 为什么没有使用 SafeERC20?
建议回答:项目当前只接受自己的标准 YD 代币,代码会检查转账是否返回成功。若以后接入其他代币,我会用 SafeERC20 包装调用,因为有些代币成功后不返回布尔值,直接按标准返回值处理会出错。它解决调用兼容,不代表任何代币的业务行为都安全。
# purchase 中 state 先更新再 external call 是否符合 CEI?
建议回答:符合。先检查可以买,再把购买状态写好,最后调用代币合约扣款。这样外部调用若又绕回购买入口,看到的已经是买过的状态;扣款失败则整笔回滚,不会留下没付款的购买记录。
# 为什么 treasury 不是 immutable?
建议回答:treasury 是收款钱包,将来可能要换成多签钱包,因此允许管理员修改;修改时要拒绝零地址并发出事件,便于追踪。这里变化的是以后款项收给谁,不是任意用户都能改收款地址。
# 为什么 ydToken 是 immutable?
建议回答:ydToken 是本合约接受的支付币地址,部署后固定,用户才不会今天授权 YD、明天管理员把币种悄悄换掉。immutable 就是部署时设定、之后不能再改。
# listCourse 为什么是 upsert?
建议回答:upsert 指有记录就更新、没有就新增。同一个接口既能上架新课程,也能给现有课程改价并重新上架。这样调用简单,但还应单独区分课程是否创建过,避免默认零值被误当作有效课程。
# 课程价格能设为零吗?
建议回答:当前代码允许,可能用于免费课程。问题不在零价本身,而在不存在的课程也有默认零价,所以需要单独的存在标记,不能只用价格判断这门课是否有效。
# NFT 为什么用 safeMint?
建议回答:如果接收方是合约,它会检查对方是否实现接收 NFT 的接口,减少发到不支持 NFT 的合约地址后无法处理的问题。这个检查会调用对方代码,所以发证前先标记已领取,避免回调过程中再次领取。
# Soulbound 能阻止私钥出售吗?
建议回答:不能。它阻止 token 转移,但持有人仍可转让整个钱包私钥。它保证地址绑定,不等同于现实身份不可转让。
# supportsInterface 有什么用?
建议回答:它让钱包或其他合约询问 “你支持哪种标准” 。项目声明支持 ERC721 和 ERC5192,外部应用才知道这是 NFT,还能进一步查询是否锁定,而不是只凭名字猜能不能转让。
# 为什么 issuer 和 owner 分开?
建议回答:issuer 是日常在线发证的地址,owner 是可以更换发证地址的管理员。分开后,发证密钥出问题时管理员可以换掉它,攻击者也不会自动获得全部管理权限。但违规发证本身仍是严重风险,不能因此放松保护。
# 合约可升级吗?
建议回答:当前不可升级,换来部署与审计简单。修复重大逻辑需新合约和迁移;主网前应评估代理带来的管理员和存储布局风险。
# mint 无 cap 是漏洞吗?
建议回答:这里说的是 YD 代币增发没有总量上限,不是证书可以重复领取。当前只有管理员能增发,所以是权限与代币政策选择,不等于任何人都能造币;若面对真实资产,还要考虑管理员滥发带来的稀释风险,用总量上限、多签或延迟执行约束。
# 如何处理合约事件重组?
建议回答:链重组可能让刚看到的事件从最终链上消失。生产处理时先等确认,保存区块高度和哈希;发现区块被替换,就回退扫描并修正链下索引。确认数只能降低风险,不代表从数学上永远不会重组。
# Uniswap 与 DeFi
# x 乘 y 等于 k 是否真的永远不变?
建议回答:不是。忽略手续费时,可以用乘积不变计算一次兑换;实际手续费留在池里,乘积通常会增大,添加或取出资金也会改变它。所以它是一套定价约束,不是说这个数字从建池起永远固定。
# price impact 和 slippage 有什么区别?
建议回答:价格影响是自己的交易把池内价格推高或压低,通常已计入页面报价;滑点是最后成交与该报价的差异,可能由等待期间的其他交易造成。滑点容忍度才是允许结果比报价差多少的上限,它不是滑点本身。
# 为什么 Pair 自己也是 ERC20?
建议回答:因为资金池需要给出资者一份可计算的份额凭证,这就是 LP 代币。持有人销毁相应份额时,可按比例取回池内两种资产;LP 代币不是第三种交易储备,也不是保本凭证。
# 为什么前端通过 Router 不直接 Pair?
建议回答:Pair 主要管资金池本身,Router 帮调用方串起转入代币、选择兑换路径、最低接收数量和有效期。通过 Router 更容易把用户保护条件一起带上;直接调 Pair 也能做,但这些细节就需要自己正确处理。
# USDC 6 位 YD 18 位怎样处理?
建议回答:各自按自己的精度转换,不能直接拿链上的整数比较。比如 1 USDC 是 1,000,000 个最小单位,1 YD 是 10 的 18 次方个;输入用 parseUnits 转整数,展示用 formatUnits 转回来,中间用 bigint 计算。
# 为什么使用官方测试 USDC 而不是 MockUSDC?
建议回答:本地测试可以用自己部署的假代币控制各种情况;Sepolia 集成时使用 Circle 官方测试 USDC,更接近实际第三方代币的地址和交互。它仍然只是测试资产,不能当成真实美元价值。
# 流动性太少会怎样?
建议回答:同样交易产生更大价格影响,报价更差,也更容易被操纵。Sepolia 小额流动性池只能证明协议接入和交易链路可运行,不能代表主网所需的流动性深度与抗操纵能力。
# 交易失败 gas 会退吗?
建议回答:状态修改回滚,但执行消耗的 gas 不退。前端应提前校验余额、allowance、网络和配置,仍不能完全消除链上竞争导致的 revert。
# deadline 应该多长?
建议回答:deadline 是允许交易执行的最后时间,避免一笔旧兑换很久后才成交。具体时长要看网络拥堵和用户确认时间,太短容易正常失败,太长让报价更容易过时。它限制时间,最低收到数量才限制成交价格。
# 为什么不做自动 swap 再 purchase 一笔完成?
建议回答:可以写组合合约,让换币和买课在同一笔交易里完成,任一步失败都回滚。但还要正确处理多余代币退款、兑换保护和授权,安全审查更复杂。项目先拆开复用成熟接口,用户步骤多一些,但资金流向和错误更容易看清。
# 认证与安全
# 签名登录会消耗 gas 吗?
建议回答:personal_sign 是链下签名,不发交易不消耗 gas。只有后续 approve、purchase、swap 或申领事件上链才需要 gas。
# 为什么消息要包含 domain 和 uri?
建议回答:domain 和 uri 告诉用户这份签名用于哪个站点,后端也必须核对它们。否则为 A 网站签的消息可能被拿到 B 网站尝试登录。再配合一次性 nonce 和有效期,限制这份凭证的用途、次数和时间。
# 为什么地址统一小写?
建议回答:同一个 EVM 地址可能以不同大小写字符串出现。数据库统一格式后,不会把它们误建成两个用户;页面展示可以用校验和格式。标准化只是便于比较,不能替代地址格式校验。
# verifyMessage 如何验证?
建议回答:根据消息哈希和 ECDSA 签名恢复公钥地址,再与声明地址比较;服务端不需要用户私钥。
# 签名是否能被拿去转账?
建议回答:本项目签的是限定域名、用途和 nonce 的登录消息,不是转账交易,不能直接当交易广播。但不能因此认为所有签名都安全:代币授权等签名可能允许别人动用资产,所以必须让用户看清签署内容。
# nonce 表为什么按地址查询最新未使用记录?
建议回答:用户可以重新发起登录,但后端只接受该地址最新、未使用且未过期的登录凭证,避免多份旧签名长期有效。并发使用同一份凭证时再通过条件更新只放行一次;获取新凭证的接口也要限流,避免不断写入垃圾记录。
# getNonce 会被刷库吗?
建议回答:这是公开写库接口,确实存在滥用风险。生产应按 IP 和地址限流、限制未过期 nonce 数量并定期清理。
# CSRF 怎么处理?
建议回答:CSRF 是恶意网站借浏览器自动携带的 Cookie,冒用用户身份发请求。SameSite=Lax 能降低部分风险,敏感写接口还应核对 Origin 来源或校验防伪令牌;仅使用同源代理、HttpOnly 都不能单独解决它。
# XSS 和 HttpOnly 的关系?
建议回答:XSS 是恶意脚本在本站页面里运行。HttpOnly 能让脚本读不到登录 Cookie,但脚本仍可能借当前浏览器发请求。因此还要转义不可信内容、限制脚本来源,不能认为加了 HttpOnly 就不怕 XSS。
# 钱包断开后端 session 为什么还在?
建议回答:钱包 SDK 和 better-auth 是两个系统。只 logout Privy 不会删除 HttpOnly cookie,所以断开动作必须调用后端 sign out。
# session 里为什么不放所有用户字段?
建议回答:会话里的信息可能被缓存,教师资格被撤销后,旧会话不一定马上变化。因此会话只保存必要身份,昵称、头像按需查最新值,教师和管理员权限在操作时重新核验,避免继续使用过期资格。
# owner 权限为什么实时读链?
建议回答:owner 可能转移,数据库角色表会陈旧。实时 owner 调用以合约当前状态为准,RPC 失败应返回暂时不可用而不是误判。
# 后端与数据库
# 为什么 course.list 用 multicall?
建议回答:列表有十门课,逐门发请求要等很多次网络往返。multicall 把多个合约读取合并成较少的 RPC 请求,再按课程 ID 拼上数据库标题,降低延迟和请求量;但失败项仍要单独处理。
# multicall 全部失败怎么办?
建议回答:页面应明确提示链上数据暂时不可用,而不是把失败当成零价格或未购买。列表可保留不依赖链的内容;涉及付费访问时,无法确认权限就暂不放行。部分失败的结果也要逐项检查,不能只看批量请求有没有返回。
# videoUrl 是否真的安全?
建议回答:只在后端确认 hasPurchased 后返回能防普通 API 越权,但 URL 一旦泄露仍可共享。生产内容保护需要短时签名 URL、播放器 token、DRM 或水印。
# 为什么 learnerCount 用 progress 行数?
建议回答:当前统计的是有学习进度记录的去重用户,不是所有付款用户。进度表按用户和课程唯一,因此数行即可;如果产品要展示购买人数,就应索引购买事件,不能把 “开始学习” 换个名字当 “已购买” 。
# 如何防止重复评论或垃圾评论?
建议回答:认证只证明地址,仍需频率限制、内容长度、审核与删除策略。Web3 身份不自动解决滥用。
# Drizzle 和 Prisma 怎么选?
建议回答:两者都能完成这些数据库操作。项目选 Drizzle,主要是查询写法接近 SQL,课程列表的关联、批量查询和约束比较直观,又能保留 TypeScript 类型提示。Prisma 的模型和客户端工具也很完整,已有团队方案时不值得只为偏好重写。
# 为什么 migration 不能改历史?
建议回答:比如线上已经执行了第一版建表脚本,后来直接改这个文件,本地新建的表就会和线上不同,因为线上不会自动重跑。应新增一条变更脚本,让所有环境按同一顺序升级;删除字段还要考虑旧版本应用是否仍在使用。
# 数据库唯一约束和应用检查是否重复?
建议回答:不重复。应用先查能给出友好提示,但两个并发请求可能都查到 “还没有” ;最终唯一约束才会阻止它们都写成功。比如同一用户同一课程只能有一条进度记录,必须靠数据库守住。
# 如何解决 N+1?
建议回答:N+1 指先查一次列表,再给每条记录额外查一次。比如十条交易分别请求课程标题,就多了十次查询。可以收集全部课程 ID 一次批量查询,再在内存里匹配;链上读取则用 multicall,先减少往返而不是盲目加缓存。
# 后端如何区分 401 403 500?
建议回答:未登录是 401 语义,已登录但非 owner 或 teacher 是 403,RPC 或依赖读取失败是 500 或 503 类暂时故障,不能混成权限不足。
# 为什么 REST 接口无认证?
建议回答:这类接口供 Relayer 或 CRE 查询资料,但不应把所有内容都当作公开数据。公开课程信息可以匿名读取并限流;用户学习完成状态应按隐私要求增加服务身份验证或网络限制。接口返回结果也不能让前端自行决定是否发证。
# 链上调用和数据库更新如何做事务?
建议回答:不能把链上和数据库放进一个普通数据库事务。应记录处理中状态和交易哈希,链上确认成功后再更新业务记录;如果数据库更新失败,按同一交易重试,而不是再付一次钱。项目审核采用先确认上链再更新数据库的顺序,异常后的核对与重试仍需设计。
# Relayer、CRE 与 IPFS
# 为什么 CompletionRequest 不直接检查购买?
建议回答:这个合约只负责留下申领请求,不直接发证,完成条件集中交给后端核验。这样流程简单,但任何人都可能提交无效请求,所以后台要先低成本检查资格,再上传资料和请求签名,避免垃圾申请消耗服务资源。
# Relayer 怎么防止同一事件处理两次?
建议回答:不能保证事件只被看到一次,但可以保证证书不重复发。合约按钱包和课程检查 hasClaimed,第二次发证会被拒绝,后台把这种错误作为已完成跳过;链下去重还可以减少无效交易,但不是最后一道保障。
# Relayer 停机期间事件怎么办?
建议回答:按数据库保存的区块进度补扫,重启时回退一段范围,避免漏掉边界事件。处理成功才更新进度,重复申请由合约 hasClaimed 拦截。这样停机会增加等待时间,但不应让恢复后的服务直接跳过积压申请。
# 如何避免两个 Relayer 同时 mint?
建议回答:让实例先竞争带过期时间的数据库租约,拿到资格的实例处理同一条事件流,其他实例等待或接管故障实例。同时协调发证地址的交易序号,避免交易相互冲突。合约再阻止重复发证,防止租约失效等极端情况下产生重复结果。
# Pinata 上传成功但 mint 失败怎么办?
建议回答:先保存资料地址和交易哈希,判断交易确实失败还是回执暂时查不到。确认失败后重试发证,并尽量复用原资料,不要每次重试都重新上传。把申请、CID 和交易哈希关联起来,后台才能从失败步骤继续。
# 后端说 completed true 是否可信?
建议回答:这里仍然信任平台后端正确记录学习结果。即使 CRE 多个节点都查到 completed=true,也只能说明它们拿到了一致回答,不能证明学生真的学完。要进一步提高可信度,需要可核验的考试或成绩记录,不能只把执行程序换个地方运行。
# CRE report 如何防跨链重放?
建议回答:报告里写明目标链和接收合约地址,Receiver 收到后与自己所在链和自身地址比较。报告被搬到另一条链或另一份合约,就应被拒绝;再校验转发者和工作流编号,限制谁有权发出报告。
# workflowId 为什么生产必须非零?
建议回答:在这份 Receiver 实现中,零值代表跳过具体工作流检查,方便隔离模拟。生产必须填预期工作流编号,否则即使报告来自合法转发合约,也可能是别的工作流发来的,不应获得本项目的发证权限。
# 为什么当前保留两套路径?
建议回答:KMS Relayer 负责当前可用流程,CRE 用来验证另一种执行方案,两边复用同一套结业判定。证书合约只配置一个发证地址,不同时给两条路径独立权限,切换时就能明确谁在负责发证。
# 切换 CRE 的安全步骤?
建议回答:先部署接收合约和工作流,配置并核对转发地址、工作流编号,在隔离环境验证。正式切换时暂停新发证并处理在途交易,记录待处理申请,再切 issuer 并做一次真实申领检查。失败就恢复原发行者;恢复扫描要用保存的起点,不能切回后从最新区块跳过中间申请。
# AWS 与 DevOps
# 为什么早期用 Vercel,后来迁到 AWS?
建议回答:早期用 Vercel 是为了快速发布页面和验证业务。系统加入独立 API、数据库和长期运行的 KMS 发证服务后,统一放在 AWS 更方便管理网络、权限和日志。取舍是维护工作和固定费用增加,不是 Vercel 不能用于生产。
# 为什么生产选择 AWS,而不是 Cloudflare?
建议回答:主要考虑现有数据、签名和后台任务:数据库与 KMS 都在 AWS,Relayer 又要持续运行,ECS 可以直接复用 Node 容器。迁到 Cloudflare 需要验证运行方式、跨云连接和权限,当前收益不足以抵消改造。若以后全球访问延迟成为主要问题,再实测边缘部署是否值得。
# 既然用了 AWS,为什么没有使用 Lambda?
建议回答:AWS 也有容器服务,不必所有代码都改成函数。现有 API 和 Relayer 按常驻 Node 服务编写,放 ECS 可以复用运行方式;Lambda 也能承担其中部分工作,但要拆分有时限的任务、保存进度并处理重复调用。当前选择主要是减少改造,不是 Lambda 做不了 Web 服务。
# Cloudflare Workers 也能跑 Next.js 和 Hono,为什么还说有迁移成本?
建议回答:框架能跑,不等于项目依赖都兼容。还要验证 Node API、数据库连接、认证和构建产物;常驻 Relayer 也不能直接搬进一次请求。适配方案会更新,所以我会用当前官方方案做小范围验证,再按结果决定,而不是仅凭支持列表切换生产。
# 如果公司要求全部部署在 Cloudflare,你会怎么改?
建议回答:先用测试环境验证 Web 和 API 的框架兼容、登录及上传;再为长期任务设计分批执行、持久进度和重试。数据库和签名能力最后迁移,因为涉及数据与资金权限。若仍通过 Hyperdrive 连 AWS RDS、仍调用 AWS KMS,就只是混合云,不算完全迁离 AWS。
# 你真的把这套系统部署到 AWS 生产了吗?
建议回答:项目有 AWS 部署和真实排障记录,业务合约运行在 Sepolia。生产方案把 Web、API、发证服务分开部署,并通过 HTTPS、私网数据库、最小权限、监控和恢复机制保护完整链路。这里要区分云端部署与主网运营,不能用测试网交易证明真实主网规模。
# 你怎么证明不是只执行了 cdk synth?
建议回答:synth 只生成模板,我判断是否完成还会看三类结果:云资源与应用是否真正运行,登录到发证是否走通,故障后能否恢复。分别对应部署记录、业务交易回执和回滚恢复测试,不是只有一张架构图。
# 怎样证明这不是一份纸上架构?
建议回答:我会展示一条可追踪的业务链:请求到了哪个服务、服务查了什么、最终对应哪笔链上交易;再选一个实际问题说明定位过程,比如任务没有应用日志时先查镜像拉取和启动。能沿着证据解释具体结果,比列一串 AWS 产品名更有说服力。
# 你用什么标准认定部署完成?
建议回答:不是页面能打开就算完成。至少要验证核心业务能走通、权限和凭据隔离正确、失败能发现、旧版本和数据能恢复。四项分别留记录;测试网部署与真实主网运营也必须明确区分。
# AWS 方案最大的缺点是什么?
建议回答:主要是低流量时仍要支付持续费用,ALB、NAT 和数据库不会因为没人访问就免费。开发与预发布环境可以缩小规格、限制日志保留并设置预算;生产则按可接受停机时间配置副本与备份。省成本应优先减少闲置资源,不是关闭必要的权限隔离和加密。
# ECS task 为什么放私有子网?
建议回答:用户应该经过统一入口访问应用,不直接连接某台容器。应用仍要主动访问 RPC 或 Pinata,所以放在可以通过 NAT 出网的私有子网;安全组再限制入口来源。私有子网并不等于应用不能主动访问互联网。
# RDS 为什么 isolated?
建议回答:数据库只需接受应用连接,不需要主动访问公网。放隔离子网并只允许应用安全组连接,可以缩小暴露面;连接还要做身份认证和 TLS 证书校验,不是有私网就万事安全。
# Secrets Manager 和环境变量是什么关系?
建议回答:secret 存在 Secrets Manager,ECS 启动时把特定字段注入进进程环境。环境变量是交付载体,不代表源码保存 secret。
# 为什么 container runtime 直接跑 TS?
建议回答:项目用 tsx 直接运行 TypeScript,构建与启动配置比较简单,也避开了启动时再经 pnpm 做额外依赖检查的问题。代价是运行镜像还需要转换工具;如果要进一步缩小镜像和减少启动工作,可以提前编译成 JavaScript。两种方式都要在构建阶段装好依赖。
# 为什么 Node 22?
建议回答:主要是对齐项目锁定的工具链和依赖。当前 Dockerfile 记录 pnpm 11.18 需要 Node 22.13 及以上,因此不能只把基础镜像换成 Node 20。升级时要一起检查包管理器、依赖和构建结果,不是 Node 22 对所有项目都更合适。
# ECS 没日志怎么排查?
建议回答:先看任务停止原因:镜像没拉下来,就查仓库权限、镜像格式和架构;进程启动后马上退出,就查命令、环境变量和内存;应用已运行却没日志,再查日志驱动和权限。没有日志不是 “代码肯定没问题” ,只是帮助判断问题在哪一层。
# 为什么 buildx provenance 会有问题?
建议回答:provenance 是镜像构建来源的附加证明,可能改变发布产物的清单结构。项目曾遇到目标拉取链不接受该产物,调整架构和附加证明后恢复。应以停止原因和实际镜像清单定位,不能总结成 “所有 Fargate 都必须关闭 provenance” 。
# HTTPS 链路怎样设计?
建议回答:我会让用户通过 HTTPS 访问 CloudFront,CloudFront 到 ALB 也使用 HTTPS;ALB 的 443 listener 绑定 ACM 证书,80 只做跳转。应用连接 RDS 时加载可信 CA,校验证书链和数据库主机名,不能因为走 VPC 内网就关闭校验。这样既保护传输内容,也验证数据库服务器身份。
# 怎样做零停机部署?
建议回答:目标是先让新实例启动并通过健康检查,再让旧实例停止接收请求和退出。新版本失败时回滚;数据库变更要同时兼容新旧版本。这个过程能降低中断风险,但长连接、在途请求和外部故障仍需单独处理,不能只设两个参数就承诺绝对零停机。
# 如何控制 AWS 成本?
建议回答:开发环境单 NAT、t3 micro、单 task 是成本取舍;同时设置预算告警、日志保留、ECR 生命周期和按需销毁。不能用成本理由跳过安全边界。
# CAA 和 CNAME 为什么会关联?
建议回答:CAA 指定允许给域名签证书的机构,CNAME 则把名称指向别名目标。证书机构会按 DNS 规则解析并检查相关授权,所以别名目标的限制可能导致签发失败。要检查实际解析链,在合适位置修正,不能在已有 CNAME 的同名位置直接混加 CAA。
# 当前生产架构下一步还会怎么演进?
建议回答:先补会丢申请的缺口:持久化扫描进度、失败重试和重启补扫,再完善权限、备份与监控验证。确实出现查询慢或消费积压后,再增加索引器、队列或读副本;跨地区灾备取决于能接受多长停机和数据恢复时间。
# 第十部分 行为面试与项目复盘
这一部分先记住
行为题要讲清自己做出的判断、采取的行动、验证结果和后续反思。只有实际负责的工作才使用 “我实现” ,团队已有能力和第三方服务要准确说明边界。
# 你最自豪的决定是什么?
建议回答:我没有在数据库再维护一份可以覆盖合约的购买状态。用户能不能看付费内容,最终回到合约核验,避免后台记录与链上付款各说各话。代价是依赖 RPC,故障时要明确返回暂时不可用,而不是为了页面正常就放宽权限。
# 你犯过什么错误?
建议回答:曾经低估部署和第三方运行时差异,例如 Foundry 脚本记录时机、CRE WASM API 和 RPC filter。后来把真实环境证据加入完成标准,不再用 typecheck 或本地测试代替上线验证。
# 如何处理不知道的问题?
建议回答:我会先说明哪部分确定、哪部分还没确认,再做能缩小范围的检查。比如发证失败,先查交易回执、目标网络和 issuer 权限,而不是直接重发交易或改管理员。这样既能定位原因,也避免排查时带来新风险。
# 怎样证明不是只会调库?
建议回答:以钱包登录为例,调签名接口并不难,难的是同一签名并发提交不能登录两次,切换钱包后旧请求不能覆盖新身份。我能解释这些失败怎么发生、代码如何阻止、测试如何复现;KMS 适配也要验证签名恢复地址和真实交易结果,不只是 SDK 返回成功。
# 为什么选择这个项目?
建议回答:它把合约安全、身份、DeFi、链上链下数据、事件驱动和云部署放进了同一个已上线系统,也让我真正处理密钥、数据一致性、故障恢复和生产发布这些跨边界问题,而不是只完成彼此孤立的技术样例。
# 下一阶段会做什么?
建议回答:先把发证恢复补完整,避免重启漏申请,再修课程存在性检查和公开接口限流。之后根据查询量评估索引器,主网上线前做权限治理和合约审计。优先顺序按可能造成的损失确定,不按想多学几个工具确定。
# 第十一部分 模拟面试与复习计划
这一部分先记住
模拟时先把项目介绍自然说清楚,再练三类追问:为什么这样设计、失败后怎么办、怎样证明做过。能用自己的话保持事实一致,比逐字背完整手册更重要。
# 十五分钟全栈模拟
第一分钟 用约一分钟介绍项目。
第二到四分钟 解释链上链下数据边界,为什么 courseId 是关联键。
第五到七分钟 解释签名登录、nonce 原子消费和 HttpOnly session。
第八到十分钟 解释 approve purchase 状态机和异步钱包切换。
第十一到十三分钟 解释怎样批量查链并拼接课程资料,以及后端和数据库如何阻止越权、重复写入。
最后两分钟 主动说已知限制与下一步。
# 二十分钟 Web3 模拟
先画四个合约和权限关系,说明 owner、issuer、treasury、user。
逐步解释 ERC20 approve transferFrom 和 CoursePlatform purchase。
解释证书为什么不能转让,以及合约怎样统一阻止转让入口。
用 10000 YD 和 100 USDC 举例讲 AMM、price impact、amountOutMin、deadline。
从申领讲到发证,说明为什么允许事件重复处理,却不能重复发证。
最后讲 CRE Receiver 的 Forwarder、workflowId、chainSelector 与未生产切换边界。
# 十五分钟云与系统设计模拟
先用一分钟说明为什么从前端托管转向统一 AWS 部署,以及云环境与区块链主网是两回事。
用两分钟比较 AWS 与 Cloudflare,必须说到 Next.js 运行时、Hono、RDS、KMS Relayer、迁移成本和跨云边界。
画 Route 53、CloudFront、WAF、ALB、Web ECS、API ECS、Relayer ECS、RDS、S3、Secrets Manager 和 KMS。
解释共享安全组为什么放到网络层,怎样避免数据库与应用相互依赖。
解释 CloudFront 到 ALB 的 HTTPS、ECS 跨可用区、多 NAT、RDS Multi-AZ、checkpoint 和 CloudWatch 告警。
解释镜像拉取失败和运行时安装依赖导致内存不足,分别如何定位。
给出从原 Vercel 环境到 AWS staging、灰度切换、监控观察和回退的完整顺序。
最后接受压力追问: “你怎样证明真的上线?” 用 CloudFormation、ECS deployment、ACM、RDS、CloudWatch、端到端记录和恢复演练回答。
# 自我检查评分表
| 能力 | 合格标准 | 自评 |
|---|---|---|
| 项目叙事 | 能在 60 秒内讲清业务 边界 链路 结果 | 一到五分 |
| 合约 | 能解释每个 storage 写入和权限 不只会背 ERC | 一到五分 |
| DeFi | 能手算简单 AMM 并区分滑点与价格影响 | 一到五分 |
| 认证 | 能画 nonce sign verify session 并解释并发重放 | 一到五分 |
| 可靠性 | 能解释重复 漏事件 重组 checkpoint 和恢复 | 一到五分 |
| 云部署 | 能画生产网络并解释高可用 发布 回滚 监控与恢复 | 一到五分 |
| 云选型 | 能比较 AWS、Cloudflare 和 Vercel,不只罗列产品名 | 一到五分 |
| 真实性 | 能准确区分 Vercel 历史环境、AWS 生产云、Sepolia、主网和 CRE simulate | 一到五分 |
# 七天复习计划
| 天 | 主题 | 输出 |
|---|---|---|
| 第一天 | 项目全景与项目介绍 | 录音复述业务、链上链下分工和发证流程 |
| 第二天 | Solidity ERC20 ERC721 ERC5192 Foundry | 手画合约状态与调用图 回答二十题 |
| 第三天 | Uniswap V2 与购买状态机 | 手算两个 AMM 例子 复述失败路径 |
| 第四天 | Privy better-auth tRPC PostgreSQL | 画认证时序 解释原子 nonce 和权限中间件 |
| 第五天 | Relayer KMS CRE IPFS | 讲两遍最难问题 画 report 与 issuer 切换 |
| 第六天 | AWS CDK ECS RDS S3 与 Cloudflare 选型 | 画生产拓扑 讲三个部署事故 完成十道压力追问 |
| 第七天 | 全真模拟与复盘 | 完成三场模拟 每场记录答不清的三个问题 |
# 面试前十分钟速记
- 业务:用户用 YD 买课,学完后领取绑定钱包的证书;没有 YD 时先兑换。
- 分工:付款与证书由合约记录,课程内容和进度由数据库保存,用课程 ID 关联。
- 登录:后端发一次性消息,钱包签名,后端验证并消费 nonce,再建立会话。
- 购买:先检查余额和授权,再购买;交易提交与交易成功分开显示。
- 发证:监听申领、核验结业、保存资料、签名广播;重复发证由合约拒绝。
- 恢复:生产用数据库记住扫描进度,失败重试;允许重复处理,不允许重复发证。
- 部署:CloudFront 缓存,ALB 分流,ECS 运行服务,RDS 存数据,KMS 签名。
- 验证:能构建不等于能上线,能打开页面不等于能恢复故障;用业务回执和演练记录证明。
- 取舍:统一 AWS 减少改造和跨云调用,代价是持续费用;CRE 是另一条执行路径,不改变后端数据的信任问题。
# 可以反问面试官的问题
团队如何定义链上与链下的真相源,是否已有事件索引和重组处理规范?
合约上线前采用哪些测试、审计、权限和部署回滚流程?
热钱包、Relayer 或 keeper 的密钥如何托管、轮换和限制交易能力?
前端交易状态如何处理确认未知、链切换、账户切换和 RPC 限流?
主网故障时,团队更关注不重复执行还是不漏执行,checkpoint 和补偿机制怎样设计?
# 附录 项目证据索引
| 主题 | 代码或文档 |
|---|---|
| 项目状态和地址 | README.md packages/contracts/deployments/sepolia.json |
| 合约 | packages/contracts/src test script |
| 购买与兑换前端 | apps/web/src/hooks/use-purchase-flow.ts use-swap-quote.ts use-eth-to-yd-swap.ts use-usdc-to-yd-swap.ts |
| 钱包认证 | apps/web/src/hooks/use-wallet-sign-in.ts apps/server/src/lib/auth-plugins/wallet-sign-in.ts |
| API 与链上聚合 | apps/server/src/trpc/routers lib/onchain |
| 数据库 | apps/server/src/db/schema migrations |
| Relayer | apps/relayer/src/listener.ts kms-account.ts completion-handler.ts |
| CRE | apps/cre-workflow CourseCompletionNFTReceiver.sol ReceiverTemplate.sol |
| 共享证书逻辑 | packages/completion-nft-shared |
| AWS | packages/infra apps/server/Dockerfile docker-entrypoint.sh |
| 技术复盘 | docs/knowledge-base AGENTS.md specs |
# Sepolia 证据
| 对象 | 地址或状态 |
|---|---|
| YDToken | 0xf64bE7869CAf8B00E42209A2CEc3e681a448c320 |
| CourseCompletionNFT | 0x411c76DCA6637286AE52107dAEfF07047f14A9b5 |
| CoursePlatform | 0x5Bd1C512458e087c6BD5188438055e3a47ed2eD9 |
| CompletionRequest | 0xF55292dc9a9256F1af1ec9F8a9758dFbD0F60032 |
| Circle Sepolia USDC | 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 |
| YD USDC Pair | 0x716A060EFF81D11D85beBfeFd8E121CA34FED531 |
| YD WETH Pair | 0x6ECd9ACd435D1EadC46a141DC35bf159C0bA5f76 |
| KMS Relayer issuer | 0xdA40b3216d5ac587BbA32181cefE4C705a56762f |
| CRE 状态 | 隔离 simulate 完成 生产 issuer 未切换 |
# 技术术语速查
| 术语 | 一句话理解 |
|---|---|
| ABI(Application Binary Interface) | 合约函数和事件的接口说明,客户端据此编码和解码 |
| EOA(Externally Owned Account) | 私钥控制的外部账户,常见例子是用户钱包 |
| RPC(Remote Procedure Call) | 客户端向区块链节点查询数据或提交交易的接口 |
| Receipt(交易回执) | 交易执行结果,包括成功或失败、手续费消耗和事件 |
| Reorg(链重组) | 最近区块被替换,之前看到的事件可能不再属于最终链 |
| CEI(Checks–Effects–Interactions) | 先检查条件、再改自身状态、最后调用外部合约 |
| Allowance(授权额度) | 用户允许某个合约从自己钱包扣取多少代币 |
| AMM(Automated Market Maker,自动做市) | 用户与资金池兑换,由规则定价,不必等另一人挂出匹配订单 |
| Price impact(价格影响) | 自己的交易改变池内比例造成的价格变化,通常已计入报价 |
| Slippage(滑点) | 最终成交与此前报价的差异,可能有利也可能不利 |
| Slippage tolerance(滑点容忍度) | 允许成交结果比报价差多少,超过就拒绝兑换 |
| Soulbound(灵魂绑定) | 绑定钱包、不能转让的凭证,不等于绑定现实身份 |
| SIWE(Sign-In with Ethereum) | 用以太坊钱包签名证明地址控制权的登录标准 |
| Nonce | 登录里是一次性随机凭证;交易里是账户发送交易的序号,用途不同 |
| Idempotency(幂等) | 同一请求重复到达,业务结果不重复,例如不多发一张证书 |
| Checkpoint(扫描进度) | 已处理到哪个区块,重启后从记录附近继续 |
| Lease(租约) | 有过期时间的处理资格,防止多个实例同时争抢同一任务 |
| CID(Content Identifier) | IPFS 内容标识,能核验内容,不能凭空恢复丢失文件 |
| DON(Decentralized Oracle Network) | 去中心化预言机网络,参与执行和报告生成 |
| KMS(Key Management Service) | 密钥管理服务,本项目用它保管不可导出的发证私钥并签名 |
| Fargate | AWS 提供的容器计算资源,不必自己维护底层服务器 |
| CAA(Certification Authority Authorization) | DNS 中指定哪些证书机构可以为域名签发证书的记录 |
# 部署选型官方参考
# 最后提醒
面试官真正判断的是你能否解释因果关系:为什么这样拆、失败会怎样、谁是可信方、重复和并发如何处理、你用什么证据确认结果。技术名词只是入口。回答时先给结论,再用这个项目的代码和故障说明,最后承认当前边界并给出下一步。