# 🔐 客户端授权接入指南（v3 · 通用授权模块 / 静默运行 + 到期付费引导）

> 目标：让你的每款软件都接上「首次激活 → **有效期内完全静默后台运行** → **到期弹窗付费引导**」的授权体系。
> 服务器：`https://download.dicad.cn`
> 版本：2026-09-29（v3 新增 **通用授权模块**：C# / Go / Python / JS 四语言单文件，统一接口 `/api/license/auth`）

---

## ⭐ 推荐用法：通用授权模块（v3 新增）

如果你要接入的是**自己的软件**（不是网页），用下面的通用授权模块最简单：
一个文件拷进项目，指定「软件名称 + 版本号」，首次输入用户名+手机号，之后完全静默。

| 语言 | 下载 | 依赖 |
|------|------|------|
| C# / .NET | https://download.dicad.cn/sdk/LicenseAuth.cs | 仅 `System.Text.Json` |
| Go | https://download.dicad.cn/sdk/licenseauth.go | 无（标准库） |
| Python | https://download.dicad.cn/sdk/license_auth.py | 无（标准库） |
| JavaScript | https://download.dicad.cn/sdk/license-auth.js | 无（Node/Electron/浏览器通吃） |

**完整接入文档**：https://download.dicad.cn/sdk/接入指南.md
**最小示例**：见接入指南末尾「三步接入（复制即用）」

```csharp
// C# 三步接入
var lic = new LicenseClient("seed-001", "FlowTask", "2.4.1");   // ① 软件名称 + 版本号

if (lic.NeedActivate()) {                                       // ② 首次运行弹一次输入框
    var r = lic.Authorize(username, phone);
    if (!r.Authorized) { ShowDialog(r.Dialog); return; }
}

var st = lic.Check();                                           // ③ 每次启动静默检查
if (!st.Authorized) { ShowDialog(st.Dialog); return; }          //    只有被拦截时才有 Dialog
// st.Silent == true → 有效期内，什么都不要弹
```

**核心行为**：有效期内 `silent = true`（零弹窗、后台静默）；到期/暂停/吊销时 `authorized = false` 并返回 `dialog`
（标题 / 文案 / 客服联系方式 / 付费链接，**全部可在后台配置**）。

**唯一接口**：`POST|GET /api/license/auth`
入参 `productId` `productName` `version` `username` `phone` `deviceId` `machineName` `os`

**后台配置位置**：软件管理 → 该软件 → 编辑资料 → **「⑧ 授权与付费提示」**
（启用授权校验 / 客服联系方式 / 付费链接 / 过期提示文案，支持 `{productName}` `{expireDate}` `{remainDays}` `{username}` 占位符）

**查看客户实际使用**：软件管理 → 授权用户 → 用户行 **🖥️ 设备按钮**（版本 / 设备 / 系统 / 鉴权次数 / 首末次时间）

> 版本号只上报记录，**不参与放行判断**；后台软件改名不会导致客户端失配（只标记 `nameMismatch`）。

---

## 一、授权体系总览（先看这张图）


```
后台管理（download.dicad.cn/admin.html）
   │  ├── 软件管理 → 每款软件维护【授权用户】（用户名/手机号/授权起止日期/消费金额）
   │  └── 授权管理 → 签发【授权码 DICC-XXX】（客户维度）
   ▼
客户端软件（你的软件里嵌入 SDK）
   │  ├── 启动时调用 验证接口
   │  ├── 服务器权威判断：时间是否在 [start_date, expire_date] 内
   │  ├── 本地缓存到期日（离线 3 天宽容）
   │  ├── 每天定时检查剩余天数 → 到期前 N 天提醒
   │  └── 到期 → 拦截（弹窗联系管理员）
```

**两种授权方式**（后台已支持，任选其一或都支持）：

| 方式 | 适用场景 | 后台配置位置 | 客户端调用 |
|------|---------|-------------|-----------|
| **授权码** `DICC-XXXXXX-XXXXXX-XXXXXX` | 企业客户整单授权 | 授权管理 → 签发授权 | `lic.activate(code)` |
| **授权用户**（用户名+手机号） | 软件内按用户授权 | 软件管理 → 👤授权用户 | `lic.userLogin(用户名, 手机号)` |

---

## 二、时间限制是怎么控制的（核心原理）

### 1. 服务器权威（防作弊）

