ethers v6 里 provider / signer 怎么拿,以及手写一个 signer
前端 dApp 连 MetaMask 用 ethers.js,v5 和 v6 API 变了。写代码总卡在"provider 怎么初始化"这一步。 ethers v6 的最小骨架 <script src="https://cdn.jsdelivr.net/npm/ethers@6.7.1/dist/ethers.min.js"></script> <script> let provider, signer;if (window.ethereum) { provider = new ethers.BrowserProvider(window.ethereum); } else { alert("请先安装 MetaMask"); }document.getElementById("connect").addEventListener("click", async () => { await provider.send("eth_requestAccounts", []); signer = await provider.getSigner(); const address = await signer.getAddress(); const balance = await provider.getBalance(address); console.log(address, ethers.formatEther(balance), "ETH"); }); </script>要点:v6 用 ethers.BrowserProvider(v5 是 ethers.providers.Web3Provider) getSigner() 现在返回 Promise,要 await ethers.formatEther(v6)替代了 ethers.utils.formatEther(v5)v5 vs v6 对照场景 v5 v6浏览器 Provider new ethers.providers.Web3Provider(eth) new ethers.BrowserProvider(eth)RPC Provider ethers.providers.JsonRpcProvider(url) ethers.JsonRpcProvider(url)formatEther ethers.utils.formatEther(v) ethers.formatEther(v)parseEther ethers.utils.parseEther(s) ethers.parseEther(s)BigNumber ethers.BigNumber.from(x) 原生 BigInt手写一个 signer(不依赖 ethers) 有些教学场景或轻量项目不想引入整个 ethers.js,直接靠 window.ethereum 也能拼出一个 signer: async function myGetSigner() { const accounts = await window.ethereum.request({ method: "eth_requestAccounts", }); const address = accounts[0]; return { address, async getAddress() { return address; }, async signMessage(message) { const hex = typeof message === "string" ? toHex(message) : message; return window.ethereum.request({ method: "personal_sign", params: [hex, address], }); }, async sendTransaction(tx) { const params = { from: address, to: tx.to, value: tx.value ? "0x" + BigInt(tx.value).toString(16) : "0x0", gas: tx.gas ? "0x" + BigInt(tx.gas).toString(16) : undefined, data: tx.data || "0x", }; return window.ethereum.request({ method: "eth_sendTransaction", params: [params], }); }, }; }// 浏览器没有 Buffer,自己写一个字符串转 hex function toHex(str) { let hex = ""; for (const ch of str) { hex += ch.charCodeAt(0).toString(16).padStart(2, "0"); } return "0x" + hex; }常见踩坑 Buffer is not defined 从 Node 教程抄的 Buffer.from(...).toString('hex') 在浏览器不能用。用上面的 toHex 或 TextEncoder: const hex = "0x" + Array.from(new TextEncoder().encode(str)) .map(b => b.toString(16).padStart(2, "0")).join("");BigInt 序列化 v6 起金额和 nonce 都是原生 BigInt,直接 JSON.stringify 会崩: TypeError: Do not know how to serialize a BigInt要么手动转字符串,要么全局 patch: BigInt.prototype.toJSON = function () { return this.toString(); };window.ethereum 是数组 装了多个钱包(MetaMask + Coinbase + OKX)后,window.ethereum 可能变成聚合对象,isMetaMask 判断失效。EIP-6963 出来后的做法: window.addEventListener("eip6963:announceProvider", (event) => { console.log(event.detail.info.name, event.detail.provider); }); window.dispatchEvent(new Event("eip6963:requestProvider"));一句话总结 ethers v6:new BrowserProvider(window.ethereum) + await provider.getSigner();不想引 ethers 时手写 signer 也不难,注意浏览器里没 Buffer 就行。
Python 3.12+ 报 No module named 'distutils':三种恢复姿势
Python 3.13 环境下跑 labelImg,直接崩: File ".../site-packages/labelImg/labelImg.py", line 5, in <module> import distutils.spawn ModuleNotFoundError: No module named 'distutils'uv / pip 装依赖时也可能顺带看到: Because there are no versions of distutils and you require distutils, we can conclude that your requirements are unsatisfiable.为什么消失了 distutils 从 Python 3.10 就被标注 deprecated,3.12 起彻底删除。它的角色被 setuptools 和 packaging 接管,但很多老库(labelImg、部分 setup.py、老工具链)里还是硬 import distutils.spawn 或 distutils.util。 方案一:让 setuptools 顶上 从 setuptools 60 开始,装 setuptools 时会顺手把 distutils 兼容层装回来: pip install --upgrade setuptools装完再跑一次原命令,八成的老库都能过。注意要装到 触发报错的那个 Python 里,虚拟环境别搞混。 方案二:改一行源码(最彻底) distutils.spawn.find_executable 其实就等价于 shutil.which。找到出错的文件: .venv\Lib\site-packages\labelImg\labelImg.py把顶部的: import distutils.spawn替换成: import shutil然后把用到的地方: distutils.spawn.find_executable("xxx") # 改成 shutil.which("xxx")一处不会漏。这个改法在 CI 环境里最省事——不用装额外依赖。 方案三:降回 3.11(保稳) 赶时间又不想改源码: py -3.11 -m venv .venv311 .\.venv311\Scripts\activate pip install labelImg labelImg3.11 保留 distutils,官方支持到 2027 年 10 月,够用很久。 三种方案怎么选场景 推荐自己项目、能改源码 方案二(shutil.which)老工具/labelImg 类第三方包 方案一 + 方案三兜底生产环境、要稳 方案三(Python 3.11)一句话总结 distutils 3.12 起被删。八成情况装 setuptools 就能过;不行就直接改 shutil.which;最保险的是回 Python 3.11。
开发测试用什么信用卡号:Stripe 测试卡 + Luhn 算法
做支付集成 / 电商网站开发,需要在开发环境测试信用卡表单——不能用真实卡号(隐私、合规、可能被误扣款)。正规做法:用支付平台官方提供的测试卡号,只在 sandbox / test 模式下有效。 Stripe 测试卡(最常用) Stripe 的 test key(以 sk_test_ 开头)下这些卡号完全有效:卡号 场景4242 4242 4242 4242 Visa 成功4000 0025 0000 3155 3DS 认证成功4000 0000 0000 9995 余额不足4000 0000 0000 0002 拒绝4000 0000 0000 0069 过期卡4000 0000 0000 0127 CVV 错误5555 5555 5555 4444 Mastercard 成功3782 822463 10005 Amex 成功其它字段:有效期:任意未来日期(比如 12/34) CVV:任意 3 位数字(Amex 是 4 位) 邮编:任意 5 位(美国)完整列表:docs.stripe.com/testing 其它支付平台 PayPal Sandbox:注册测试账号(personal + business),登录时用测试账号密码,不用真实卡号。 Braintree: 4111 1111 1111 1111 → Visa 成功 4000 1111 1111 1115 → Visa 拒绝Adyen: 4111 1111 4555 1142 → Visa 成功 2223 0000 4841 0010 → Mastercard 成功支付宝 / 微信支付沙箱:用它们的沙箱环境,扫码支付按提示走。国内也不涉及信用卡。 Luhn 算法:卡号是怎么校验的 信用卡号的最后一位是校验位,用 Luhn(罗恩)算法生成,防止手误。这算法能查错但不能验真——只保证格式合法,不能保证卡真的存在。 规则:从右往左数,偶数位(第 2、4、6... 位)乘 2 乘完之后 > 9 的,减 9(等价于两位数字相加) 所有数字相加 和是 10 的倍数 → 合法例:4242 4242 4242 4242 4 2 4 2 4 2 4 2 4 2 4 2 4 2 4 2 ×2 ×2 ×2 ×2 ×2 ×2 ×2 ×2 = 8 2 8 2 8 2 8 2 8 2 8 2 8 2 8 2 和 = 80,能被 10 整除 → 合法Python 实现 Luhn 校验 def luhn_check(card: str) -> bool: digits = [int(c) for c in card if c.isdigit()] if not digits: return False total = 0 for i, d in enumerate(reversed(digits)): if i % 2 == 1: # 偶数位(从右数) d *= 2 if d > 9: d -= 9 total += d return total % 10 == 0print(luhn_check("4242 4242 4242 4242")) # True print(luhn_check("4242 4242 4242 4241")) # False生成一个合法但假的卡号 前 15 位随便填,用 Luhn 算最后一位: def luhn_checksum(partial: str) -> int: """给一串数字算 Luhn 校验位""" digits = [int(c) for c in partial] total = 0 for i, d in enumerate(reversed(digits)): if i % 2 == 0: # 从右数第一位不动 d *= 2 if d > 9: d -= 9 total += d return (10 - total % 10) % 10# 例:Visa 前缀 4,前 15 位随便填 partial = "424242424242424" check = luhn_checksum(partial) card = partial + str(check) print(card) # 4242424242424242注意:这样生成的卡号格式合法但不代表真的能用——支付网关会实时验证 BIN、发卡行、账户状态。用于开发时的假数据填充可以,但绕不过实际支付流程。 卡号结构 一张信用卡号一般 15-16 位: 4 2 4 2 4 2 4 2 4 2 4 2 4 2 4 2 ├───┤ ├─────────────┤ BIN 账户 + 校验位 6 位 后面剩余BIN(前 6 位):Bank Identification Number,标识发卡机构 中间位:账户号 最后一位:Luhn 校验位发卡组织前缀:起始位 卡种4 Visa51-55, 22-27 Mastercard34, 37 Amex6011, 65 Discover62 银联35 JCBCVV 是什么CVV1:磁条里的验证码 CVV2:卡背面 3 位数字(Amex 是正面 4 位) 网上支付都用 CVV2Stripe 测试环境里 CVV 任意 3 位都行——真实场景由发卡行验证。 一句话总结 开发 / 测试永远用 Stripe 等平台的官方测试卡号(4242 4242 4242 4242),不要用真实卡号。Luhn 算法可以做格式校验和假数据生成,但格式合法 ≠ 真的能用。真实交易由支付网关全套验证。
TypeScript `import type` 和 "没有导出的成员" 的坑
Vue 项目里想约束一个变量只能是 element-plus tag 的合法类型: import type { TagType } from "element-plus";const t: TagType = "success";结果 TS 报错: 模块 "element-plus" 没有导出的成员 "TagType"。 你是想改用 "import TagType from 'element-plus'" 吗?问题不是 import type 用错了,是 element-plus 根本没在主入口导出 TagType。 import type 是干嘛的 先说 import type 本身,它只导入类型信息,编译成 JS 时会完全消失: import type { User } from "./types";const u: User = { id: 1 }; // 编译后 import 语句被抹掉对比普通 import: import { User } from "./types"; // 编译后仍然 require("./types") — 运行时如果这个模块只导出类型就报错import type 的三个作用:避免运行时副作用 — 只用类型的场景不会拉进模块 配合 verbatimModuleSyntax — 现代 TS 项目里"类型 vs 值"必须显式区分 减小打包体积 — 无用运行时代码会被 tree-shake 掉为什么 element-plus 报"没有导出的成员" TagType 在 element-plus 内部定义: // packages/components/tag/src/tag.ts export type TagType = "" | "success" | "info" | "warning" | "danger";但没转发到主入口 element-plus/es/index.d.ts。所以: import type { TagType } from "element-plus"; // 找不到两种解法 方案 1:自己定义(推荐) 三秒钟搞定,稳定: type TagType = "" | "success" | "info" | "warning" | "danger";const t: TagType = "success";对内部类型 element-plus 后续升级也不会一言不合就改。自己定义等于 API 边界完全掌控。 方案 2:从深路径引 理论上可以: import type { TagType } from "element-plus/es/components/tag/src/tag";局限:这是非公开 API,element-plus 版本升级可能改路径 需要构建工具能解析这种深路径 不同版本的 element-plus 目录结构可能不同(lib/ vs es/ vs dist/)做工具库或第三方组件时优先方案 1,业务代码里图省事偶尔可以用方案 2。 import type 的常见变体 内联 type(TS 4.5+): import { type User, createUser } from "./user"; // ^^^^ 只这个是类型同一 import 里混装类型和值,编译后 User 被抹掉,createUser 保留。 全部当值 import(老式,不推荐): import { User } from "./user"; type MyRole = User["role"]; // 运行时会真的加载模块export type: export type { User } from "./user";再转发类型,同样只影响类型系统,不产生运行时依赖。 一张速查表场景 写法只用类型 import type { X } from "..."同一 import 混装 import { type X, y } from "..."转发类型 export type { X } from "..."库没有导出的内部类型 自己定义 or 深路径引(慎用)verbatimModuleSyntax=true 类型必须用 import type,否则报错一句话总结 import type = 只要类型不要运行时代码。库没导出的内部类型别硬拽——自己定义一份最稳,深路径 import 只是应急。
跨域 CORS 完整解决方案:后端、Nginx、前端代理全覆盖
跨域的本质 浏览器的同源策略阻止了跨源请求:协议、域名、端口任一不同即为跨域。服务端不受此限制,同源策略是浏览器行为,后端直接请求后端不存在跨域问题。 浏览器在发送非简单请求前会先发一个 OPTIONS 预检请求(preflight),服务端必须正确响应: Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization后端配置(根本解法) Node.js / Express 最简单的做法是手动加响应头,或使用 cors 中间件: // 手动写 app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); res.header("Access-Control-Allow-Headers", "Content-Type,Authorization"); if (req.method === "OPTIONS") return res.sendStatus(200); next(); });// 或使用 cors 包 const cors = require("cors"); app.use(cors({ origin: "https://your-frontend.com", methods: ["GET", "POST"], allowedHeaders: ["Content-Type", "Authorization"] }));生产环境不要用 *,写具体允许的 origin。带 credentials 时更不能用 *: app.use(cors({ origin: "https://your-frontend.com", credentials: true // 允许 Cookie }));Spring Boot 单个接口: @CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST}) @RestController public class MyController { @GetMapping("/api/data") public String getData() { ... } }全局配置(推荐统一管理): @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://your-frontend.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }Nginx 反向代理配置 适合把前端和后端统一在同一域名下,或给第三方接口套一层代理: location /api/ { proxy_pass http://backend:8080; add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; add_header Access-Control-Allow-Credentials true always; if ($request_method = OPTIONS) { return 204; } }always 参数确保在非 200 响应(如 4xx、5xx)时也带上跨域头。 前端开发环境代理 开发阶段用本地代理把请求转发到后端,不产生跨域: Vite: // vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } });Vue CLI / webpack-dev-server: // vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };Create React App: // package.json { "proxy": "http://localhost:8080" }JSONP(仅 GET,老项目兼容) 利用 <script> 标签不受同源限制: <script src="https://api.example.com/data?callback=onData"></script> <script> function onData(data) { console.log(data); // { "name": "test" } } </script>服务端返回: onData({"name":"test"})JSONP 只支持 GET,现代项目不推荐,但对接老旧第三方接口时可能还会遇到。 后端转发(BFF 模式) 前端请求自己的服务端,由服务端再去请求第三方接口: 浏览器 → 自己的后端(同源)→ 第三方 API这样浏览器始终只看到同源请求,完全绕开 CORS 限制。适合第三方接口不支持配置 CORS 的情况。 方案速查场景 推荐方案生产环境,自己控制后端 后端配置 CORS 响应头生产环境,用 Nginx Nginx 加 Access-Control-* 头开发环境调试 本地代理(Vite/Vue CLI/CRA)第三方接口不支持 CORS 后端转发(BFF)老项目只有 GET 需求 JSONP临时调试(不上线) 浏览器 CORS 插件
