#21 授权信息功能

授权功能本地测试流程

覆盖注册 → 试用 → 宽限期 → 只读 → 锁定 → 续费激活全链路

🔑 前置准备
✅ 试用期
⚠️ 宽限期
🔒 只读模式
🚫 锁定
🔄 续费激活
🛡️ 防伪验证
📊

阶段速查表

各阶段对应的 expires_at 偏移量与行为
阶段 expires_at 设置 写操作 登录 前端表现
normal 未来时间(未过期) ✅ 正常 ✅ 正常 绿色状态,无横幅
grace 1–7 天前 ✅ 正常 ✅ 正常 橙色横幅 + 启动弹窗
readonly 7–15 天前 🚫 403 ✅ 正常 红色横幅,写操作提示错误
locked 15+ 天前 🚫 403 🚫 拒绝 登录失败提示
🔑

前置准备 仅需一次

生成密钥对,配置后端环境变量
  1. 生成 Ed25519 密钥对:
    # 在项目根目录执行
    export PATH="/opt/homebrew/bin:$PATH"
    cd backend
    go run ./cmd/genkey
    输出两段 base64:第一段是私钥(存 Bitwarden),第二段是公钥
  2. 配置后端环境变量(在启动脚本或 .env 中):
    export LICENSE_ED25519_PRIVATE_KEY="<私钥 base64>"
    export LICENSE_ED25519_PUBLIC_KEY="<公钥 base64>"
  3. 重启后端服务,使配置生效。
💡 未配置私钥时,注册不会自动创建试用授权,注册后设置页会显示「无授权信息」。

阶段一:注册 → 自动试用 normal

验证注册时自动签发 30 天试用授权
  1. 用客户端注册一个新账号(新门店)
  2. 登录后进入 设置 → 授权 标签
  3. (可选)查数据库确认自动创建:
    SELECT id, shop_id, type, expires_at, is_active, max_devices
    FROM licenses
    ORDER BY id DESC LIMIT 1;
    记下 shop_id,后续所有 SQL 替换 1 为该值。
预期结果
  • 设置页显示「试用版」绿色状态
  • 到期时间约为注册后 30 天
  • 无横幅,无弹窗
  • DB 中 type='trial',is_active=1
⚠️

阶段二:宽限期 grace

过期 0–7 天 · 仍可写 · 出现橙色横幅
  1. 执行 SQL,模拟 3 天前过期:
    -- shop_id 替换为你的实际值
    UPDATE licenses
    SET expires_at = DATE_SUB(NOW(), INTERVAL 3 DAY)
    WHERE shop_id = 1;
  2. 退出登录 → 重新登录(JWT 重新签发,携带新的过期时间)
  3. 观察顶部横幅,进入设置查看状态颜色
  4. 尝试新建入库单或编辑商品,确认写操作正常
预期结果
  • 顶部出现橙色横幅「授权已过期,请续费」
  • 首次登录弹出到期提示弹窗
  • 设置页状态为橙色「宽限期」
  • 写操作(入库、编辑)仍然可以正常使用
🔒

阶段三:只读模式 readonly

过期 7–15 天 · 写操作被拦截
  1. 执行 SQL,模拟 10 天前过期:
    UPDATE licenses
    SET expires_at = DATE_SUB(NOW(), INTERVAL 10 DAY)
    WHERE shop_id = 1;
  2. 退出登录 → 重新登录
  3. 尝试新建入库单编辑商品
  4. 尝试查看商品列表、库存记录(只读应正常)
预期结果
  • 顶部红色横幅
  • 写操作被拦截,显示「系统处于只读模式,无法修改」
  • 查看类操作(GET 请求)正常响应
  • 设置页状态为红色「只读模式」
🚫

阶段四:锁定 locked

过期 15+ 天 · 无法登录
  1. 执行 SQL,模拟 20 天前过期:
    UPDATE licenses
    SET expires_at = DATE_SUB(NOW(), INTERVAL 20 DAY)
    WHERE shop_id = 1;
  2. 不要退出,先测试当前会话:尝试任何写操作 → 应被拦截
  3. 然后退出登录
  4. 尝试重新登录
预期结果
  • 当前会话写操作返回 403
  • 退出后重新登录:失败,提示「授权已锁定,请续费后重新激活」
ℹ️ 登录被拒是从数据库实时读取的,无需重启服务或刷新 JWT 即可生效。
🔄

阶段五:续费激活

签发新授权码 → 用户激活 → 恢复正常

5.1 运营方:签发授权码

  1. 执行 issue 命令(确保环境变量已配置):
    # shop 替换为实际 shop_id;days 为有效天数;devices 为最大设备数
    export PATH="/opt/homebrew/bin:$PATH"
    cd backend
    go run ./cmd/issue \
      -shop 1 \
      -days 365 \
      -type annual \
      -devices 5
    输出的 token 字符串即为授权码,复制备用。

5.2 用户:在应用内激活

  1. 此时登录仍会被拒。需要先用 SQL 临时解锁以便进入设置:
    -- 临时恢复到 grace 阶段(允许登录)
    UPDATE licenses
    SET expires_at = DATE_SUB(NOW(), INTERVAL 3 DAY)
    WHERE shop_id = 1;
  2. 重新登录,进入 设置 → 授权 → 激活授权码
  3. 粘贴步骤 5.1 的 token → 点击「激活」
  4. 激活成功后退出登录 → 重新登录
预期结果
  • 激活成功,设置页显示「正式版(annual)」绿色状态
  • 到期日显示约 1 年后
  • 重新登录成功,无横幅,写操作恢复正常
🛡️

阶段六:防伪验证

确认伪造/篡改的授权码被拒绝
  1. 取上一步签发的 token,随意修改几个字符(如把末尾 3 个字符换掉)
  2. 在激活框粘贴修改后的 token → 点击「激活」
  3. 再试一次:随便输入一段乱码文本
预期结果
  • 两次激活均失败
  • 提示签名无效或格式错误
  • 当前授权状态不受影响

🔄 测试完毕 — 快速重置

-- 恢复为 30 天后到期(正常试用状态)
UPDATE licenses
SET expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY)
WHERE shop_id = 1;
📝

注意事项

  1. 每次改 DB 后必须重新登录LicenseExpiresAt 缓存在 JWT 里,不重登前端阶段判断不会更新(登录拦截除外,那个实时读 DB)
  2. shop_id 替换为你实际注册账号对应的值,通常第一个账号为 1
  3. issue 工具依赖 LICENSE_ED25519_PRIVATE_KEY 环境变量,确保 shell 会话中已导出
  4. 设置页的「激活授权码」卡片仅在授权为非正常状态(grace/readonly/locked)或 readonly phase 时显示;normal 下隐藏