所有时间判断**以服务器为准**。客户端启动时调用接口，服务器返回：

```json
{
  "valid": true,
  "status": "active",
  "startDate": "2026-08-01",
  "expireDate": "2027-08-01",
  "remainDays": 347,          // ← 剩余天数（客户端做提醒用）
  "serverTime": "2026-08-19"  // ← 服务器时间（客户端不要用本地时间）
}
```

服务器按以下顺序判断（不可绕过）：
1. **未生效**：`start_date > 今天` → `not_started`（提前开授权的客户到点才生效）
2. **已到期**：`expire_date < 今天` → `expired`
3. **已吊销/暂停/过期**：`status` 字段 → 对应拦截

> ⚠️ 用户**改本地时间**也没用：判断在服务器完成，客户端只拿结果。

### 2. 客户端本地缓存（离线宽容）

授权成功后，客户端保存 `expireDate` + `lastVerifiedAt`：
- **在线**：每次启动向服务器确认（可即时发现吊销/暂停/续费）
- **离线**：最后一次验证成功后的 **3 天**内可继续使用（`OFFLINE_GRACE_DAYS`）
- **离线超过 3 天**：拦截，提示联网验证

### 3. 到期提醒（前台软件主动弹提醒）

SDK 自动计算 `remainDays`，**剩余 ≤ 7 天**时触发 `expire_soon` 事件，你的软件可以弹窗/横幅提醒用户续费；同时提供**每日定时检查**（`startDailyCheck()`），软件运行期间每天自动检查。

---

## 三、JS 版 SDK（Web 应用 / H5 / Electron）

### 快速接入（JS）

```html
<!-- 放 </body> 前 -->
<script src="https://download.dicad.cn/js/license-sdk.js"></script>
<script>
var lic = LicenseSDK.init({
  productId: 'seed-001',                    // ← 后台「软件管理」里的产品 ID
  serverUrl: window.location.origin         // 线上 https://download.dicad.cn
});

// ① 启动检查（进入主界面之前调用）——时间限制控制入口
lic.check().then(function (r) {
  if (r.ok) {
    initApp();                              // ✅ 授权有效 → 运行软件
    if (r.remainDays !== null && r.remainDays <= 7) {
      showExpireBanner(r.remainDays);       // ⏰ 到期前 7 天内显示提醒横幅
    }
  } else if (r.status === 'trial') {
    initApp(); lic.ui.showTrialBar(r.trialRemainMs);  // 试用中
  } else {
    lic.ui.showActivateModal({ allowClose: false }); // ❌ 拦截 + 激活窗
  }
});

// ② 授权用户登录（用户名+手机号，二选一激活方式）
//    把登录表单的「用户名」「手机号」传给 SDK：
lic.userLogin('张伟', '13800001111').then(function (r) {
  if (r.ok) {
    initApp();
    showToast('授权有效期至 ' + r.expireDate + '，剩余 ' + r.remainDays + ' 天');
  } else {
    alert(r.message);   // 未生效/已到期/被暂停/台数上限，都会给出明确提示
  }
});

// ③ 到期提醒（启动时 + 每天自动检查）
lic.onStatusChange(function (status, extra) {
  if (status === 'expire_soon') {
    alert('您的授权还剩 ' + extra.remainDays + ' 天到期，请联系管理员续费！');
  }
  if (status === 'expired')   { alert('授权已过期，请联系管理员续费'); }
  if (status === 'revoked')   { alert('授权已被吊销'); }
  if (status === 'suspended') { alert('授权已被暂停'); }
  if (status === 'not_started') { alert(extra.message); }
});
lic.startDailyCheck();   // 软件运行期间每 24 小时自动检查一次到期
</script>
```

### 完整 API 手册

| 方法 | 说明 |
|------|------|
| `lic.init({productId, serverUrl, trialMinutes})` | 初始化 |
| `lic.check([tryOnline])` | **启动检查**。返回 `{ok, status, message, remainDays, expireDate, trialRemainMs}` |
| `lic.activate('DICC-XXX-XXX-XXX')` | 授权码激活（客户维度） |
| `lic.userLogin(username, phone)` | **授权用户登录**（用户名+手机号） |
| `lic.userLogout()` | 授权用户退出（结束在线会话） |
| `lic.getStatus()` | 同步状态：`trial/active/expired/revoked/suspended/offline/not_started` |
| `lic.onStatusChange(fn)` | 状态变化回调（含 `expire_soon` 到期提醒） |
| `lic.startDailyCheck()` | 启动每日定时到期检查 |
| `lic.checkExpireReminder()` | 立即执行一次到期检查 |
| `lic.getFingerprint()` | 设备指纹 |
| `lic.reset()` | 清除本地授权状态 |
| `lic.ui.showActivateModal()` | 激活弹窗 |
| `lic.ui.showTrialBar(ms)` | 试用进度条 |

