I’m Yifan, a software engineer based in Beijing, working in fintech and slowly getting the hang of building with AI.
I write about what I learn from building things, and sometimes about life. Glad you stopped by.
从 1Password 迁移到 Vaultwarden
背景 1Password 不再有跨区订阅优惠,Family Annual ¥498/年 还是有些肉疼。同事们都推荐 Vaultwarden,正好有空闲的 VPS,干脆自建。 Vaultwarden 是 Bitwarden 服务端的 Rust 重写实现,协议完全兼容官方客户端(桌面 / 移动端 / 浏览器扩展),但资源占用极低,1C/1G 的小机器跑得很轻松。 前置准备 一台 Debian 12 VPS(1C/1G 起步即可,家用绰绰有余) 一个域名,例如 vault.example.com,在 DNS 服务商把 A 记录指向 VPS 公网 IP 一个 SMTP 邮箱(用于邀请家人、找回密码),推荐 Resend 环境初始化 apt update && apt upgrade -y && apt install -y curl ca-certificates timedatectl set-timezone Asia/Shanghai adduser evan usermod -aG sudo evan curl -fsSL https://get.docker.com | sh systemctl enable --now docker usermod -aG docker evan 部署 Vaultwarden + Caddy 切到普通用户,建立工作目录: ...
PHP Migrating 8.4 to 8.5
简介 PHP 8.5 引入了许多令人兴奋的新特性,包括管道运算符(Pipe Operator)、URI 扩展、Clone With 语法、#[\NoDiscard] 属性等。本文将介绍主要变化和如何从 PHP 8.4 迁移到 PHP 8.5。 参考资源 在线测试环境:https://3v4l.org/ sandbox:https://onlinephp.io/ 官方发布说明:https://www.php.net/releases/8.5/en.php 迁移指南:https://www.php.net/manual/en/migration85.php 更新详情:https://php.watch/versions/8.5 Supported Versions:https://www.php.net/supported-versions.php EOL:https://www.php.net/eol.php PHP 8.5 新特性 管道运算符(Pipe Operator) PHP 8.5 引入了管道运算符 |>,使函数链式调用更加清晰易读: // PHP 8.5 之前 - 嵌套函数调用,难以阅读 $slug = strtolower(str_replace('.', '', str_replace(' ', '-', trim($title)))); // PHP 8.5 - 使用管道运算符,从左到右清晰阅读 $slug = $title |> trim(...) |> (fn($str) => str_replace(' ', '-', $str)) |> (fn($str) => str_replace('.', '', $str)) |> strtolower(...); 管道运算符将左侧的值作为右侧函数的第一个参数传递,实现函数组合的链式调用。 URI 扩展 PHP 8.5 新增了内置的 URI 扩展,遵循 RFC 3986 和 WHATWG URL 标准来解析和处理 URL。 URI 与 URL 的关系:URI(统一资源标识符)用于标识资源,URL(统一资源定位符)用于定位资源。所有 URL 都是 URI,但不是所有 URI 都是 URL。该扩展命名为 URI 是因为它能处理更广泛的标识符格式。 ...
自建代理订阅管理
背景 挂着两三家机场 + 一台自建节点,每家命名规则不一样、节点重复、规则集要在每个客户端各贴一份。机场换链接、加节点、改分流,所有设备都要重订阅一轮。 要的是:所有原始订阅塞进一个聚合器,自动去重 / 改名 / 分组 / 套规则,输出一条稳定 URL,客户端只订阅这条。本文用 Sub-Store + subconverter + Docker Compose 实现。 架构与暴露策略 三层各暴露一个稳定 URL,上下游互不影响。链路单向:客户端 → subconverter 公网 URL → 容器内拉 Sub-Store → Sub-Store 拉机场。中间任一层挂掉只影响下次订阅刷新,已下载的配置继续可用。 机场订阅 · 自建节点 ↓ Sub-Store · 合并 / 去重 / 改名 / 过滤 / 分组 ↓ 纯节点订阅 subconverter (+ sub-web GUI) · 套 ACL4SSR · 转目标格式 ↓ 客户端 · Mihomo / Surge / sing-box 只有 subconverter 必须公网(客户端要拉订阅),其余关进 SSH 隧道: ...
NGINX 启用 HTTP/3
简介 HTTP/2 解决了"多路复用、头部压缩、优先级"等 HTTP/1.x 的瓶颈,但仍受 TCP 先天限制。 HTTP/3 则把同样的语义搬到 QUIC。Quick UDP Internet Connections 是 Google 设计的一种基于 UDP 的多路复用安全传输层协议。标准化后,HTTP/3 = HTTP over QUIC。 层级 HTTP/2 HTTP/3 传输 TCP(必)+ TLS 1.2/1.3(多数场景) QUIC(UDP 之上)+ TLS 1.3(强制) 流(Stream) Mux 在 TCP 里的逻辑流 Mux 在 QUIC 原生流 头压缩 HPACK QPACK(避免队头阻塞) HTTP/2 浏览器要求必须跑在 TLS 之上(ALPN=“h2”)。栈变为 HTTP/2 → TLS → TCP → IP。 HTTP/3 使用 QUIC 替换 TCP,栈变为 HTTP/3 → QUIC(拥塞/重传)→ UDP → IP,加密集成在 QUIC(基于 TLS 1.3)。 Mux 即 multiplex,多路复用。 HTTP/2 必须顺序传输,而 HTTP/3 支持乱序。 如果沿用 HPACK,丢一个 UDP 包就会让所有流的头部解码卡住,因此 IETF 为 HTTP/3 设计了 QPACK 来"避开队头阻塞(Head-of-Line Blocking, HoL)"。 连接迁移:通过 Connection ID 识别会话,IP 或端口改变(如移动网络/Wi-Fi 切换)无需重新建立连接。 使用 HTTP/3 的网站: ...
在本地环境使用 HTTPS
公网环境使用 HTTPS 参考:使用 Certbot 获取 Let’s Encrypt 颁发的 TLS 证书 | ZYF.IM。 简介 HTTPS(HyperText Transfer Protocol Secure)是在 HTTP 之上通过 TLS/SSL 实现加密的安全通信协议,用来保护浏览器与服务器之间的数据机密性、完整性与身份可信性。 开始实验 mkcert 会在本地生成一套根 CA(包含根证书和私钥),并把根证书导入系统/浏览器的信任列表。随后,它用这套 CA 的私钥为 localhost 等域名签发服务器证书;因为根证书已被信任,浏览器访问时就会把服务器证书视为可信。 brew install mkcert mkcert -install mkcert localhost # The certificate is at "./localhost.pem" and the key at "./localhost-key.pem" ✅ # vi default.conf server { listen 443 ssl; server_name localhost; ssl_certificate /etc/ssl/certs/localhost.pem; ssl_certificate_key /etc/ssl/certs/localhost-key.pem; # 只允许 TLSv1.2 和 TLSv1.3 协议,显式禁止 TLSv1.0 和 TLSv1.1 协议 ssl_protocols TLSv1.2 TLSv1.3; # 只保留高强度、带身份验证、且不使用 MD5 的 TLS 1.2(及以下)套件 ssl_ciphers HIGH:!aNULL:!MD5; # 优先使用服务器端加密套件 ssl_prefer_server_ciphers on; location / { root /usr/share/nginx/html; index index.html index.htm; } } docker run -d --name nginx-ssl \ -p 8443:443 \ -v ./default.conf:/etc/nginx/conf.d/default.conf:ro \ -v ./localhost.pem:/etc/ssl/certs/localhost.pem:ro \ -v ./localhost-key.pem:/etc/ssl/certs/localhost-key.pem:ro \ nginx:alpine # docker rm -f nginx-ssl # docker container restart nginx-ssl 测试 浏览器 浏览器访问 https://localhost:8443/,如果证书是绿色的,说明成功了。 ...
PHP Migrating 8.3 to 8.4
简介 PHP 8.4 引入了许多新特性,如属性钩子(Property Hooks)、非对称可见性(Asymmetric Visibility)、#[Deprecated] 属性以及更新的 DOM API 等。本文将介绍主要变化和如何进行迁移。 参考资源 在线测试环境:https://onlinephp.io/ 官方发布说明:https://www.php.net/releases/8.4/en.php 迁移指南:https://www.php.net/manual/en/migration84.php 更新详情:https://php.watch/versions/8.4 PHP 8.4 新特性 属性钩子与非对称可见性 // 属性钩子(Property Hooks)与非对称可见性(Asymmetric Visibility) class User { private bool $isModified = false; public function __construct(public private(set) string $first, private string $last) { } public string $fullName { get => $this->first . " " . $this->last; set { [$this->first, $this->last] = explode(' ', $value, 2); $this->isModified = true; } } } $user = new User("Ming", "Li"); echo $user->fullName; // 输出: Ming Li $user->fullName = "Hong Xiao"; echo $user->fullName; // 输出: Hong Xiao // 非对称可见性示例 echo $user->first; // 输出: Hong $user->first = "Hua"; // 错误: Fatal error: Uncaught Error: Cannot modify private(set) property User::$first 属性钩子允许我们定义属性的获取和设置行为,而非对称可见性允许我们为属性设置不同的读写权限。在上例中,$first 属性可以公开读取但只能私有写入。 #[Deprecated] 属性 PHP 8.4 引入了 #[Deprecated] 属性,可以用来标记已弃用的代码: ...
PHP Migrating 8.2 to 8.3
简介 PHP 8.3 引入了许多新特性,包括类型化类常量(Typed Class Constants)、#[\Override] 属性、json_validate() 函数、只读属性的深度克隆等。本文将介绍主要变化和如何从 PHP 8.2 迁移到 PHP 8.3。 参考资源 在线测试环境:https://onlinephp.io/ 官方发布说明:https://www.php.net/releases/8.3/en.php 迁移指南:https://www.php.net/manual/en/migration83.php 更新详情:https://php.watch/versions/8.3 PHP 8.3 新特性 类型化类常量(Typed Class Constants) PHP 8.3 允许为类常量声明类型,增强了类型安全性: interface DatabaseInterface { public const string ENGINE = 'mysql'; } trait ConfigTrait { final protected const int MAX_CONNECTIONS = 100; } enum Status: string { public const string DEFAULT = 'pending'; case Pending = 'pending'; case Active = 'active'; } class Database implements DatabaseInterface { use ConfigTrait; // 类型遵循 LSP(里氏替换原则),子类可以收窄类型 public const string ENGINE = 'postgresql'; } 类常量类型遵循里氏替换原则(LSP),子类可以收窄父类常量的类型: class ParentClass { public const string|int VALUE = 'MyValue'; } class ChildClass extends ParentClass { // ✅ 可以将 string|int 收窄为 string public const string VALUE = 'MyValue'; } #[\Override] 属性 新的 #[\Override] 属性确保方法确实覆盖了父类方法,有助于捕获拼写错误和简化重构: class ParentClass { protected function setUp(): void {} } class ChildClass extends ParentClass { #[\Override] protected function setUp(): void {} // ✅ 正确覆盖 #[\Override] protected function setUpP(): void {} // ❌ Fatal error: 父类没有该方法 } 这在重构时特别有用 - 如果父类方法被重命名或删除,PHP 会立即报错而不是静默失败。 ...
PHP Migrating 8.1 to 8.2
简介 PHP 8.2 引入了许多类型系统改进,包括只读类(Readonly Classes)、析取范式类型(DNF Types)、独立的 null、true、false 类型、敏感参数隐藏、新的 Random 扩展等。本文将介绍主要变化和如何从 PHP 8.1 迁移到 PHP 8.2。 参考资源 在线测试环境:https://onlinephp.io/ 官方发布说明:https://www.php.net/releases/8.2/en.php 迁移指南:https://www.php.net/manual/en/migration82.php 更新详情:https://php.watch/versions/8.2 PHP 8.2 新特性 只读类(Readonly Classes) PHP 8.2 允许将整个类声明为只读,类中的所有属性自动成为只读属性: readonly class User { public function __construct( public string $username, public string $email, ) {} } $user = new User('john_doe', 'john@example.com'); echo $user->username; // john_doe $user->email = 'new@example.com'; // Fatal error: Cannot modify readonly property User::$email 只读类的特性: 所有属性自动成为只读 不能有动态属性 所有属性必须有类型声明 子类也必须是只读的 析取范式类型(DNF Types) 析取范式(Disjunctive Normal Form)允许组合联合类型和交叉类型,交叉类型必须用括号分组: // 接受同时实现 Countable 和 Iterator 的对象,或者 null function process((Countable&Iterator)|null $item): int { return $item ? count($item) : 0; } // 更复杂的 DNF 类型示例 function handle( (HTMLRequest&RequestInterface)|APIRequest|null $request ): Response { // 处理请求 } DNF 类型必须遵循特定格式:交叉类型用括号包裹,再与其他类型用 | 连接。 ...
MongoDB 高手课
04 特色及优势 维度 能力 数据模型 多形性(同集合不同字段)、动态性(在线改模式)、JSONSchema 治理 开发体验 单存储区读写、反范式 + 无关联优化、API 自然 高可用 Replica Set(2~50 节点,建议奇数)、自恢复、多中心容灾、滚动维护 横向扩展 无缝扩容、应用透明、多种数据分布策略,可达 TB-PB 06 基本操作 环境与样本数据: cloud.mongodb.com 云服务 样本数据 dump.tar.gz tar -xvf dump.tar.gz mongorestore --uri="mongodb://root:root@10.130.0.12/?&authMechanism=SCRAM-SHA-1" CRUD // 插入 db.fruit.insertOne({ name: "apple" }) db.fruit.insertMany([{ name: "apple" }, { name: "pear" }, { name: "orange" }]) // 查询 db.customers.find({ username: "fmiller", name: "Elizabeth Ray" }) db.customers.find({ username: /^f/ }) db.customers.find({ $or: [{ username: /^f/ }, { name: /^E/ }] }) // 投影(projection) db.customers.find({ username: /^f/ }, { name: 0, email: 0 }) // 排除 db.customers.find({ username: /^f/ }, { _id: 1, name: 1 }) // 仅返回 // 删除 db.customers.remove({ username: "abrown" }) // 更新 db.customers.updateOne({ username: "fmiller" }, { $set: { from: "China" } }) db.customers.updateMany({ username: "fmiller" }, { $set: { from: "China" } }) // 表 / 库管理 db.fruit.drop() show collections db.dropDatabase() show dbs SQL ↔ Mongo 操作符对照 SQL Mongo a <> 1 {a: {$ne: 1}} a > 1 / >= {a: {$gt: 1}} / {$gte: 1} a < 1 / <= {a: {$lt: 1}} / {$lte: 1} a = 1 OR b = 1 {$or: [{a: 1}, {b: 1}]} a IS NULL {a: {$exists: false}} a IN (1, 2, 3) {a: {$in: [1, 2, 3]}} 同时满足子文档条件 {$elemMatch: {city: "Rome", country: "USA"}} 常用更新操作符 操作 含义 $set / $unset 设置 / 移除字段 $push / $pop 数组追加 / 弹出 $pull / $pullAll 按匹配从数组中删除 $addToSet 不存在则添加,去重 08 聚合查询 Aggregation Framework: ...
手把手教你落地 DDD
01 DDD 小传 DDD(Domain-Driven Design):Eric Evans 2003 年提出,一套用于开发复杂软件的方法论与思想,核心是领域建模。常被概括成"把面向对象做对(OO Done Right)"。 传统 OO 在企业应用里的痛 → DDD 的回应 痛点 DDD 的回应 重技术轻业务 通用语言(业务 / 开发同一套词) 领域建模无章法 模型驱动设计、限界上下文 业务 / 技术协作不足 事件风暴、协作产出统一语言 难以适应频繁变化 柔性设计 + 持续重构,避免过度设计 为什么近几年才"出圈" 数字化推高复杂度与变化频率。 敏捷 / DevOps 把"小步演进"做成日常。 Spring Boot、微服务、整洁架构、事件驱动、CQRS 等生态成熟,DDD 不再是"屠龙术"。 02 案例需求:企业工时系统 要做的 SaaS:员工每周填报"在哪些项目上花了多少时间"。 需求 核心对象 / 范围 主要功能 (CRUD / 流程) 关键规则 / 约束 关键字段 / 记录 多租户 租户(企业) 管理多个租户 每个租户代表一个使用 SaaS 的企业 — 人员与组织管理 部门、员工 部门:增删改查;员工:增删改查;员工分配部门 员工只能从属于一个部门 部门层级示例:开发中心 → 开发组;职能部门(人事 / 财务等) 项目管理 客户、合同、项目 客户:增删改查;合同:增删改查 + 开始 / 结束;项目:增删改查 + 开始 / 结束 客户对应客户经理;合同对应销售;项目对应项目经理;合同下可有多个项目 合同 / 项目开始时间、结束时间等 人员分配 项目成员关系 为项目分配人员;人员退出项目 项目可多人;员工可同时参与多个项目;需记录投入比例 投入百分比(人 - 项目) 工时登记 工时(周报 / 日记录) 员工每周填报工时;查询;修改;填写备注 仅当员工已分配到项目后,才可在该项目报工时 日期、项目、投入时长、备注 ...