标签: WordPress

  • 个人备案的边界:什么能做,什么别碰

    这个站是个人备案的。办之前以为”备案完就能随便做”,办完之后才明白:备案通过只是起点,它同时划了一条边界

    下面这些是我在建站过程中逐个确认过的,如果你也在办个人站,可以少走些弯路。

    两个备案是两回事

    很多人只做了第一个:

    • ICP 备案(工信部):向通信管理局提交,拿到”粤ICP备XXXXXXXX号”这样的备案号。这是网站能在中国境内访问的前提。
    • 公安联网备案:拿到 ICP 号之后,还需在 30 日内到全国互联网安全管理服务平台提交,拿到”公网安备”号。

    两个号都要挂在页面底部,ICP 号按工信部要求需要链接到备案管理系统。这个站目前只挂了 ICP 号——公安备案还在办,办下来会补上。

    个人备案 = 非经营性

    这是最核心的一条限制。个人备案的性质是非经营性,也就是说网站不能用于经营活动。

    落到内容层面,就是要避免出现经营性质的表述:商品价格、下单购买、客服咨询、招商加盟、充值付费、会员开通这类内容,都不该出现在个人备案的网站上。

    这不意味着个人站不能有任何价值——技术分享、作品展示、开源项目、工具服务都可以。边界在于有没有交易性质

    为什么关掉评论

    这个站全站关闭了评论,包括已发布的文章。

    原因是:开放评论意味着网站开始承载用户生成内容(UGC)。一旦有了 UGC,网站的性质就从”个人发布”向”交互式服务”靠拢,监管要求会明显上一个台阶——内容审核、留存、举报机制都得跟上。

    对一个没有运营团队、只想安静写点东西的个人站来说,这个代价和收益完全不成比例。关掉评论之后:

    • 不用处理垃圾评论和机器人灌水。
    • 不用承担内容留存与审核义务。
    • 网站性质保持简单清晰。

    想交流的话留个邮箱就够了。

    生成式 AI:个人基本做不了

    这一点值得单独说,因为很多人(包括我最初)以为”调用个 API 做个 AI 应用”没什么门槛。

    实际情况是:在国内提供具有舆论属性或社会动员能力的生成式 AI 服务,需要完成算法备案;如果涉及自研或微调模型,还需要大模型登记。而这两件事的共同前提是——备案主体必须是境内法人企业,个人主体办不了。

    也就是说,个人身份下做生成式 AI 应用,要么挂靠有资质的平台,要么出海,要么干脆不做。

    还有个容易踩的坑:自研或微调模型会把”登记”升级为”备案”,周期从三四个月拉长到六到八个月。所以即便有公司主体,也应该优先用现成模型做应用层。

    我在这个站上的具体做法

    1. 页脚悬挂 ICP 备案号,链接到工信部备案管理系统,位置在 <footer> 语义标签内。
    2. 全站关闭评论,新文章默认也是关闭。
    3. 内容只涉及技术分享与作品展示,不出现任何经营性质表述。
    4. 联系方式只留邮箱,不放电话、即时通讯等带经营色彩的信息。
    5. 不做生成式 AI 功能,工具箱全部是确定性的图像处理(压缩、转换、裁剪、遮盖)。

    这些不是过度谨慎,而是把”个人备案”这四个字对应的义务想清楚之后的自然结果。

    如果确实要经营

    那就不是”在个人站上加功能”的问题了,而是要换主体:注册公司、办经营性 ICP 许可证、按企业类型重新备案。这是另一条完全不同的路,成本也完全不同。

    我的判断是:先用个人站把事情做出来、验证有没有人用,等真要赚钱了再换主体。顺序反过来的话,成本会压在还没验证的想法上。

  • 一个 MySQL 实例喂五个应用:库隔离与两个连接池

    这台机器上只有一个 MySQL 实例,却被 WordPress、二维码平台、在线工具箱同时用着。下面的取舍和踩坑记录,对任何”一个数据库服务多个应用”的场景都适用。

    按库隔离,不按表前缀

    每个应用一套独立的库,按环境再分:

    wordpress_prod   / wordpress_beta
    qrcode_prod      / qrcode_beta
    tools_prod       / tools_beta
    users_prod       / users_beta   ← 注意这个

    用独立库而不是”一个库 + 表名前缀”,好处是备份、迁移、授权都能按库粒度做,某天要把某个应用挪走也不用在一堆表里挑挑拣拣。

    为什么用户表要单独一个库

    这是我改动最大的一个设计。最初每个应用各自建用户表,结果就是:在二维码平台注册的用户,到工具箱还得再注册一次。

    后来把用户表抽出来放进独立的 users_* 库,两个应用连同一个用户表,配合同一把 JWT 密钥,登录态就打通了。

    代价是应用侧要维护两个连接池

    getDB()      → 连业务库(qrcode_* / tools_*)
    getAuthDB()  → 连共享用户库(users_*)

    多一个连接池不算复杂,但它换来的是”一次注册,全站通用”。对用户来说是天壤之别。

    坑一:mysql2 不接受 undefined

    这个坑的表现非常迷惑:接口返回 500,但代码看起来毫无问题。

    // 报错
    await db.query('INSERT INTO t (a, b) VALUES (?, ?)', [foo, bar]);
    // bar 是 undefined 时 → 500

    原因是 mysql2 的绑定参数只接受 null,传 undefined 会直接抛错。而 JavaScript 里”对象上不存在的属性”返回的就是 undefined,这在拼装请求体时太常见了。

    解决办法是在数据库访问层统一做一次转换,而不是在每个调用点小心翼翼:

    function toParams(arr) {
      return arr.map((v) =&gt; (v === undefined ? null : v));
    }

    在数据访问层收口,比靠人记得写 ?? null 可靠得多。

    坑二:连接的时区必须等于实例的时区

    这个坑更隐蔽——它不报错,只是读出来的时间整体偏移几个小时

    MySQL 连接如果不显式指定时区,驱动会按自己的默认值解释 TIMESTAMP。容器里默认是 UTC,而你可能下意识按东八区理解,于是所有时间都差 8 小时。

    // 线上实例是 UTC,就明确写 Z
    createPool({
      timezone: 'Z',
      // ...
    });

    判断实例实际时区:

    SELECT @@global.time_zone, @@session_time_zone, NOW();

    然后让连接池的 timezone 和它一致。不要靠猜。

    这个方案的真实风险

    必须说清楚:单实例 MySQL 就是单点。它挂了,所有应用一起挂。

    对个人项目来说这个风险可以接受,但前提是要做到两件事——定期备份,并且真的演练过一次恢复。没有演练过的备份等于没有备份。

  • 一个 HTTP 请求头引发的 72 小时崩溃循环

    有一阵子 beta.mazter.cn 一直打不开,返回 404。当时没太在意,以为是 WordPress 初始化慢。三天后去看 Pod,才发现它已经重启了 5 次,一直卡在崩溃循环里。

    这篇文章记录完整的排查链条和根因——它有个挺有意思的特点:每一个组件都在按设计工作,组合起来却产生了死循环

    现象

    $ kubectl get pods -n beta
    NAME                         READY   STATUS    RESTARTS   AGE
    wordpress-5c79d8dc94-xxxxx   0/1     Running   5          3d22h

    同一套配置在 prod 命名空间一切正常(0 次重启),所以第一反应不是配置错,而是”beta 环境有什么特殊的”。这个判断后来被证明是错的——prod 只是运气好。

    看 Events

    kubectl describe pod 是这类问题唯一的正确答案来源:

    Events:
      Warning  Unhealthy  liveness probe failed: HTTP probe failed with statuscode: 503
      Warning  Unhealthy  Readiness probe failed: HTTP probe failed with statuscode: 503
      Normal   Killing    Container failed liveness probe, will be restarted

    还有一条很关键的:Probe terminated redirects。探针跟随重定向,然后被终止了。

    还原死循环

    把整条链路串起来是这样:

    • 探针按配置去请求 http://<PodIP>:80/wp-login.php
    • WordPress 收到的是明文 HTTP,而站点地址配置的是 HTTPS,于是它回一个 302,让客户端跳到 https://beta.mazter.cn/wp-login.php
    • 这个 HTTPS 请求出去之后,经过 Traefik Ingress 又被路由回来——目标是同一个 WordPress Service。
    • 而此刻这个 Pod 还没通过 readiness 检查,后端池里没有可用端点,Traefik 返回 503
    • 探针拿到 503,判定存活失败,kubelet 杀掉容器重启。
    • 重启后一切重演。

    为什么 WordPress 坚持要跳 HTTPS?因为官方镜像的 wp-config.php 里有一段条件判断,只有在检测到代理头 X-Forwarded-Proto: https 时,才会把 $_SERVER['HTTPS'] 置为 on。探针直连 Pod,不带这个头,于是 WordPress 认为”你现在是 HTTP,不安全”,坚持把你推向 HTTPS。

    修复

    给探针补上这个头即可,readiness 和 liveness 都要改:

    readinessProbe:
      httpGet:
        path: /wp-login.php
        port: 80
        httpHeaders:
          - name: X-Forwarded-Proto
            value: https
      initialDelaySeconds: 30
      periodSeconds: 10
    livenessProbe:
      httpGet:
        path: /wp-login.php
        port: 80
        httpHeaders:
          - name: X-Forwarded-Proto
            value: https
      initialDelaySeconds: 60
      periodSeconds: 15

    改完 apply 之后 Pod 一次就 Ready 了,beta.mazter.cn/wp-login.php 恢复 200。

    为什么 prod 没炸

    这是最值得警惕的部分。prod 之所以正常,是因为它的 Pod 已经跑了很久、早就处于 Ready 状态,重定向绕回来时能命中一个健康的后端,拿到 200 而不是 503。

    换句话说:prod 不是没有问题,只是还没被触发。任何一次重启(节点重启、镜像更新、资源不足被驱逐)都可能让 prod 走进同一个循环。所以修复同时打到了两个环境。

    更根本的解法

    补请求头是把 WordPress 的行为”哄”对了。更稳妥的做法是让探针打一个不会重定向的端点,比如在站点根放一个空的 healthz.html,探它即可。这样探针不依赖应用层的 HTTPS 判断逻辑,也不受代理头约定变化的影响。

    两种方案都行,重要的是别让健康检查去请求一个”可能会重定向”的地址——重定向会让探针的失败原因变得极难解释。