### Electron 桌面应用

主进程 `preload.js` 注入，渲染进程调用（同 Web 用法）：

```js
// preload.js
const { contextBridge } = require('electron');
const LicenseSDK = require('./license-sdk.js');   // 把 js/license-sdk.js 复制到项目
contextBridge.exposeInMainWorld('LicenseSDK', LicenseSDK.init({
  productId: 'seed-001',
  serverUrl: 'https://download.dicad.cn'
}));
```

---

## 四、C# 版（.NET 桌面软件 / WinForms / WPF）

**完整实现**：启动验证 + 时间限制 + 到期提醒（全部服务器权威判断）。

```csharp
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

public class LicenseService
{
    private static readonly HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(8) };
    private const string SERVER = "https://download.dicad.cn";
    private string _productId;
    private string _expireDate;          // 本地缓存的到期日
    private DateTime _lastVerify = DateTime.MinValue;
    private const int OFFLINE_GRACE_DAYS = 3;

    public LicenseService(string productId) { _productId = productId; }

    /// <summary>授权用户登录（用户名+手机号）。返回剩余天数，-1 表示失败。</summary>
    public async Task<(bool ok, string message, int remainDays, string expireDate)> UserLogin(string username, string phone)
    {
        var body = JsonSerializer.Serialize(new {
            username, phone, productId = _productId,
            deviceId = Environment.MachineName + "-" + Environment.UserName
        });
        var res = await client.PostAsync($"{SERVER}/api/license/user-login",
            new StringContent(body, System.Text.Encoding.UTF8, "application/json"));
        var json = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
        var d = json.RootElement.GetProperty("data");

        if (d.GetProperty("valid").GetBoolean())
        {
            _expireDate = d.GetProperty("expireDate").GetString();
            _lastVerify = DateTime.Now;
            int remain = d.GetProperty("remainDays").GetInt32();
            return (true, "授权有效", remain, _expireDate);
        }
        return (false, d.GetProperty("message").GetString(), -1, null);
    }

    /// <summary>授权码激活（客户维度）</summary>
    public async Task<(bool ok, string message, int remainDays)> Activate(string authCode)
    {
        var body = JsonSerializer.Serialize(new {
            authCode, productId = _productId,
            deviceId = Environment.MachineName + "-" + Environment.UserName
        });
        var res = await client.PostAsync($"{SERVER}/api/license/verify",
            new StringContent(body, System.Text.Encoding.UTF8, "application/json"));
        var json = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
        var d = json.RootElement.GetProperty("data");
        if (d.GetProperty("valid").GetBoolean())
        {
            _expireDate = d.GetProperty("expireDate").GetString();
            _lastVerify = DateTime.Now;
            return (true, "激活成功", CalcRemainDays(_expireDate));
        }
        return (false, d.GetProperty("message").GetString(), -1);
    }

    /// <summary>启动检查：服务器权威 + 离线宽容。返回剩余天数，-1 拦截。</summary>
    public async Task<int> CheckStartup()
    {
        if (string.IsNullOrEmpty(_expireDate)) return -1;   // 未激活 → 弹登录窗
        // 本地先看是否已过期（兜底，真实判断以服务器为准）
        int localRemain = CalcRemainDays(_expireDate);
        if (localRemain <= 0) return -1;
        // 离线宽容检查
        if ((DateTime.Now - _lastVerify).TotalDays > OFFLINE_GRACE_DAYS) return -1; // 需联网
        try { /* 在线刷新（可发现吊销/暂停）*/ } catch { /* 离线放行 */ }
        return localRemain;
    }

    /// <summary>到期提醒：剩余天数 ≤ 7 时返回 true，软件弹提醒</summary>
    public bool ShouldRemindExpire()
    {
        int remain = CalcRemainDays(_expireDate);
        return remain > 0 && remain <= 7;
    }

    private int CalcRemainDays(string expireDate)
    {
        var end = DateTime.Parse(expireDate).Date;
        return (int)Math.Ceiling((end - DateTime.Today).TotalDays);
    }
}
```

