47b3c89460
- web/dist/changelog/ 版本历史页,收录 v1.0.0–v1.0.29 全部发版记录 - web/dist/terms/ 服务条款页(付费/授权/数据/免责等) - web/dist/privacy/ 隐私政策页(数据收集/使用/共享/用户权利等) - 全站页脚补充 更新日志、服务条款、隐私政策三个链接 - about_screen 「更新日志」按钮链接由 /downloads/ 改为 /changelog/ - 新增 backend/cmd/issue 工具,用于签发指定门店的授权码 token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
642 lines
20 KiB
HTML
642 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>#21 授权功能测试流程</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
|
||
background: #f5f5f7;
|
||
color: #1d1d1f;
|
||
line-height: 1.6;
|
||
padding: 32px 16px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 860px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
header {
|
||
margin-bottom: 36px;
|
||
}
|
||
|
||
header .badge {
|
||
display: inline-block;
|
||
background: #0071e3;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
letter-spacing: .04em;
|
||
padding: 3px 10px;
|
||
border-radius: 20px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
letter-spacing: -.02em;
|
||
}
|
||
|
||
header p {
|
||
margin-top: 6px;
|
||
color: #6e6e73;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* Progress bar */
|
||
.progress-bar {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-bottom: 36px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.progress-step {
|
||
flex: 1;
|
||
min-width: 90px;
|
||
text-align: center;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 6px 4px;
|
||
border-radius: 8px;
|
||
background: #e5e5ea;
|
||
color: #6e6e73;
|
||
position: relative;
|
||
}
|
||
|
||
.progress-step.phase-normal { background: #d1fae5; color: #065f46; }
|
||
.progress-step.phase-grace { background: #fef3c7; color: #92400e; }
|
||
.progress-step.phase-readonly{ background: #fee2e2; color: #991b1b; }
|
||
.progress-step.phase-locked { background: #fecaca; color: #7f1d1d; }
|
||
.progress-step.phase-renew { background: #dbeafe; color: #1e40af; }
|
||
.progress-step.phase-prep { background: #ede9fe; color: #5b21b6; }
|
||
|
||
/* Card */
|
||
.card {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 28px 32px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.04);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.card-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.icon-prep { background: #ede9fe; }
|
||
.icon-normal { background: #d1fae5; }
|
||
.icon-grace { background: #fef3c7; }
|
||
.icon-readonly { background: #fee2e2; }
|
||
.icon-locked { background: #fecaca; }
|
||
.icon-renew { background: #dbeafe; }
|
||
.icon-verify { background: #f3f4f6; }
|
||
|
||
.card-title-block h2 {
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.card-title-block .subtitle {
|
||
font-size: 13px;
|
||
color: #6e6e73;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* Phase pill */
|
||
.phase-pill {
|
||
display: inline-block;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
padding: 2px 9px;
|
||
border-radius: 20px;
|
||
margin-left: 8px;
|
||
vertical-align: middle;
|
||
letter-spacing: .03em;
|
||
}
|
||
.pill-normal { background: #d1fae5; color: #065f46; }
|
||
.pill-grace { background: #fef3c7; color: #92400e; }
|
||
.pill-readonly { background: #fee2e2; color: #991b1b; }
|
||
.pill-locked { background: #fecaca; color: #7f1d1d; }
|
||
|
||
/* Steps */
|
||
ol.steps {
|
||
list-style: none;
|
||
counter-reset: step;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
ol.steps li {
|
||
counter-increment: step;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
font-size: 14px;
|
||
}
|
||
|
||
ol.steps li::before {
|
||
content: counter(step);
|
||
min-width: 24px;
|
||
height: 24px;
|
||
background: #f2f2f7;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #3c3c43;
|
||
flex-shrink: 0;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
/* Expect block */
|
||
.expect {
|
||
margin-top: 14px;
|
||
padding: 12px 16px;
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.expect-ok { background: #f0fdf4; border-left: 3px solid #22c55e; }
|
||
.expect-warn{ background: #fffbeb; border-left: 3px solid #f59e0b; }
|
||
.expect-err { background: #fff1f2; border-left: 3px solid #f43f5e; }
|
||
|
||
.expect .label {
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
font-size: 12px;
|
||
letter-spacing: .05em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.expect-ok .label { color: #16a34a; }
|
||
.expect-warn .label { color: #d97706; }
|
||
.expect-err .label { color: #e11d48; }
|
||
|
||
.expect ul {
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.expect ul li::before {
|
||
content: "✓ ";
|
||
font-weight: 700;
|
||
color: #22c55e;
|
||
}
|
||
.expect-warn ul li::before { color: #f59e0b; content: "⚠ "; }
|
||
.expect-err ul li::before { color: #f43f5e; content: "✗ "; }
|
||
|
||
/* Code block */
|
||
pre {
|
||
background: #1c1c1e;
|
||
color: #e5e7eb;
|
||
border-radius: 10px;
|
||
padding: 14px 18px;
|
||
font-size: 13px;
|
||
font-family: "SF Mono", "Fira Code", Consolas, monospace;
|
||
overflow-x: auto;
|
||
margin-top: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
pre .comment { color: #6b7280; }
|
||
pre .keyword { color: #93c5fd; }
|
||
pre .string { color: #a3e635; }
|
||
|
||
/* Note */
|
||
.note {
|
||
background: #f2f2f7;
|
||
border-radius: 10px;
|
||
padding: 12px 16px;
|
||
font-size: 13px;
|
||
color: #3c3c43;
|
||
margin-top: 12px;
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.note .note-icon { flex-shrink: 0; font-size: 15px; }
|
||
|
||
/* Divider */
|
||
.divider {
|
||
border: none;
|
||
border-top: 1px solid #e5e5ea;
|
||
margin: 24px 0;
|
||
}
|
||
|
||
/* Reset card */
|
||
.reset-card {
|
||
background: #fff9f0;
|
||
border: 1.5px dashed #fb923c;
|
||
border-radius: 16px;
|
||
padding: 22px 28px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.reset-card h3 {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #c2410c;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* Table */
|
||
.phase-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.phase-table th {
|
||
background: #f2f2f7;
|
||
padding: 9px 14px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
color: #6e6e73;
|
||
letter-spacing: .04em;
|
||
}
|
||
|
||
.phase-table td {
|
||
padding: 9px 14px;
|
||
border-bottom: 1px solid #f2f2f7;
|
||
}
|
||
|
||
.phase-table tr:last-child td { border-bottom: none; }
|
||
|
||
footer {
|
||
text-align: center;
|
||
color: #aeaeb2;
|
||
font-size: 12px;
|
||
margin-top: 40px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<header>
|
||
<div class="badge">#21 授权信息功能</div>
|
||
<h1>授权功能本地测试流程</h1>
|
||
<p>覆盖注册 → 试用 → 宽限期 → 只读 → 锁定 → 续费激活全链路</p>
|
||
</header>
|
||
|
||
<!-- Phase overview -->
|
||
<div class="progress-bar">
|
||
<div class="progress-step phase-prep">🔑 前置准备</div>
|
||
<div class="progress-step phase-normal">✅ 试用期</div>
|
||
<div class="progress-step phase-grace">⚠️ 宽限期</div>
|
||
<div class="progress-step phase-readonly">🔒 只读模式</div>
|
||
<div class="progress-step phase-locked">🚫 锁定</div>
|
||
<div class="progress-step phase-renew">🔄 续费激活</div>
|
||
<div class="progress-step">🛡️ 防伪验证</div>
|
||
</div>
|
||
|
||
<!-- Phase reference table -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-prep">📊</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段速查表</h2>
|
||
<div class="subtitle">各阶段对应的 expires_at 偏移量与行为</div>
|
||
</div>
|
||
</div>
|
||
<table class="phase-table">
|
||
<thead>
|
||
<tr>
|
||
<th>阶段</th>
|
||
<th>expires_at 设置</th>
|
||
<th>写操作</th>
|
||
<th>登录</th>
|
||
<th>前端表现</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><span class="phase-pill pill-normal">normal</span></td>
|
||
<td>未来时间(未过期)</td>
|
||
<td>✅ 正常</td>
|
||
<td>✅ 正常</td>
|
||
<td>绿色状态,无横幅</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="phase-pill pill-grace">grace</span></td>
|
||
<td>1–7 天前</td>
|
||
<td>✅ 正常</td>
|
||
<td>✅ 正常</td>
|
||
<td>橙色横幅 + 启动弹窗</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="phase-pill pill-readonly">readonly</span></td>
|
||
<td>7–15 天前</td>
|
||
<td>🚫 403</td>
|
||
<td>✅ 正常</td>
|
||
<td>红色横幅,写操作提示错误</td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="phase-pill pill-locked">locked</span></td>
|
||
<td>15+ 天前</td>
|
||
<td>🚫 403</td>
|
||
<td>🚫 拒绝</td>
|
||
<td>登录失败提示</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Step 0: Prerequisites -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-prep">🔑</div>
|
||
<div class="card-title-block">
|
||
<h2>前置准备 <small style="font-weight:400;color:#6e6e73;">仅需一次</small></h2>
|
||
<div class="subtitle">生成密钥对,配置后端环境变量</div>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li>
|
||
<div>
|
||
生成 Ed25519 密钥对:
|
||
<pre><span class="comment"># 在项目根目录执行</span>
|
||
export PATH="/opt/homebrew/bin:$PATH"
|
||
cd backend
|
||
go run ./cmd/genkey</pre>
|
||
输出两段 base64:第一段是<strong>私钥</strong>(存 Bitwarden),第二段是<strong>公钥</strong>。
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<div>
|
||
配置后端环境变量(在启动脚本或 <code>.env</code> 中):
|
||
<pre>export LICENSE_ED25519_PRIVATE_KEY="<私钥 base64>"
|
||
export LICENSE_ED25519_PUBLIC_KEY="<公钥 base64>"</pre>
|
||
</div>
|
||
</li>
|
||
<li><strong>重启后端服务</strong>,使配置生效。</li>
|
||
</ol>
|
||
<div class="note">
|
||
<span class="note-icon">💡</span>
|
||
<span>未配置私钥时,注册不会自动创建试用授权,注册后设置页会显示「无授权信息」。</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 1: Register -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-normal">✅</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段一:注册 → 自动试用 <span class="phase-pill pill-normal">normal</span></h2>
|
||
<div class="subtitle">验证注册时自动签发 30 天试用授权</div>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li>用客户端注册一个<strong>新账号</strong>(新门店)</li>
|
||
<li>登录后进入 <strong>设置 → 授权</strong> 标签</li>
|
||
<li>
|
||
(可选)查数据库确认自动创建:
|
||
<pre><span class="keyword">SELECT</span> id, shop_id, type, expires_at, is_active, max_devices
|
||
<span class="keyword">FROM</span> licenses
|
||
<span class="keyword">ORDER BY</span> id <span class="keyword">DESC LIMIT</span> 1;</pre>
|
||
记下 <code>shop_id</code>,后续所有 SQL 替换 <code>1</code> 为该值。
|
||
</li>
|
||
</ol>
|
||
<div class="expect expect-ok">
|
||
<div class="label">预期结果</div>
|
||
<ul>
|
||
<li>设置页显示「试用版」绿色状态</li>
|
||
<li>到期时间约为注册后 30 天</li>
|
||
<li>无横幅,无弹窗</li>
|
||
<li>DB 中 type='trial',is_active=1</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 2: Grace -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-grace">⚠️</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段二:宽限期 <span class="phase-pill pill-grace">grace</span></h2>
|
||
<div class="subtitle">过期 0–7 天 · 仍可写 · 出现橙色横幅</div>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li>
|
||
执行 SQL,模拟 3 天前过期:
|
||
<pre><span class="comment">-- shop_id 替换为你的实际值</span>
|
||
<span class="keyword">UPDATE</span> licenses
|
||
<span class="keyword">SET</span> expires_at = DATE_SUB(NOW(), INTERVAL 3 DAY)
|
||
<span class="keyword">WHERE</span> shop_id = 1;</pre>
|
||
</li>
|
||
<li><strong>退出登录 → 重新登录</strong>(JWT 重新签发,携带新的过期时间)</li>
|
||
<li>观察顶部横幅,进入设置查看状态颜色</li>
|
||
<li>尝试新建入库单或编辑商品,确认写操作正常</li>
|
||
</ol>
|
||
<div class="expect expect-warn">
|
||
<div class="label">预期结果</div>
|
||
<ul>
|
||
<li>顶部出现橙色横幅「授权已过期,请续费」</li>
|
||
<li>首次登录弹出到期提示弹窗</li>
|
||
<li>设置页状态为橙色「宽限期」</li>
|
||
<li>写操作(入库、编辑)仍然可以正常使用</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 3: ReadOnly -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-readonly">🔒</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段三:只读模式 <span class="phase-pill pill-readonly">readonly</span></h2>
|
||
<div class="subtitle">过期 7–15 天 · 写操作被拦截</div>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li>
|
||
执行 SQL,模拟 10 天前过期:
|
||
<pre><span class="keyword">UPDATE</span> licenses
|
||
<span class="keyword">SET</span> expires_at = DATE_SUB(NOW(), INTERVAL 10 DAY)
|
||
<span class="keyword">WHERE</span> shop_id = 1;</pre>
|
||
</li>
|
||
<li><strong>退出登录 → 重新登录</strong></li>
|
||
<li>尝试<strong>新建入库单</strong>或<strong>编辑商品</strong></li>
|
||
<li>尝试<strong>查看</strong>商品列表、库存记录(只读应正常)</li>
|
||
</ol>
|
||
<div class="expect expect-err">
|
||
<div class="label">预期结果</div>
|
||
<ul>
|
||
<li>顶部红色横幅</li>
|
||
<li>写操作被拦截,显示「系统处于只读模式,无法修改」</li>
|
||
<li>查看类操作(GET 请求)正常响应</li>
|
||
<li>设置页状态为红色「只读模式」</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 4: Locked -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-locked">🚫</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段四:锁定 <span class="phase-pill pill-locked">locked</span></h2>
|
||
<div class="subtitle">过期 15+ 天 · 无法登录</div>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li>
|
||
执行 SQL,模拟 20 天前过期:
|
||
<pre><span class="keyword">UPDATE</span> licenses
|
||
<span class="keyword">SET</span> expires_at = DATE_SUB(NOW(), INTERVAL 20 DAY)
|
||
<span class="keyword">WHERE</span> shop_id = 1;</pre>
|
||
</li>
|
||
<li><strong>不要退出</strong>,先测试当前会话:尝试任何写操作 → 应被拦截</li>
|
||
<li>然后<strong>退出登录</strong></li>
|
||
<li>尝试重新登录</li>
|
||
</ol>
|
||
<div class="expect expect-err">
|
||
<div class="label">预期结果</div>
|
||
<ul>
|
||
<li>当前会话写操作返回 403</li>
|
||
<li>退出后重新登录:失败,提示「授权已锁定,请续费后重新激活」</li>
|
||
</ul>
|
||
</div>
|
||
<div class="note">
|
||
<span class="note-icon">ℹ️</span>
|
||
<span><strong>登录被拒</strong>是从数据库实时读取的,无需重启服务或刷新 JWT 即可生效。</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 5: Renewal -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-renew">🔄</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段五:续费激活</h2>
|
||
<div class="subtitle">签发新授权码 → 用户激活 → 恢复正常</div>
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="divider">
|
||
<p style="font-weight:600;font-size:14px;margin-bottom:12px;">5.1 运营方:签发授权码</p>
|
||
<ol class="steps">
|
||
<li>
|
||
执行 issue 命令(确保环境变量已配置):
|
||
<pre><span class="comment"># shop 替换为实际 shop_id;days 为有效天数;devices 为最大设备数</span>
|
||
export PATH="/opt/homebrew/bin:$PATH"
|
||
cd backend
|
||
go run ./cmd/issue \
|
||
-shop 1 \
|
||
-days 365 \
|
||
-type annual \
|
||
-devices 5</pre>
|
||
输出的 token 字符串即为<strong>授权码</strong>,复制备用。
|
||
</li>
|
||
</ol>
|
||
|
||
<hr class="divider">
|
||
<p style="font-weight:600;font-size:14px;margin-bottom:12px;">5.2 用户:在应用内激活</p>
|
||
<ol class="steps">
|
||
<li>
|
||
此时登录仍会被拒。需要先用 SQL 临时解锁以便进入设置:
|
||
<pre><span class="comment">-- 临时恢复到 grace 阶段(允许登录)</span>
|
||
<span class="keyword">UPDATE</span> licenses
|
||
<span class="keyword">SET</span> expires_at = DATE_SUB(NOW(), INTERVAL 3 DAY)
|
||
<span class="keyword">WHERE</span> shop_id = 1;</pre>
|
||
</li>
|
||
<li>重新登录,进入 <strong>设置 → 授权 → 激活授权码</strong></li>
|
||
<li>粘贴步骤 5.1 的 token → 点击「激活」</li>
|
||
<li>激活成功后<strong>退出登录 → 重新登录</strong></li>
|
||
</ol>
|
||
<div class="expect expect-ok">
|
||
<div class="label">预期结果</div>
|
||
<ul>
|
||
<li>激活成功,设置页显示「正式版(annual)」绿色状态</li>
|
||
<li>到期日显示约 1 年后</li>
|
||
<li>重新登录成功,无横幅,写操作恢复正常</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 6: Forgery test -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-verify">🛡️</div>
|
||
<div class="card-title-block">
|
||
<h2>阶段六:防伪验证</h2>
|
||
<div class="subtitle">确认伪造/篡改的授权码被拒绝</div>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li>取上一步签发的 token,随意修改几个字符(如把末尾 3 个字符换掉)</li>
|
||
<li>在激活框粘贴修改后的 token → 点击「激活」</li>
|
||
<li>再试一次:随便输入一段乱码文本</li>
|
||
</ol>
|
||
<div class="expect expect-err">
|
||
<div class="label">预期结果</div>
|
||
<ul>
|
||
<li>两次激活均失败</li>
|
||
<li>提示签名无效或格式错误</li>
|
||
<li>当前授权状态不受影响</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reset -->
|
||
<div class="reset-card">
|
||
<h3>🔄 测试完毕 — 快速重置</h3>
|
||
<pre><span class="comment">-- 恢复为 30 天后到期(正常试用状态)</span>
|
||
<span class="keyword">UPDATE</span> licenses
|
||
<span class="keyword">SET</span> expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||
<span class="keyword">WHERE</span> shop_id = 1;</pre>
|
||
</div>
|
||
|
||
<!-- Notes -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-icon icon-prep">📝</div>
|
||
<div class="card-title-block">
|
||
<h2>注意事项</h2>
|
||
</div>
|
||
</div>
|
||
<ol class="steps">
|
||
<li><strong>每次改 DB 后必须重新登录</strong> — <code>LicenseExpiresAt</code> 缓存在 JWT 里,不重登前端阶段判断不会更新(登录拦截除外,那个实时读 DB)</li>
|
||
<li><code>shop_id</code> 替换为你实际注册账号对应的值,通常第一个账号为 1</li>
|
||
<li>issue 工具依赖 <code>LICENSE_ED25519_PRIVATE_KEY</code> 环境变量,确保 shell 会话中已导出</li>
|
||
<li>设置页的「激活授权码」卡片仅在授权为非正常状态(grace/readonly/locked)或 readonly phase 时显示;normal 下隐藏</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<footer>
|
||
docs/testing/license-test-flow.html · #21 授权信息功能 · 测试流程文档
|
||
</footer>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|