**软件里怎么用**（入口处）：

```csharp
// Program.cs 主入口
var lic = new LicenseService("seed-001");   // ← 换成你的产品 ID

// ① 启动验证
int remain = await lic.CheckStartup();
if (remain < 0)
{
    MessageBox.Show("未激活或授权已过期，请联系管理员。");
    return;   // 拦截，不进入主界面
}

// ② 到期提醒（启动时）
if (lic.ShouldRemindExpire())
    MessageBox.Show($"您的授权还剩 {remain} 天到期，请及时续费！");

// ③ 登录界面按钮回调（用户名+手机号）
var (ok, msg, days, _) = await lic.UserLogin(txtUser.Text, txtPhone.Text);
if (!ok) MessageBox.Show(msg); else initMainForm();
```

---

## 五、Python 版（脚本 / 后端服务 / 工具软件）

```python
import requests
from datetime import datetime, date

SERVER = "https://download.dicad.cn"
OFFLINE_GRACE_DAYS = 3

class LicenseClient:
    def __init__(self, product_id: str):
        self.product_id = product_id
        self.expire_date: str | None = None
        self.last_verify: date | None = None

    def _device_id(self) -> str:
        import socket, getpass
        return f"{socket.gethostname()}-{getpass.getuser()}"

    def user_login(self, username: str, phone: str = "") -> dict:
        """授权用户登录。返回 {ok, message, remainDays, expireDate}"""
        r = requests.post(f"{SERVER}/api/license/user-login", json={
            "username": username, "phone": phone,
            "productId": self.product_id, "deviceId": self._device_id()
        }, timeout=8)
        d = r.json()["data"]
        if d.get("valid"):
            self.expire_date = d["expireDate"]
            self.last_verify = date.today()
            return {"ok": True, "message": f"授权有效，剩余 {d['remainDays']} 天",
                    "remainDays": d["remainDays"], "expireDate": d["expireDate"]}
        return {"ok": False, "message": d.get("message", "登录失败"), "remainDays": -1}

    def activate(self, auth_code: str) -> dict:
        """授权码激活"""
        r = requests.post(f"{SERVER}/api/license/verify", json={
            "authCode": auth_code, "productId": self.product_id, "deviceId": self._device_id()
        }, timeout=8)
        d = r.json()["data"]
        if d.get("valid"):
            self.expire_date = d["expireDate"]
            self.last_verify = date.today()
            return {"ok": True, "message": f"激活成功，剩余 {self._remain_days()} 天"}
        return {"ok": False, "message": d.get("message", "激活失败")}

    def check_startup(self) -> int:
        """启动检查。返回剩余天数；<0 表示拦截（未激活/到期/离线超期）"""
        if not self.expire_date:
            return -1
        remain = self._remain_days()
        if remain <= 0:
            return -1
        if self.last_verify and (date.today() - self.last_verify).days > OFFLINE_GRACE_DAYS:
            return -1  # 离线超期，需联网
        return remain

    def should_remind(self, threshold: int = 7) -> bool:
        """到期提醒：剩余天数 ≤ threshold 且 > 0 时返回 True"""
        remain = self._remain_days()
        return 0 < remain <= threshold

    def _remain_days(self) -> int:
        if not self.expire_date:
            return -1
        end = datetime.strptime(self.expire_date, "%Y-%m-%d").date()
        return max(0, (end - date.today()).days)


# ====== 软件入口用法 ======
lic = LicenseClient("seed-001")   # ← 换成你的产品 ID

remain = lic.check_startup()
if remain < 0:
    sys.exit("未激活或授权已到期，请联系管理员。")   # 拦截退出

if lic.should_remind():
    print(f"⚠️ 您的授权还剩 {remain} 天到期，请及时续费！")
```

---

## 六、后台操作指引（管理员怎么给用户开授权）

### 方式一：按授权用户（推荐，软件内按人管理）

1. 后台 → **软件管理** → 找到对应软件 → 点 **👤 授权用户**
2. 点 **新建授权用户** → 填：**用户名**、**手机号**、授权**开始日期**、授权**到期日期**、**消费金额**、备注
3. 保存后，客户在他的软件里输入 **用户名+手机号** 即可登录使用
4. 续费：点用户行的 **续费** → 新到期日 + 金额（自动延长并累计收益）
5. 到期前 7 天，客户软件会自动弹"还剩 X 天"提醒

### 方式二：按授权码（企业客户整单）

1. 后台 → **客户管理** → 添加客户
2. 后台 → **授权管理** → 签发新授权 → 选客户/选软件/设到期日 → 把 `DICC-XXX` 授权码发给客户
3. 客户在他的软件里输入授权码激活

---

## 七、时间限制细节与防作弊说明

| 机制 | 说明 |
|------|------|
| **服务器权威判断** | 生效/到期判断全部在服务器完成，改本地时间无效 |
| **未生效拦截** | `start_date` 在未来 → 返回 `not_started`（提前授权到点才生效） |
| **离线宽容 3 天** | 断网后 3 天内可离线使用（`OFFLINE_GRACE_DAYS` 可调） |
| **到期即时生效** | 续费后客户端下次启动自动刷新；已开着的软件用 `startDailyCheck()` 每天同步 |
| **吊销/暂停即时生效** | 在线状态下服务器返回 `revoked/suspended` 即拦截 |
| **台数限制** | 超过授权台数 → `seat_limit`（提示在其他设备退出） |
| **设备指纹** | 每台设备唯一指纹，防止一台激活到处用（可配合后台扩展） |

---

## 八、上线检查清单

- [ ] `productId` 对应后台「软件管理」里的产品 ID
- [ ] 客户端能访问 `https://download.dicad.cn/api/license/user-login`（HTTPS）
- [ ] 试用时长按需调整：`init({ trialMinutes: 15 })`
- [ ] 到期提醒天数按需调整：SDK 里 `EXPIRE_REMIND_DAYS = 7`
- [ ] 离线宽容天数按需调整：`OFFLINE_GRACE_DAYS = 3`
- [ ] 在软件「设置」里提供退出登录入口（`lic.userLogout()` / `lic.logout()`），便于换账号

---

## 九、在线体验

1. 打开演示页：`https://download.dicad.cn/license-demo.html`
2. 用种子账号测试：用户名 `张伟` / 手机号 `13800001111`（产品 FlowTask）
3. 或到后台 `admin.html` → 软件管理 → 👤授权用户 → 新建自己的测试用户

---

## 十、内测账号接入（软件内绑定账号，后台统一管理有效期）

> 适合**内测阶段**：软件里绑定一个带密码的内测账号，后台「内测管理」统一维护，**只有账号在有效期内软件才能使用**。到期/暂停/吊销后，软件立即无法使用。

### 1. 原理

```
后台「内测管理」新建账号 → 用户名 + 密码 + 关联软件 + 授权起止日期
        ↓
软件内绑定该账号 → lic.betaLogin(用户名, 密码)
        ↓
服务器权威验证（改本地时间无效）：
  ① 密码错误          → 拦截「密码错误」
  ② 未到开始日期      → 拦截「内测尚未开始」
  ③ 已过到期日期      → 拦截「内测已到期，请联系管理员续期」
  ④ 账号被暂停/吊销   → 拦截
        ↓
有效 → 返回剩余天数 → 软件放行使用
```

### 2. JS 版最小接入代码（Web / Electron）

```js
var lic = LicenseSDK.init({
  productId: 'seed-001',            // ← 后台「软件管理」里的产品 ID
  serverUrl: window.location.origin // https://download.dicad.cn
});

// ① 启动时：软件内绑定账号登录（用户名+密码）
lic.betaLogin('beta_flow', 'Beta123!').then(function(r){
  if(r.ok){ initApp(); }           // ✅ 有效期内 → 放行使用
  else{ blockApp(r.message); }     // ❌ 密码错/未开始/到期/暂停 → 拦截
});

// ② 本地有效期检查（服务器时间校准，改本地时间无效）
var st = lic.betaCheck();
if(!st.ok) blockApp(st.message);   // 到期/未生效 → 禁止使用

// ③ 到期提醒（剩 ≤7 天触发 beta_expire_soon；到期触发 beta_expired）
lic.onStatusChange(function(status, extra){
  if(status==='beta_expire_soon') alert('内测还剩 '+extra.remainDays+' 天到期！');
  if(status==='beta_expired') alert('内测已到期，软件将锁定！');
});
```

**API 速查**：
- `lic.betaLogin(username, password)` → `{ok, valid, status, message, expireDate, remainDays}`（登录并缓存）
- `lic.betaCheck()` → 本地校验有效期（同步返回，服务器时间校准）
- `lic.getBetaState()` → 当前绑定的内测账号信息
- `lic.betaLogout()` → 退出内测账号

### 3. C# 版（.NET 桌面软件）— 内测账号登录

```csharp
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

// 启动时调用：软件内绑定内测账号
public async Task<(bool ok, string msg, string expireDate, int remainDays)> BetaLoginAsync(
    string serverUrl, string productId, string username, string password)
{
    using var client = new HttpClient();
    client.Timeout = TimeSpan.FromSeconds(8);
    var resp = await client.PostAsJsonAsync(serverUrl + "/api/beta/login", new
    {
        username, password, productId
    });
    var json = JsonSerializer.Deserialize<JsonElement>(await resp.Content.ReadAsStringAsync());
    var d = json.GetProperty("data");
    bool valid = d.GetProperty("valid").GetBoolean();
    string msg = d.GetProperty("message").GetString();
    if (valid)
    {
        string expire = d.GetProperty("expireDate").GetString();
        int remain = d.GetProperty("remainDays").GetInt32();
        // 缓存到期日到本地（可同时缓存 serverTime 用于防改时间）
        SaveSetting("beta_expire", expire);
        return (true, msg, expire, remain);
    }
    return (false, msg, null, 0);
}

// 软件启动时：先本地检查有效期，再尝试联网刷新
// 用法：var r = await BetaLoginAsync("https://download.dicad.cn", "seed-001", "beta_flow", "Beta123!");
//       if(!r.ok) 弹窗 r.msg 并退出；else 正常启动并提示剩余天数
```

### 4. Python 版（脚本 / 工具）

```python
import requests, sys

BASE = "https://download.dicad.cn"

def beta_login(product_id: str, username: str, password: str):
    r = requests.post(f"{BASE}/api/beta/login",
        json={"username": username, "password": password, "productId": product_id},
        timeout=8)
    d = r.json()["data"]
    return d  # d["valid"] / d["status"] / d["message"] / d["expireDate"] / d["remainDays"]

if __name__ == "__main__":
    d = beta_login("seed-001", "beta_flow", "Beta123!")
    if d["valid"]:
        print(f"✅ 内测授权有效，剩余 {d['remainDays']} 天，到期 {d['expireDate']}")
        # 正常启动你的工具
    else:
        print(f"❌ {d['message']}")
        sys.exit(1)
```

### 5. 后台操作（管理员）

1. 后台 → **内测管理** → **新建内测账号**：填账号名、密码、关联软件、授权起止日期
2. 把账号+密码给测试人员，软件内绑定即可使用
3. 内测到期前：账号行点 **续期** 延长；临时封禁：**暂停**；彻底收回：**删除**
4. 换密码：点 **重置密码**，软件内旧密码立即失效

### 6. 演示页

打开 `https://download.dicad.cn/license-demo.html` → 「内测账号登录」卡，输入 `beta_flow / Beta123!` 体验。
后台试一下：内测管理 → 把 beta_flow 到期日改成昨天或暂停 → 重新登录即被拦截。

---

## 十一、通用授权接口原始契约（不想用 SDK 时可直接调）

### 请求

```
POST /api/license/auth
Content-Type: application/json

{
  "productId":   "seed-001",       // 后台软件目录里的产品 ID（必填）
  "productName": "FlowTask",       // 软件名称（建议传，与后台核对，不一致只告警）
  "version":     "2.4.1",          // 软件版本号（建议传，仅记录不做限制）
  "username":    "张伟",           // 授权用户名（必填）
  "phone":       "13800001111",    // 手机号（后台该用户填了手机号时必填）
  "deviceId":    "dev_xxxxxxxx",   // 设备标识（首次生成后本地持久化）
  "machineName": "DESKTOP-ABC",    // 可选，仅记录
  "os":          "Windows 11"      // 可选，仅记录
}
```

`GET /api/license/auth?productId=...&username=...` 参数同名字段，效果一致。

### 响应（有效期内 —— 客户端应完全静默）

```json
{
  "code": 0, "message": "ok",
  "data": {
    "authorized": true,
    "status": "active",
    "message": "授权有效",
    "silent": true,
    "remainDays": 90,
    "expireDate": "2027-12-31",
    "startDate": "2026-02-20",
    "username": "张伟",
    "productName": "FlowTask",
    "version": "2.4.1",
    "seats": 3,
    "serverTime": "2026-09-29",
    "nameMismatch": false,
    "expiredDialog": null
  }
}
```

### 响应（已到期 —— 客户端应弹窗引导付费）

```json
{
  "code": 0, "message": "ok",
  "data": {
    "authorized": false,
    "status": "expired",
    "message": "授权已到期（2026-08-30）",
    "silent": false,
    "remainDays": 0,
    "expireDate": "2026-08-30",
    "username": "批量乙",
    "productName": "FlowTask",
    "expiredDialog": {
      "title": "授权已到期",
      "message": "您的 FlowTask 授权已于 2026-08-30 到期，续费后即可继续使用。",
      "reason": "授权已到期（2026-08-30）",
      "contact": "客服QQ：800000000  邮箱：support@dicad.cn",
      "payUrl": "https://download.dicad.cn/product-list.html",
      "expireDate": "2026-08-30"
    }
  }
}
```

### status 取值与处理

| status | authorized | silent | 含义 | 客户端该做什么 |
|--------|-----------|--------|------|----------------|
| `active` | ✅ | ✅ | 授权有效 | 静默运行 |
| `free` | ✅ | ✅ | 该软件免授权（后台关闭了授权校验） | 静默运行 |
| `expired` | ❌ | ❌ | 已到期 | 弹 `expiredDialog` 引导付费 |
| `suspended` | ❌ | ❌ | 被暂停 | 弹窗提示联系客服 |
| `revoked` | ❌ | ❌ | 被吊销 | 弹窗提示联系客服 |
| `not_started` | ❌ | ❌ | 未到生效日期 | 告知生效日期 |
| `not_found` | ❌ | ❌ | 用户名/手机号不匹配 | 提示核对信息 |
| `invalid_request` | ❌ | ❌ | 参数缺失 | 检查接入代码 |

### 配套管理接口（需登录 + CSRF）

| 接口 | 说明 |
|------|------|
| `GET /api/products` | 产品列表，含 `authRequired` / `contactInfo` / `payUrl` / `expireDialogMessage` |
| `PUT /api/products/:id` | 更新产品，支持上述 4 个授权配置字段 |
| `GET /api/products/:id/activations` | 软件维度激活设备清单（含版本分布统计） |
| `GET /api/products/:id/users/:uid/activations` | 授权用户维度设备清单 |
| `GET /api/products/:id/users` | 授权用户列表，含 `lastVersion` / `deviceCount` / `lastSeenAt` |

---

## 十二、v3 变更说明（2026-09-29）

1. **新增统一鉴权接口** `POST|GET /api/license/auth` —— 一个接口完成全部授权判断，
   返回 `authorized` / `silent` / `expiredDialog`，客户端逻辑简化为一句话：
   `authorized` 为 true 就静默运行，为 false 就弹 `dialog`。
2. **新增 `license_activations` 表** —— 记录每次鉴权上报的版本号 + 设备 + 系统 + 次数 + 最近状态。
3. **products 新增 4 个授权配置列** —— `auth_required`（是否启用授权校验）、`contact_info`（客服联系方式）、
   `pay_url`（付费链接）、`expire_dialog_message`（过期提示文案，支持占位符）。
4. **后台新增「⑧ 授权与付费提示」配置区块**（软件编辑表单内）。
5. **后台授权用户列表新增「使用情况」列**（最近版本 + 设备台数）与 **🖥️「使用设备 / 版本」弹窗**。
6. **修复既有 Bug** —— 编辑已有软件后保存会被当成新建、产生重复产品
   （`showForm()` 中 `switchView()` 会重置 `editingId`，已调整赋值顺序）。
7. **全局默认配置** —— 系统设置新增 `silent_mode` / `default_contact_info` / `default_pay_url` /
   `default_expire_message`，软件未单独配置时作为兜底。

> 原有接口（`/api/license/verify`、`/api/license/user-login`、`/api/license/user-status`、
> `/api/beta/login` 等）**全部保留**，已接入的软件不受影响。

