Bläddra i källkod

Feature init license admin

Carl 2 veckor sedan
incheckning
d3fbbbe0a4
13 ändrade filer med 3068 tillägg och 0 borttagningar
  1. 29 0
      .gitignore
  2. 335 0
      README.md
  3. 30 0
      database/database.go
  4. 41 0
      go.mod
  5. 96 0
      go.sum
  6. 60 0
      handlers/auth.go
  7. 525 0
      handlers/license.go
  8. 174 0
      handlers/verify.go
  9. 110 0
      main.go
  10. 52 0
      middleware/auth.go
  11. 121 0
      models/license.go
  12. 1227 0
      web/index.html
  13. 268 0
      web/login.html

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+# 数据库文件
+license.db
+license.db-shm
+license.db-wal
+
+# Go 构建文件
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+*.test
+*.out
+go.work
+license-check
+
+# IDE 文件
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# 操作系统文件
+.DS_Store
+Thumbs.db
+
+
+

+ 335 - 0
README.md

@@ -0,0 +1,335 @@
+# License 管理平台
+
+一个轻量级的 License 验证和管理服务,用于 Chrome 浏览器插件后端。支持激活码管理、设备绑定、批量操作等功能。
+
+## ✨ 功能特性
+
+### 核心功能
+- 🔑 **License 验证** - 支持设备绑定验证,自动记录激活时间
+- 📦 **批量生成** - 一键批量生成激活码(支持最多 1000 个)
+- 📋 **CRUD 操作** - 完整的激活码增删改查功能
+- 🔐 **Token 认证** - 基于 Token 的安全认证机制
+- 📊 **设备管理** - 查看已绑定设备和激活时间
+- 🎨 **现代化 UI** - 美观易用的 Web 管理界面
+
+### 技术特性
+- 使用 SQLite 文件数据库,部署简单
+- RESTful API 设计
+- 响应式前端界面
+- 支持 CORS(Chrome 插件调用)
+
+## 🚀 快速开始
+
+### 环境要求
+- Go 1.21 或更高版本
+- 现代浏览器(Chrome、Firefox、Safari、Edge)
+
+### 安装步骤
+
+1. **克隆项目**
+```bash
+git clone <repository-url>
+cd license-check
+```
+
+2. **安装依赖**
+```bash
+go mod download
+```
+
+3. **配置 Token(可选)**
+```bash
+# 设置认证 Token(默认:admin-token-123456)
+export AUTH_TOKEN=your-secret-token
+```
+
+4. **启动服务**
+```bash
+go run main.go
+# 或编译后运行
+go build -o license-check
+./license-check
+```
+
+5. **访问管理平台**
+```
+浏览器打开:http://localhost:8080
+```
+
+## 📖 使用指南
+
+### 登录系统
+
+1. 访问 `http://localhost:8080`,自动跳转到登录页
+2. 输入 Token(默认:`admin-token-123456`)
+3. 登录成功后进入管理界面
+
+### 创建激活码
+
+**单个创建:**
+1. 点击"创建 License"按钮
+2. 填写激活码、最大设备数等信息
+3. 点击保存
+
+**批量生成:**
+1. 点击"批量生成"按钮
+2. 设置前缀(如:VIP)
+3. 设置生成数量(1-1000)
+4. 设置最大设备数
+5. 点击生成
+
+生成的激活码格式:`前缀-32位随机字符串`(如:`VIP-A3B9C2D4E5F6G7H8I9J0K1L2M3N4O5P6`)
+
+### 管理激活码
+
+- **查看列表** - 分页显示所有激活码
+- **编辑** - 修改最大设备数、绑定设备列表
+- **删除** - 单个删除或批量删除
+- **复制** - 一键复制激活码到剪贴板
+
+### 设备绑定
+
+激活码支持设备绑定功能:
+- 每个激活码可设置最大设备数(默认 2 个)
+- 设备首次验证时自动绑定
+- 自动记录设备激活时间
+- 超过最大设备数时验证失败
+
+## 🔌 API 文档
+
+### 公开接口(无需认证)
+
+#### 1. License 验证
+```http
+POST /api/verify
+Content-Type: application/json
+
+{
+  "key": "VIP-123456",
+  "device_id": "device-uuid-123"
+}
+```
+
+**响应:**
+```json
+{
+  "code": 0,
+  "msg": "success",
+  "data": {
+    "valid": true
+  }
+}
+```
+
+#### 2. 登录
+```http
+POST /api/login
+Content-Type: application/json
+
+{
+  "token": "admin-token-123456"
+}
+```
+
+**响应:**
+```json
+{
+  "code": 0,
+  "msg": "登录成功",
+  "data": {
+    "token": "admin-token-123456"
+  }
+}
+```
+
+### 管理接口(需要认证)
+
+所有管理接口需要在请求头中添加 Token:
+```
+Authorization: Bearer your-token
+```
+
+#### 1. 获取 License 列表
+```http
+GET /api/licenses?page=1&page_size=10
+Authorization: Bearer your-token
+```
+
+#### 2. 获取单个 License
+```http
+GET /api/licenses/:id
+Authorization: Bearer your-token
+```
+
+#### 3. 创建 License
+```http
+POST /api/licenses
+Authorization: Bearer your-token
+Content-Type: application/json
+
+{
+  "key": "VIP-8888",
+  "max_devices": 2,
+  "bound_devices": "[]"
+}
+```
+
+#### 4. 更新 License
+```http
+PUT /api/licenses/:id
+Authorization: Bearer your-token
+Content-Type: application/json
+
+{
+  "max_devices": 5,
+  "bound_devices": "[\"device-1\", \"device-2\"]"
+}
+```
+
+#### 5. 删除 License
+```http
+DELETE /api/licenses/:id
+Authorization: Bearer your-token
+```
+
+#### 6. 批量创建 License
+```http
+POST /api/licenses/batch
+Authorization: Bearer your-token
+Content-Type: application/json
+
+{
+  "prefix": "VIP",
+  "count": 10,
+  "max_devices": 2
+}
+```
+
+#### 7. 批量删除 License
+```http
+DELETE /api/licenses/batch
+Authorization: Bearer your-token
+Content-Type: application/json
+
+{
+  "ids": [1, 2, 3]
+}
+```
+
+## 🗂️ 项目结构
+
+```
+license-check/
+├── main.go                 # 主程序入口
+├── go.mod                  # Go 模块依赖
+├── go.sum                  # 依赖校验
+├── database/
+│   └── database.go        # 数据库初始化
+├── models/
+│   └── license.go         # License 数据模型
+├── handlers/
+│   ├── auth.go            # 认证处理器
+│   ├── license.go         # License CRUD 处理器
+│   └── verify.go          # License 验证处理器
+├── middleware/
+│   └── auth.go            # Token 认证中间件
+├── web/
+│   ├── index.html         # 管理平台前端
+│   └── login.html         # 登录页面
+├── license.db             # SQLite 数据库文件(自动生成)
+└── README.md              # 项目文档
+```
+
+## ⚙️ 配置说明
+
+### 环境变量
+
+- `AUTH_TOKEN` - 认证 Token(默认:`admin-token-123456`)
+
+### 数据库
+
+- 使用 SQLite 文件数据库
+- 数据库文件:`license.db`
+- 首次启动自动创建表结构
+
+### 端口配置
+
+默认端口:`8080`
+
+修改端口:编辑 `main.go` 中的 `r.Run(":8080")`
+
+## 🔒 安全建议
+
+1. **生产环境配置**
+   - 修改默认 Token,使用强密码
+   - 通过环境变量设置 `AUTH_TOKEN`
+   - 使用 HTTPS 部署
+
+2. **Token 管理**
+   - 定期更换 Token
+   - 不要在代码中硬编码 Token
+   - 使用环境变量或密钥管理服务
+
+3. **数据库安全**
+   - 定期备份 `license.db` 文件
+   - 设置适当的文件权限
+
+## 📝 开发说明
+
+### 技术栈
+
+- **后端:** Go 1.21+、Gin、GORM、SQLite
+- **前端:** HTML5、CSS3、JavaScript (ES6+)
+
+### 依赖包
+
+- `github.com/gin-gonic/gin` - Web 框架
+- `gorm.io/gorm` - ORM 框架
+- `gorm.io/driver/sqlite` - SQLite 驱动
+
+### 开发模式
+
+```bash
+# 开发模式运行
+go run main.go
+
+# 编译二进制
+go build -o license-check
+
+# 运行测试
+go test ./...
+```
+
+## 🐛 常见问题
+
+### 1. 数据库文件不存在
+首次运行会自动创建 `license.db` 文件,确保有写入权限。
+
+### 2. 端口被占用
+修改 `main.go` 中的端口号,或停止占用 8080 端口的程序。
+
+### 3. Token 验证失败
+检查:
+- Token 是否正确
+- 环境变量 `AUTH_TOKEN` 是否设置
+- 请求头中是否包含 `Authorization: Bearer <token>`
+
+### 4. CORS 错误
+已在代码中配置 CORS,如仍有问题,检查浏览器控制台错误信息。
+
+## 📄 许可证
+
+本项目采用 MIT 许可证。
+
+## 🤝 贡献
+
+欢迎提交 Issue 和 Pull Request!
+
+## 📧 联系方式
+
+如有问题或建议,请提交 Issue。
+
+---
+
+**注意:** 本项目为轻量级服务,适用于中小规模场景。生产环境使用请做好安全配置和性能优化。
+

+ 30 - 0
database/database.go

@@ -0,0 +1,30 @@
+package database
+
+import (
+	"license-check/models"
+
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+)
+
+var DB *gorm.DB
+
+// InitDB 初始化数据库连接
+func InitDB() error {
+	var err error
+	DB, err = gorm.Open(sqlite.Open("license.db"), &gorm.Config{})
+	if err != nil {
+		return err
+	}
+
+	// 自动迁移数据库表
+	err = DB.AutoMigrate(&models.License{})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+
+

+ 41 - 0
go.mod

@@ -0,0 +1,41 @@
+module license-check
+
+go 1.21
+
+require (
+	github.com/gin-gonic/gin v1.9.1
+	gorm.io/driver/sqlite v1.5.4
+	gorm.io/gorm v1.25.5
+)
+
+require (
+	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.14.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-sqlite3 v1.14.17 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/crypto v0.9.0 // indirect
+	golang.org/x/net v0.10.0 // indirect
+	golang.org/x/sys v0.8.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+

+ 96 - 0
go.sum

@@ -0,0 +1,96 @@
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
+github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
+gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
+gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
+gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 60 - 0
handlers/auth.go

@@ -0,0 +1,60 @@
+package handlers
+
+import (
+	"license-check/middleware"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+)
+
+// LoginRequest 登录请求结构
+type LoginRequest struct {
+	Token string `json:"token" binding:"required"` // 认证token
+}
+
+// LoginResponse 登录响应结构
+type LoginResponse struct {
+	Code int    `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string `json:"msg"`  // 响应消息
+	Data struct {
+		Token string `json:"token"` // 返回的token(用于前端存储)
+	} `json:"data,omitempty"`
+}
+
+// Login 登录接口
+// POST /api/login
+func Login() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var req LoginRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, LoginResponse{
+				Code: 400,
+				Msg:  "请求参数错误: " + err.Error(),
+			})
+			return
+		}
+
+		// 验证token
+		expectedToken := middleware.GetAuthToken()
+		if req.Token != expectedToken {
+			c.JSON(http.StatusUnauthorized, LoginResponse{
+				Code: 401,
+				Msg:  "Token 无效",
+			})
+			return
+		}
+
+		// 登录成功
+		c.JSON(http.StatusOK, LoginResponse{
+			Code: 0,
+			Msg:  "登录成功",
+			Data: struct {
+				Token string `json:"token"`
+			}{
+				Token: req.Token,
+			},
+		})
+	}
+}
+
+

+ 525 - 0
handlers/license.go

@@ -0,0 +1,525 @@
+package handlers
+
+import (
+	"encoding/json"
+	"license-check/models"
+	"math/rand"
+	"net/http"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm"
+)
+
+// CreateLicenseRequest 创建 License 请求结构
+type CreateLicenseRequest struct {
+	Key        string `json:"key" binding:"required"`        // 激活码(必填)
+	MaxDevices int    `json:"max_devices"`                    // 最大设备数(可选,默认2)
+	BoundDevices string `json:"bound_devices,omitempty"`      // 初始绑定设备列表(可选,默认为空数组)
+}
+
+// UpdateLicenseRequest 更新 License 请求结构
+type UpdateLicenseRequest struct {
+	Key        string `json:"key,omitempty"`        // 激活码(可选)
+	MaxDevices *int   `json:"max_devices,omitempty"` // 最大设备数(可选,使用指针以区分零值)
+	BoundDevices string `json:"bound_devices,omitempty"` // 绑定设备列表(可选)
+}
+
+// LicenseResponse License 响应结构
+type LicenseResponse struct {
+	Code int             `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string          `json:"msg"`  // 响应消息
+	Data *models.License `json:"data,omitempty"` // License 数据
+}
+
+// LicenseListResponse License 列表响应结构
+type LicenseListResponse struct {
+	Code int               `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string            `json:"msg"`  // 响应消息
+	Data []models.License  `json:"data"` // License 列表
+	Total int64            `json:"total"` // 总数
+}
+
+// CreateLicense 创建新的 License
+// POST /api/licenses
+func CreateLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var req CreateLicenseRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, LicenseResponse{
+				Code: 400,
+				Msg:  "请求参数错误: " + err.Error(),
+			})
+			return
+		}
+
+		// 检查 Key 是否已存在
+		var existingLicense models.License
+		result := db.Where("key = ?", req.Key).First(&existingLicense)
+		if result.Error == nil {
+			// Key 已存在
+			c.JSON(http.StatusBadRequest, LicenseResponse{
+				Code: 400,
+				Msg:  "激活码已存在",
+			})
+			return
+		}
+		if result.Error != gorm.ErrRecordNotFound {
+			// 数据库查询错误
+			c.JSON(http.StatusInternalServerError, LicenseResponse{
+				Code: 500,
+				Msg:  "数据库查询错误: " + result.Error.Error(),
+			})
+			return
+		}
+
+		// 设置默认值
+		if req.MaxDevices <= 0 {
+			req.MaxDevices = 2 // 默认最大设备数为 2
+		}
+		if req.BoundDevices == "" {
+			req.BoundDevices = "[]" // 默认为空数组
+		}
+
+		// 创建新的 License
+		license := models.License{
+			Key:          req.Key,
+			MaxDevices:   req.MaxDevices,
+			BoundDevices: req.BoundDevices,
+		}
+
+		if err := db.Create(&license).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, LicenseResponse{
+				Code: 500,
+				Msg:  "创建 License 失败: " + err.Error(),
+			})
+			return
+		}
+
+		c.JSON(http.StatusOK, LicenseResponse{
+			Code: 0,
+			Msg:  "创建成功",
+			Data: &license,
+		})
+	}
+}
+
+// GetLicenseList 获取 License 列表
+// GET /api/licenses?page=1&page_size=10
+func GetLicenseList(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// 解析分页参数
+		page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+		pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
+
+		if page < 1 {
+			page = 1
+		}
+		if pageSize < 1 || pageSize > 100 {
+			pageSize = 10
+		}
+
+		var licenses []models.License
+		var total int64
+
+		// 获取总数
+		if err := db.Model(&models.License{}).Count(&total).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, LicenseListResponse{
+				Code: 500,
+				Msg:  "查询总数失败: " + err.Error(),
+				Data: []models.License{},
+				Total: 0,
+			})
+			return
+		}
+
+		// 分页查询
+		offset := (page - 1) * pageSize
+		if err := db.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&licenses).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, LicenseListResponse{
+				Code: 500,
+				Msg:  "查询 License 列表失败: " + err.Error(),
+				Data: []models.License{},
+				Total: 0,
+			})
+			return
+		}
+
+		c.JSON(http.StatusOK, LicenseListResponse{
+			Code: 0,
+			Msg:  "success",
+			Data: licenses,
+			Total: total,
+		})
+	}
+}
+
+// GetLicense 获取单个 License
+// GET /api/licenses/:id
+func GetLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, LicenseResponse{
+				Code: 400,
+				Msg:  "无效的 ID 参数",
+			})
+			return
+		}
+
+		var license models.License
+		result := db.First(&license, id)
+		if result.Error != nil {
+			if result.Error == gorm.ErrRecordNotFound {
+				c.JSON(http.StatusNotFound, LicenseResponse{
+					Code: 404,
+					Msg:  "License 不存在",
+				})
+				return
+			}
+			c.JSON(http.StatusInternalServerError, LicenseResponse{
+				Code: 500,
+				Msg:  "查询失败: " + result.Error.Error(),
+			})
+			return
+		}
+
+		c.JSON(http.StatusOK, LicenseResponse{
+			Code: 0,
+			Msg:  "success",
+			Data: &license,
+		})
+	}
+}
+
+// UpdateLicense 更新 License
+// PUT /api/licenses/:id
+func UpdateLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, LicenseResponse{
+				Code: 400,
+				Msg:  "无效的 ID 参数",
+			})
+			return
+		}
+
+		// 查找 License
+		var license models.License
+		result := db.First(&license, id)
+		if result.Error != nil {
+			if result.Error == gorm.ErrRecordNotFound {
+				c.JSON(http.StatusNotFound, LicenseResponse{
+					Code: 404,
+					Msg:  "License 不存在",
+				})
+				return
+			}
+			c.JSON(http.StatusInternalServerError, LicenseResponse{
+				Code: 500,
+				Msg:  "查询失败: " + result.Error.Error(),
+			})
+			return
+		}
+
+		// 解析更新请求
+		var req UpdateLicenseRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, LicenseResponse{
+				Code: 400,
+				Msg:  "请求参数错误: " + err.Error(),
+			})
+			return
+		}
+
+		// 更新字段
+		if req.Key != "" {
+			// 检查新 Key 是否与其他 License 冲突
+			var existingLicense models.License
+			checkResult := db.Where("key = ? AND id != ?", req.Key, id).First(&existingLicense)
+			if checkResult.Error == nil {
+				c.JSON(http.StatusBadRequest, LicenseResponse{
+					Code: 400,
+					Msg:  "激活码已存在",
+				})
+				return
+			}
+			if checkResult.Error != gorm.ErrRecordNotFound {
+				c.JSON(http.StatusInternalServerError, LicenseResponse{
+					Code: 500,
+					Msg:  "检查激活码失败: " + checkResult.Error.Error(),
+				})
+				return
+			}
+			license.Key = req.Key
+		}
+
+		if req.MaxDevices != nil {
+			if *req.MaxDevices <= 0 {
+				c.JSON(http.StatusBadRequest, LicenseResponse{
+					Code: 400,
+					Msg:  "最大设备数必须大于 0",
+				})
+				return
+			}
+			license.MaxDevices = *req.MaxDevices
+		}
+
+		if req.BoundDevices != "" {
+			// 验证 BoundDevices 是否为有效的 JSON 数组
+			var testDevices []string
+			if err := json.Unmarshal([]byte(req.BoundDevices), &testDevices); err != nil {
+				c.JSON(http.StatusBadRequest, LicenseResponse{
+					Code: 400,
+					Msg:  "bound_devices 必须是有效的 JSON 数组: " + err.Error(),
+				})
+				return
+			}
+			license.BoundDevices = req.BoundDevices
+		}
+
+		// 保存更新
+		if err := db.Save(&license).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, LicenseResponse{
+				Code: 500,
+				Msg:  "更新失败: " + err.Error(),
+			})
+			return
+		}
+
+		c.JSON(http.StatusOK, LicenseResponse{
+			Code: 0,
+			Msg:  "更新成功",
+			Data: &license,
+		})
+	}
+}
+
+// DeleteLicense 删除 License
+// DELETE /api/licenses/:id
+func DeleteLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+		if err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{
+				"code": 400,
+				"msg":  "无效的 ID 参数",
+			})
+			return
+		}
+
+		// 查找 License
+		var license models.License
+		result := db.First(&license, id)
+		if result.Error != nil {
+			if result.Error == gorm.ErrRecordNotFound {
+				c.JSON(http.StatusNotFound, gin.H{
+					"code": 404,
+					"msg":  "License 不存在",
+				})
+				return
+			}
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"code": 500,
+				"msg":  "查询失败: " + result.Error.Error(),
+			})
+			return
+		}
+
+		// 删除 License
+		if err := db.Delete(&license).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"code": 500,
+				"msg":  "删除失败: " + err.Error(),
+			})
+			return
+		}
+
+		c.JSON(http.StatusOK, gin.H{
+			"code": 0,
+			"msg":  "删除成功",
+		})
+	}
+}
+
+// BatchDeleteLicenseRequest 批量删除 License 请求结构
+type BatchDeleteLicenseRequest struct {
+	IDs []uint `json:"ids" binding:"required"` // License ID 列表
+}
+
+// BatchDeleteLicenseResponse 批量删除响应结构
+type BatchDeleteLicenseResponse struct {
+	Code int    `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string `json:"msg"`  // 响应消息
+	Total int   `json:"total"` // 成功删除的数量
+}
+
+// BatchDeleteLicense 批量删除 License
+// DELETE /api/licenses/batch
+func BatchDeleteLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var req BatchDeleteLicenseRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
+				Code: 400,
+				Msg:  "请求参数错误: " + err.Error(),
+				Total: 0,
+			})
+			return
+		}
+
+		// 验证参数
+		if len(req.IDs) == 0 {
+			c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
+				Code: 400,
+				Msg:  "请至少选择一个 License",
+				Total: 0,
+			})
+			return
+		}
+
+		if len(req.IDs) > 100 {
+			c.JSON(http.StatusBadRequest, BatchDeleteLicenseResponse{
+				Code: 400,
+				Msg:  "一次最多只能删除 100 个 License",
+				Total: 0,
+			})
+			return
+		}
+
+		// 批量删除
+		result := db.Where("id IN ?", req.IDs).Delete(&models.License{})
+		if result.Error != nil {
+			c.JSON(http.StatusInternalServerError, BatchDeleteLicenseResponse{
+				Code: 500,
+				Msg:  "批量删除失败: " + result.Error.Error(),
+				Total: 0,
+			})
+			return
+		}
+
+		deletedCount := int(result.RowsAffected)
+		c.JSON(http.StatusOK, BatchDeleteLicenseResponse{
+			Code: 0,
+			Msg:  "成功删除 " + strconv.Itoa(deletedCount) + " 个 License",
+			Total: deletedCount,
+		})
+	}
+}
+
+// BatchCreateLicenseRequest 批量创建 License 请求结构
+type BatchCreateLicenseRequest struct {
+	Prefix     string `json:"prefix" binding:"required"`     // 激活码前缀(必填)
+	Count      int    `json:"count" binding:"required"`      // 生成数量(必填)
+	MaxDevices int    `json:"max_devices"`                    // 最大设备数(可选,默认2)
+}
+
+// BatchCreateLicenseResponse 批量创建响应结构
+type BatchCreateLicenseResponse struct {
+	Code int               `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string            `json:"msg"`  // 响应消息
+	Data []models.License  `json:"data"` // 创建的 License 列表
+	Total int              `json:"total"` // 成功创建的数量
+}
+
+// BatchCreateLicense 批量创建 License
+// POST /api/licenses/batch
+func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var req BatchCreateLicenseRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
+				Code: 400,
+				Msg:  "请求参数错误: " + err.Error(),
+				Data: []models.License{},
+				Total: 0,
+			})
+			return
+		}
+
+		// 验证参数
+		if req.Count <= 0 || req.Count > 1000 {
+			c.JSON(http.StatusBadRequest, BatchCreateLicenseResponse{
+				Code: 400,
+				Msg:  "生成数量必须在 1-1000 之间",
+				Data: []models.License{},
+				Total: 0,
+			})
+			return
+		}
+
+		// 设置默认值
+		if req.MaxDevices <= 0 {
+			req.MaxDevices = 2
+		}
+
+		// 批量创建 License
+		var licenses []models.License
+		var successCount int
+		var failedKeys []string
+
+		for i := 1; i <= req.Count; i++ {
+			// 生成激活码:前缀 + 32位随机字符串
+			key := req.Prefix + "-" + generateRandomKey(32)
+
+			// 检查 Key 是否已存在
+			var existingLicense models.License
+			result := db.Where("key = ?", key).First(&existingLicense)
+			if result.Error == nil {
+				// Key 已存在,跳过并记录
+				failedKeys = append(failedKeys, key)
+				continue
+			}
+			if result.Error != gorm.ErrRecordNotFound {
+				// 数据库查询错误
+				failedKeys = append(failedKeys, key)
+				continue
+			}
+
+			// 创建新的 License
+			license := models.License{
+				Key:          key,
+				MaxDevices:   req.MaxDevices,
+				BoundDevices: "[]",
+			}
+
+			if err := db.Create(&license).Error; err != nil {
+				failedKeys = append(failedKeys, key)
+				continue
+			}
+
+			licenses = append(licenses, license)
+			successCount++
+		}
+
+		// 构建响应消息
+		msg := "成功创建 " + strconv.Itoa(successCount) + " 个 License"
+		if len(failedKeys) > 0 {
+			msg += ",跳过 " + strconv.Itoa(len(failedKeys)) + " 个重复或失败的激活码"
+		}
+
+		c.JSON(http.StatusOK, BatchCreateLicenseResponse{
+			Code: 0,
+			Msg:  msg,
+			Data: licenses,
+			Total: successCount,
+		})
+	}
+}
+
+// generateRandomKey 生成随机激活码后缀
+func generateRandomKey(length int) string {
+	const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	b := make([]byte, length)
+	for i := range b {
+		b[i] = charset[getRandomInt(len(charset))]
+	}
+	return string(b)
+}
+
+// getRandomInt 获取随机整数
+func getRandomInt(max int) int {
+	return rand.Intn(max)
+}
+

+ 174 - 0
handlers/verify.go

@@ -0,0 +1,174 @@
+package handlers
+
+import (
+	"license-check/models"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm"
+)
+
+// VerifyRequest 验证请求结构
+type VerifyRequest struct {
+	Key      string `json:"key" binding:"required"`       // 激活码
+	DeviceID string `json:"device_id" binding:"required"` // 设备ID(客户端UUID)
+}
+
+// VerifyResponse 验证响应结构
+type VerifyResponse struct {
+	Code int    `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string `json:"msg"`  // 响应消息
+	Data struct {
+		Valid bool `json:"valid"` // 验证是否有效
+	} `json:"data"`
+}
+
+// VerifyLicense 验证许可证
+// 核心逻辑:
+// 1. 在数据库中查找该 key,如果不存在返回错误
+// 2. 解析 BoundDevices 字段(JSON 字符串反序列化为 Slice)
+// 3. 情况A: device_id 已在列表中 -> 验证成功
+// 4. 情况B: device_id 不在列表中,且列表长度 < MaxDevices -> 绑定新设备并保存 -> 验证成功
+// 5. 情况C: device_id 不在列表中,且列表长度 >= MaxDevices -> 验证失败
+func VerifyLicense(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// 解析请求参数
+		var req VerifyRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, VerifyResponse{
+				Code: 400,
+				Msg:  "请求参数错误: " + err.Error(),
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: false,
+				},
+			})
+			return
+		}
+
+		// 在数据库中查找该 key
+		var license models.License
+		result := db.Where("key = ?", req.Key).First(&license)
+		if result.Error != nil {
+			// 如果不存在,返回 404 或错误信息
+			if result.Error == gorm.ErrRecordNotFound {
+				c.JSON(http.StatusOK, VerifyResponse{
+					Code: 400,
+					Msg:  "无效的激活码",
+					Data: struct {
+						Valid bool `json:"valid"`
+					}{
+						Valid: false,
+					},
+				})
+				return
+			}
+			// 数据库查询错误
+			c.JSON(http.StatusInternalServerError, VerifyResponse{
+				Code: 500,
+				Msg:  "数据库查询错误: " + result.Error.Error(),
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: false,
+				},
+			})
+			return
+		}
+
+		// 解析 BoundDevices 字段(JSON 字符串反序列化为 Slice)
+		boundDevices, err := license.GetBoundDeviceList()
+		if err != nil {
+			// 解析失败,返回错误
+			c.JSON(http.StatusInternalServerError, VerifyResponse{
+				Code: 500,
+				Msg:  "解析绑定设备列表失败: " + err.Error(),
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: false,
+				},
+			})
+			return
+		}
+
+		// 情况A: 如果 device_id 已经在 BoundDevices 列表中 -> 验证成功
+		isBound := false
+		for _, deviceID := range boundDevices {
+			if deviceID == req.DeviceID {
+				isBound = true
+				break
+			}
+		}
+
+		if isBound {
+			c.JSON(http.StatusOK, VerifyResponse{
+				Code: 0,
+				Msg:  "success",
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: true,
+				},
+			})
+			return
+		}
+
+		// 情况C: 如果 device_id 不在列表中,且列表长度 >= MaxDevices -> 验证失败
+		if len(boundDevices) >= license.MaxDevices {
+			c.JSON(http.StatusOK, VerifyResponse{
+				Code: 400,
+				Msg:  "设备数已满",
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: false,
+				},
+			})
+			return
+		}
+
+		// 情况B: 如果 device_id 不在列表中,且当前列表长度 < MaxDevices
+		// 将该 device_id 加入列表,重新序列化为 JSON 并保存回数据库
+		err = license.AddDevice(req.DeviceID)
+		if err != nil {
+			c.JSON(http.StatusInternalServerError, VerifyResponse{
+				Code: 500,
+				Msg:  "绑定设备失败: " + err.Error(),
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: false,
+				},
+			})
+			return
+		}
+
+		// 保存到数据库
+		result = db.Save(&license)
+		if result.Error != nil {
+			c.JSON(http.StatusInternalServerError, VerifyResponse{
+				Code: 500,
+				Msg:  "保存绑定信息失败: " + result.Error.Error(),
+				Data: struct {
+					Valid bool `json:"valid"`
+				}{
+					Valid: false,
+				},
+			})
+			return
+		}
+
+		// 验证成功,返回信息中提示"新设备已绑定"
+		c.JSON(http.StatusOK, VerifyResponse{
+			Code: 0,
+			Msg:  "success",
+			Data: struct {
+				Valid bool `json:"valid"`
+			}{
+				Valid: true,
+			},
+		})
+	}
+}

+ 110 - 0
main.go

@@ -0,0 +1,110 @@
+package main
+
+import (
+	"license-check/database"
+	"license-check/handlers"
+	"license-check/middleware"
+	"license-check/models"
+	"log"
+
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm"
+)
+
+// seedData 初始化种子数据
+// 在数据库为空时,自动插入一条测试数据
+func seedData(db *gorm.DB) error {
+	var count int64
+	// 检查数据库中是否已有数据
+	if err := db.Model(&models.License{}).Count(&count).Error; err != nil {
+		return err
+	}
+
+	// 如果数据库为空,插入测试数据
+	if count == 0 {
+		testLicense := models.License{
+			Key:          "TEST-KEY-123456",
+			BoundDevices: "[]", // 空数组
+			MaxDevices:   2,
+		}
+		if err := db.Create(&testLicense).Error; err != nil {
+			return err
+		}
+		log.Println("已插入测试数据: Key=TEST-KEY-123456, MaxDevices=2")
+	}
+
+	return nil
+}
+
+func main() {
+	// 初始化数据库
+	if err := database.InitDB(); err != nil {
+		log.Fatalf("初始化数据库失败: %v", err)
+	}
+
+	// 初始化种子数据(如果数据库为空)
+	if err := seedData(database.DB); err != nil {
+		log.Fatalf("初始化种子数据失败: %v", err)
+	}
+
+	// 创建 Gin 路由
+	r := gin.Default()
+
+	// 添加 CORS 中间件(Chrome 插件可能需要)
+	r.Use(func(c *gin.Context) {
+		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
+		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
+
+		if c.Request.Method == "OPTIONS" {
+			c.AbortWithStatus(204)
+			return
+		}
+
+		c.Next()
+	})
+
+	// 静态文件服务(前端页面)
+	r.Static("/web", "./web")
+	r.GET("/", func(c *gin.Context) {
+		c.Redirect(302, "/web/login.html")
+	})
+
+	// 健康检查接口
+	r.GET("/health", func(c *gin.Context) {
+		c.JSON(200, gin.H{
+			"status": "ok",
+		})
+	})
+
+	// API 路由组
+	api := r.Group("/api")
+	{
+		// 公开接口(不需要认证)
+		api.POST("/verify", handlers.VerifyLicense(database.DB)) // License 验证接口(Chrome插件使用)
+		api.POST("/login", handlers.Login())                     // 登录接口
+
+		// 需要认证的管理接口
+		api.Use(middleware.AuthMiddleware())
+		{
+			// License 管理接口(CRUD)
+			licenses := api.Group("/licenses")
+			{
+				licenses.POST("", handlers.CreateLicense(database.DB))              // 创建 License
+				licenses.POST("/batch", handlers.BatchCreateLicense(database.DB))   // 批量创建 License
+				licenses.DELETE("/batch", handlers.BatchDeleteLicense(database.DB)) // 批量删除 License
+				licenses.GET("", handlers.GetLicenseList(database.DB))              // 获取 License 列表
+				licenses.GET("/:id", handlers.GetLicense(database.DB))              // 获取单个 License
+				licenses.PUT("/:id", handlers.UpdateLicense(database.DB))           // 更新 License
+				licenses.DELETE("/:id", handlers.DeleteLicense(database.DB))        // 删除 License
+			}
+		}
+	}
+
+	// 启动服务器
+	log.Println("License 验证服务启动在 :8080")
+	if err := r.Run(":8080"); err != nil {
+		log.Fatalf("启动服务器失败: %v", err)
+	}
+}

+ 52 - 0
middleware/auth.go

@@ -0,0 +1,52 @@
+package middleware
+
+import (
+	"net/http"
+	"os"
+
+	"github.com/gin-gonic/gin"
+)
+
+// AuthToken 从环境变量或默认值获取认证token
+func GetAuthToken() string {
+	token := os.Getenv("AUTH_TOKEN")
+	if token == "" {
+		// 默认token,生产环境应该通过环境变量设置
+		return "admin-token-123456"
+	}
+	return token
+}
+
+// AuthMiddleware Token认证中间件
+func AuthMiddleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// 从Header中获取token
+		token := c.GetHeader("Authorization")
+		
+		// 如果Header中没有,尝试从Query参数获取
+		if token == "" {
+			token = c.Query("token")
+		}
+
+		// 移除 "Bearer " 前缀(如果存在)
+		if len(token) > 7 && token[:7] == "Bearer " {
+			token = token[7:]
+		}
+
+		// 验证token
+		expectedToken := GetAuthToken()
+		if token != expectedToken {
+			c.JSON(http.StatusUnauthorized, gin.H{
+				"code": 401,
+				"msg":  "未授权,请先登录",
+			})
+			c.Abort()
+			return
+		}
+
+		// token验证通过,继续处理
+		c.Next()
+	}
+}
+
+

+ 121 - 0
models/license.go

@@ -0,0 +1,121 @@
+package models
+
+import (
+	"encoding/json"
+	"time"
+)
+
+// License 许可证模型
+type License struct {
+	ID              uint      `gorm:"primarykey" json:"id"`
+	Key             string    `gorm:"uniqueIndex;not null" json:"key"`
+	BoundDevices    string    `gorm:"type:text" json:"bound_devices"`        // JSON 数组字符串,例如 '["uuid-1", "uuid-2"]'
+	DeviceActivations string  `gorm:"type:text" json:"device_activations"`  // JSON 对象字符串,例如 '{"uuid-1": "2024-01-01T00:00:00Z", "uuid-2": "2024-01-02T00:00:00Z"}'
+	MaxDevices      int       `gorm:"default:2" json:"max_devices"`
+	CreatedAt       time.Time `json:"created_at"`
+	UpdatedAt       time.Time `json:"updated_at"`
+}
+
+// GetBoundDeviceList 获取已绑定设备ID列表
+func (l *License) GetBoundDeviceList() ([]string, error) {
+	if l.BoundDevices == "" {
+		return []string{}, nil
+	}
+	var devices []string
+	err := json.Unmarshal([]byte(l.BoundDevices), &devices)
+	return devices, err
+}
+
+// SetBoundDeviceList 设置已绑定设备ID列表
+func (l *License) SetBoundDeviceList(devices []string) error {
+	data, err := json.Marshal(devices)
+	if err != nil {
+		return err
+	}
+	l.BoundDevices = string(data)
+	return nil
+}
+
+// IsDeviceBound 检查设备是否已绑定
+func (l *License) IsDeviceBound(deviceID string) (bool, error) {
+	devices, err := l.GetBoundDeviceList()
+	if err != nil {
+		return false, err
+	}
+	for _, d := range devices {
+		if d == deviceID {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+// AddDevice 添加设备到绑定列表
+func (l *License) AddDevice(deviceID string) error {
+	devices, err := l.GetBoundDeviceList()
+	if err != nil {
+		return err
+	}
+	devices = append(devices, deviceID)
+	if err := l.SetBoundDeviceList(devices); err != nil {
+		return err
+	}
+	// 记录设备激活时间
+	return l.RecordDeviceActivation(deviceID)
+}
+
+// GetDeviceActivations 获取设备激活时间映射
+func (l *License) GetDeviceActivations() (map[string]time.Time, error) {
+	if l.DeviceActivations == "" {
+		return make(map[string]time.Time), nil
+	}
+	var activations map[string]string
+	if err := json.Unmarshal([]byte(l.DeviceActivations), &activations); err != nil {
+		return nil, err
+	}
+	
+	result := make(map[string]time.Time)
+	for deviceID, timeStr := range activations {
+		t, err := time.Parse(time.RFC3339, timeStr)
+		if err != nil {
+			return nil, err
+		}
+		result[deviceID] = t
+	}
+	return result, nil
+}
+
+// RecordDeviceActivation 记录设备激活时间
+func (l *License) RecordDeviceActivation(deviceID string) error {
+	activations, err := l.GetDeviceActivations()
+	if err != nil {
+		activations = make(map[string]time.Time)
+	}
+	
+	// 如果设备已存在,不更新激活时间;如果不存在,记录当前时间
+	if _, exists := activations[deviceID]; !exists {
+		activations[deviceID] = time.Now()
+	}
+	
+	// 转换为字符串格式存储
+	activationsStr := make(map[string]string)
+	for id, t := range activations {
+		activationsStr[id] = t.Format(time.RFC3339)
+	}
+	
+	data, err := json.Marshal(activationsStr)
+	if err != nil {
+		return err
+	}
+	l.DeviceActivations = string(data)
+	return nil
+}
+
+// CanBindMoreDevices 检查是否可以绑定更多设备
+func (l *License) CanBindMoreDevices() (bool, error) {
+	devices, err := l.GetBoundDeviceList()
+	if err != nil {
+		return false, err
+	}
+	return len(devices) < l.MaxDevices, nil
+}

+ 1227 - 0
web/index.html

@@ -0,0 +1,1227 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>License 管理平台</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            padding: 20px;
+        }
+
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+        }
+
+        .header {
+            background: white;
+            padding: 30px;
+            border-radius: 10px;
+            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+            margin-bottom: 20px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .header h1 {
+            color: #333;
+            font-size: 28px;
+        }
+
+        .btn {
+            padding: 10px 20px;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: all 0.3s;
+            font-weight: 500;
+        }
+
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+
+        .btn-primary:hover {
+            background: #5568d3;
+            transform: translateY(-2px);
+            box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+        }
+
+        .btn-danger {
+            background: #ef4444;
+            color: white;
+        }
+
+        .btn-danger:hover {
+            background: #dc2626;
+        }
+
+        .btn-secondary {
+            background: #6b7280;
+            color: white;
+        }
+
+        .btn-secondary:hover {
+            background: #4b5563;
+        }
+
+        .card {
+            background: white;
+            border-radius: 10px;
+            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+            padding: 30px;
+            margin-bottom: 20px;
+        }
+
+        .table-container {
+            overflow-x: auto;
+        }
+
+        table {
+            width: 100%;
+            border-collapse: collapse;
+        }
+
+        th, td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #e5e7eb;
+        }
+
+        th {
+            background: #f9fafb;
+            font-weight: 600;
+            color: #374151;
+        }
+
+        tr:hover {
+            background: #f9fafb;
+        }
+
+        .badge {
+            display: inline-block;
+            padding: 4px 12px;
+            border-radius: 12px;
+            font-size: 12px;
+            font-weight: 500;
+        }
+
+        .badge-success {
+            background: #d1fae5;
+            color: #065f46;
+        }
+
+        .badge-warning {
+            background: #fef3c7;
+            color: #92400e;
+        }
+
+        .modal {
+            display: none;
+            position: fixed;
+            z-index: 1000;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            background: rgba(0, 0, 0, 0.5);
+            animation: fadeIn 0.3s;
+        }
+
+        .modal.show {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        @keyframes fadeIn {
+            from { opacity: 0; }
+            to { opacity: 1; }
+        }
+
+        .modal-content {
+            background: white;
+            border-radius: 10px;
+            padding: 30px;
+            width: 90%;
+            max-width: 500px;
+            animation: slideDown 0.3s;
+        }
+
+        @keyframes slideDown {
+            from {
+                transform: translateY(-50px);
+                opacity: 0;
+            }
+            to {
+                transform: translateY(0);
+                opacity: 1;
+            }
+        }
+
+        .modal-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 20px;
+        }
+
+        .modal-header h2 {
+            color: #333;
+            font-size: 24px;
+        }
+
+        .close {
+            font-size: 28px;
+            font-weight: bold;
+            color: #999;
+            cursor: pointer;
+            border: none;
+            background: none;
+        }
+
+        .close:hover {
+            color: #333;
+        }
+
+        .form-group {
+            margin-bottom: 20px;
+        }
+
+        .form-group label {
+            display: block;
+            margin-bottom: 8px;
+            color: #374151;
+            font-weight: 500;
+        }
+
+        .form-group input {
+            width: 100%;
+            padding: 10px;
+            border: 1px solid #d1d5db;
+            border-radius: 5px;
+            font-size: 14px;
+        }
+
+        .form-group input:focus {
+            outline: none;
+            border-color: #667eea;
+            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+        }
+
+        .form-actions {
+            display: flex;
+            gap: 10px;
+            justify-content: flex-end;
+            margin-top: 20px;
+        }
+
+        .pagination {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            gap: 10px;
+            margin-top: 20px;
+        }
+
+        .pagination button {
+            padding: 8px 16px;
+            border: 1px solid #d1d5db;
+            background: white;
+            border-radius: 5px;
+            cursor: pointer;
+        }
+
+        .pagination button:hover:not(:disabled) {
+            background: #f9fafb;
+        }
+
+        .pagination button:disabled {
+            opacity: 0.5;
+            cursor: not-allowed;
+        }
+
+        .pagination .page-info {
+            color: #6b7280;
+        }
+
+        .loading {
+            text-align: center;
+            padding: 40px;
+            color: #6b7280;
+        }
+
+        .empty-state {
+            text-align: center;
+            padding: 60px 20px;
+            color: #6b7280;
+        }
+
+        .empty-state svg {
+            width: 100px;
+            height: 100px;
+            margin-bottom: 20px;
+            opacity: 0.5;
+        }
+
+        .device-list {
+            font-size: 12px;
+            color: #6b7280;
+            max-width: 200px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+
+        .actions {
+            display: flex;
+            gap: 8px;
+        }
+
+        .btn-sm {
+            padding: 6px 12px;
+            font-size: 12px;
+        }
+
+        .license-key-cell {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            max-width: 200px;
+        }
+
+        .license-key-text {
+            flex: 1;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            font-weight: 600;
+        }
+
+        .copy-btn {
+            padding: 6px 12px;
+            font-size: 12px;
+            background: #667eea;
+            color: white;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+            transition: all 0.2s;
+            flex-shrink: 0;
+            font-weight: 500;
+            display: flex;
+            align-items: center;
+            gap: 4px;
+        }
+
+        .copy-btn:hover {
+            background: #5568d3;
+            transform: translateY(-1px);
+            box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
+        }
+
+        .copy-btn:active {
+            transform: translateY(0);
+        }
+
+        /* Toast 通知样式 */
+        .toast-container {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            z-index: 10000;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+        }
+
+        .toast {
+            background: white;
+            padding: 16px 20px;
+            border-radius: 8px;
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+            display: flex;
+            align-items: center;
+            gap: 12px;
+            min-width: 300px;
+            max-width: 400px;
+            animation: slideInRight 0.3s ease-out;
+            position: relative;
+        }
+
+        @keyframes slideInRight {
+            from {
+                transform: translateX(100%);
+                opacity: 0;
+            }
+            to {
+                transform: translateX(0);
+                opacity: 1;
+            }
+        }
+
+        .toast.success {
+            border-left: 4px solid #10b981;
+        }
+
+        .toast.error {
+            border-left: 4px solid #ef4444;
+        }
+
+        .toast.warning {
+            border-left: 4px solid #f59e0b;
+        }
+
+        .toast.info {
+            border-left: 4px solid #3b82f6;
+        }
+
+        .toast-icon {
+            font-size: 20px;
+            flex-shrink: 0;
+        }
+
+        .toast.success .toast-icon {
+            color: #10b981;
+        }
+
+        .toast.error .toast-icon {
+            color: #ef4444;
+        }
+
+        .toast.warning .toast-icon {
+            color: #f59e0b;
+        }
+
+        .toast.info .toast-icon {
+            color: #3b82f6;
+        }
+
+        .toast-message {
+            flex: 1;
+            color: #374151;
+            font-size: 14px;
+            line-height: 1.5;
+        }
+
+        .toast-close {
+            background: none;
+            border: none;
+            font-size: 18px;
+            color: #9ca3af;
+            cursor: pointer;
+            padding: 0;
+            width: 20px;
+            height: 20px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            flex-shrink: 0;
+        }
+
+        .toast-close:hover {
+            color: #374151;
+        }
+
+        /* 确认对话框样式 */
+        .confirm-dialog {
+            display: none;
+            position: fixed;
+            z-index: 10001;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            background: rgba(0, 0, 0, 0.5);
+            align-items: center;
+            justify-content: center;
+            animation: fadeIn 0.3s;
+        }
+
+        .confirm-dialog.show {
+            display: flex;
+        }
+
+        .confirm-content {
+            background: white;
+            border-radius: 10px;
+            padding: 30px;
+            width: 90%;
+            max-width: 400px;
+            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+            animation: slideDown 0.3s;
+        }
+
+        .confirm-icon {
+            font-size: 48px;
+            text-align: center;
+            margin-bottom: 20px;
+        }
+
+        .confirm-title {
+            font-size: 20px;
+            font-weight: 600;
+            color: #374151;
+            text-align: center;
+            margin-bottom: 15px;
+        }
+
+        .confirm-message {
+            color: #6b7280;
+            text-align: center;
+            margin-bottom: 25px;
+            line-height: 1.6;
+        }
+
+        .confirm-actions {
+            display: flex;
+            gap: 10px;
+            justify-content: center;
+        }
+
+        .confirm-actions .btn {
+            min-width: 100px;
+        }
+    </style>
+</head>
+<body>
+    <!-- Toast 通知容器 -->
+    <div class="toast-container" id="toast-container"></div>
+
+    <!-- 确认对话框 -->
+    <div id="confirmDialog" class="confirm-dialog">
+        <div class="confirm-content">
+            <div class="confirm-icon">⚠️</div>
+            <div class="confirm-title" id="confirm-title">确认操作</div>
+            <div class="confirm-message" id="confirm-message"></div>
+            <div class="confirm-actions">
+                <button class="btn btn-secondary" onclick="closeConfirmDialog(false)">取消</button>
+                <button class="btn btn-danger" onclick="closeConfirmDialog(true)" id="confirm-ok-btn">确定</button>
+            </div>
+        </div>
+    </div>
+
+    <div class="container">
+        <div class="header">
+            <h1>🔑 License 管理平台</h1>
+            <div style="display: flex; gap: 10px;">
+                <button class="btn btn-primary" onclick="openBatchModal()">📦 批量生成</button>
+                <button class="btn btn-primary" onclick="openCreateModal()">+ 创建 License</button>
+            </div>
+        </div>
+
+        <div class="card">
+            <div id="loading" class="loading" style="display: none;">加载中...</div>
+            <div id="empty-state" class="empty-state" style="display: none;">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                </svg>
+                <p>暂无 License 数据</p>
+            </div>
+            <div class="table-container" id="table-container" style="display: none;">
+                <div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
+                    <div style="display: flex; align-items: center; gap: 10px;">
+                        <input type="checkbox" id="select-all" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
+                        <label for="select-all" style="cursor: pointer; user-select: none;">全选</label>
+                        <span id="selected-count" style="color: #6b7280; font-size: 14px;">已选择 0 项</span>
+                    </div>
+                    <button class="btn btn-danger" id="batch-delete-btn" onclick="batchDeleteLicenses()" style="display: none;">
+                        批量删除
+                    </button>
+                </div>
+                <table>
+                    <thead>
+                        <tr>
+                            <th style="width: 50px;">
+                                <input type="checkbox" id="select-all-header" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
+                            </th>
+                            <th>ID</th>
+                            <th>激活码</th>
+                            <th>最大设备数</th>
+                            <th>已绑定设备</th>
+                            <th>设备激活时间</th>
+                            <th>绑定设备数</th>
+                            <th>创建时间</th>
+                            <th>操作</th>
+                        </tr>
+                    </thead>
+                    <tbody id="license-table-body">
+                    </tbody>
+                </table>
+                <div class="pagination" id="pagination"></div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 创建/编辑 Modal -->
+    <div id="licenseModal" class="modal">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h2 id="modal-title">创建 License</h2>
+                <button class="close" onclick="closeModal()">&times;</button>
+            </div>
+            <form id="licenseForm" onsubmit="handleSubmit(event)">
+                <input type="hidden" id="license-id">
+                <div class="form-group">
+                    <label for="license-key">激活码 *</label>
+                    <input type="text" id="license-key" required placeholder="例如: VIP-8888">
+                </div>
+                <div class="form-group">
+                    <label for="license-max-devices">最大设备数 *</label>
+                    <input type="number" id="license-max-devices" required min="1" value="2">
+                </div>
+                <div class="form-group">
+                    <label for="license-bound-devices">已绑定设备 (JSON 数组,可选)</label>
+                    <input type="text" id="license-bound-devices" placeholder='例如: ["device-1", "device-2"]'>
+                </div>
+                <div class="form-actions">
+                    <button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
+                    <button type="submit" class="btn btn-primary">保存</button>
+                </div>
+            </form>
+        </div>
+    </div>
+
+    <!-- 批量生成 Modal -->
+    <div id="batchModal" class="modal">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h2>批量生成 License</h2>
+                <button class="close" onclick="closeBatchModal()">&times;</button>
+            </div>
+            <form id="batchForm" onsubmit="handleBatchSubmit(event)">
+                <div class="form-group">
+                    <label for="batch-prefix">激活码前缀 *</label>
+                    <input type="text" id="batch-prefix" required placeholder="例如: VIP" value="VIP">
+                    <small style="color: #6b7280; font-size: 12px; margin-top: 4px; display: block;">
+                        生成的激活码格式:前缀-随机32位字符串(如:VIP-A3B9C2D4E5F6G7H8I9J0K1L2M3N4O5P6)
+                    </small>
+                </div>
+                <div class="form-group">
+                    <label for="batch-count">生成数量 *</label>
+                    <input type="number" id="batch-count" required min="1" max="1000" value="10">
+                    <small style="color: #6b7280; font-size: 12px; margin-top: 4px; display: block;">
+                        一次最多可生成 1000 个
+                    </small>
+                </div>
+                <div class="form-group">
+                    <label for="batch-max-devices">最大设备数 *</label>
+                    <input type="number" id="batch-max-devices" required min="1" value="2">
+                </div>
+                <div class="form-actions">
+                    <button type="button" class="btn btn-secondary" onclick="closeBatchModal()">取消</button>
+                    <button type="submit" class="btn btn-primary">生成</button>
+                </div>
+            </form>
+        </div>
+    </div>
+
+    <script>
+        const API_BASE = 'http://localhost:8080/api';
+        let currentPage = 1;
+        let pageSize = 10;
+        let total = 0;
+        let editingId = null;
+
+        // Toast 通知函数
+        function showToast(message, type = 'info', duration = 3000) {
+            const container = document.getElementById('toast-container');
+            const toast = document.createElement('div');
+            toast.className = `toast ${type}`;
+            
+            const icons = {
+                success: '✅',
+                error: '❌',
+                warning: '⚠️',
+                info: 'ℹ️'
+            };
+            
+            toast.innerHTML = `
+                <span class="toast-icon">${icons[type] || icons.info}</span>
+                <span class="toast-message">${message}</span>
+                <button class="toast-close" onclick="this.parentElement.remove()">&times;</button>
+            `;
+            
+            container.appendChild(toast);
+            
+            // 自动移除
+            setTimeout(() => {
+                if (toast.parentElement) {
+                    toast.style.animation = 'slideInRight 0.3s ease-out reverse';
+                    setTimeout(() => {
+                        if (toast.parentElement) {
+                            toast.remove();
+                        }
+                    }, 300);
+                }
+            }, duration);
+        }
+
+        // 确认对话框函数
+        let confirmCallback = null;
+        
+        function showConfirmDialog(message, title = '确认操作', okText = '确定', okType = 'danger') {
+            return new Promise((resolve) => {
+                document.getElementById('confirm-title').textContent = title;
+                document.getElementById('confirm-message').textContent = message;
+                const okBtn = document.getElementById('confirm-ok-btn');
+                okBtn.textContent = okText;
+                okBtn.className = `btn btn-${okType}`;
+                
+                confirmCallback = resolve;
+                document.getElementById('confirmDialog').classList.add('show');
+            });
+        }
+        
+        function closeConfirmDialog(confirmed) {
+            document.getElementById('confirmDialog').classList.remove('show');
+            if (confirmCallback) {
+                confirmCallback(confirmed);
+                confirmCallback = null;
+            }
+        }
+
+        // 复制激活码到剪贴板
+        async function copyLicenseKey(key) {
+            try {
+                await navigator.clipboard.writeText(key);
+                showToast('激活码已复制到剪贴板', 'success', 2000);
+            } catch (err) {
+                // 降级方案:使用传统方法
+                const textArea = document.createElement('textarea');
+                textArea.value = key;
+                textArea.style.position = 'fixed';
+                textArea.style.left = '-999999px';
+                document.body.appendChild(textArea);
+                textArea.select();
+                try {
+                    document.execCommand('copy');
+                    showToast('激活码已复制到剪贴板', 'success', 2000);
+                } catch (err) {
+                    showToast('复制失败,请手动复制', 'error');
+                }
+                document.body.removeChild(textArea);
+            }
+        }
+
+        // 全选/取消全选
+        function toggleSelectAll() {
+            const selectAll = document.getElementById('select-all');
+            const selectAllHeader = document.getElementById('select-all-header');
+            const checkboxes = document.querySelectorAll('.license-checkbox');
+            const isChecked = selectAll.checked || selectAllHeader.checked;
+            
+            checkboxes.forEach(checkbox => {
+                checkbox.checked = isChecked;
+            });
+            
+            // 同步两个全选复选框
+            selectAll.checked = isChecked;
+            selectAllHeader.checked = isChecked;
+            
+            updateSelectedCount();
+        }
+
+        // 更新选中数量
+        function updateSelectedCount() {
+            const checkboxes = document.querySelectorAll('.license-checkbox:checked');
+            const count = checkboxes.length;
+            const selectedCountEl = document.getElementById('selected-count');
+            const batchDeleteBtn = document.getElementById('batch-delete-btn');
+            
+            selectedCountEl.textContent = `已选择 ${count} 项`;
+            
+            if (count > 0) {
+                batchDeleteBtn.style.display = 'block';
+            } else {
+                batchDeleteBtn.style.display = 'none';
+            }
+            
+            // 更新全选复选框状态
+            const allCheckboxes = document.querySelectorAll('.license-checkbox');
+            const allChecked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
+            document.getElementById('select-all').checked = allChecked;
+            document.getElementById('select-all-header').checked = allChecked;
+        }
+
+        // 批量删除 License
+        async function batchDeleteLicenses() {
+            const checkboxes = document.querySelectorAll('.license-checkbox:checked');
+            const selectedIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
+            
+            if (selectedIds.length === 0) {
+                showToast('请至少选择一个 License', 'warning');
+                return;
+            }
+            
+            const confirmed = await showConfirmDialog(
+                `确定要删除选中的 ${selectedIds.length} 个 License 吗?此操作不可恢复!`,
+                '确认批量删除',
+                '删除',
+                'danger'
+            );
+            
+            if (!confirmed) {
+                return;
+            }
+            
+            try {
+                const response = await apiRequest(`${API_BASE}/licenses/batch`, {
+                    method: 'DELETE',
+                    body: JSON.stringify({
+                        ids: selectedIds
+                    })
+                });
+                if (!response) return;
+                
+                const result = await response.json();
+                
+                if (result.code === 0) {
+                    showToast(result.msg, 'success');
+                    loadLicenses(currentPage);
+                } else {
+                    showToast('批量删除失败: ' + result.msg, 'error');
+                }
+            } catch (error) {
+                showToast('请求失败: ' + error.message, 'error');
+            }
+        }
+
+        // 获取认证token
+        function getAuthToken() {
+            return localStorage.getItem('auth_token');
+        }
+
+        // 检查是否已登录
+        function checkAuth() {
+            const token = getAuthToken();
+            if (!token) {
+                window.location.href = '/web/login.html';
+                return false;
+            }
+            return true;
+        }
+
+        // 获取API请求头(包含token)
+        function getAuthHeaders() {
+            const token = getAuthToken();
+            return {
+                'Content-Type': 'application/json',
+                'Authorization': `Bearer ${token}`
+            };
+        }
+
+        // 处理API错误响应
+        async function handleApiError(response) {
+            if (response.status === 401) {
+                // 未授权,清除token并跳转到登录页
+                localStorage.removeItem('auth_token');
+                showToast('登录已过期,请重新登录', 'error');
+                setTimeout(() => {
+                    window.location.href = '/web/login.html';
+                }, 1000);
+                return true;
+            }
+            return false;
+        }
+
+        // 统一的API请求函数
+        async function apiRequest(url, options = {}) {
+            const headers = getAuthHeaders();
+            if (options.headers) {
+                Object.assign(headers, options.headers);
+            }
+            
+            const response = await fetch(url, {
+                ...options,
+                headers: headers
+            });
+            
+            if (await handleApiError(response)) {
+                return null;
+            }
+            
+            return response;
+        }
+
+        // 页面加载时检查登录状态
+        window.onload = () => {
+            if (checkAuth()) {
+                loadLicenses();
+            }
+        };
+
+        // 加载 License 列表
+        async function loadLicenses(page = 1) {
+            currentPage = page;
+            const loadingEl = document.getElementById('loading');
+            const tableContainer = document.getElementById('table-container');
+            const emptyState = document.getElementById('empty-state');
+
+            loadingEl.style.display = 'block';
+            tableContainer.style.display = 'none';
+            emptyState.style.display = 'none';
+
+            try {
+                const response = await apiRequest(`${API_BASE}/licenses?page=${page}&page_size=${pageSize}`);
+                if (!response) return;
+                
+                const result = await response.json();
+
+                if (result.code === 0) {
+                    total = result.total;
+                    const licenses = result.data;
+
+                    if (licenses.length === 0) {
+                        loadingEl.style.display = 'none';
+                        emptyState.style.display = 'block';
+                        return;
+                    }
+
+                    renderTable(licenses);
+                    renderPagination();
+                    loadingEl.style.display = 'none';
+                    tableContainer.style.display = 'block';
+                } else {
+                    showToast('加载失败: ' + result.msg, 'error');
+                    loadingEl.style.display = 'none';
+                }
+            } catch (error) {
+                showToast('请求失败: ' + error.message, 'error');
+                loadingEl.style.display = 'none';
+            }
+        }
+
+        // 渲染表格
+        function renderTable(licenses) {
+            const tbody = document.getElementById('license-table-body');
+            tbody.innerHTML = licenses.map(license => {
+                let boundDevices = [];
+                try {
+                    boundDevices = JSON.parse(license.bound_devices || '[]');
+                } catch (e) {
+                    boundDevices = [];
+                }
+                
+                // 解析设备激活时间
+                let deviceActivations = {};
+                try {
+                    const activationsStr = JSON.parse(license.device_activations || '{}');
+                    deviceActivations = activationsStr;
+                } catch (e) {
+                    deviceActivations = {};
+                }
+                
+                // 构建设备激活时间显示
+                let activationTimesHtml = '无';
+                if (boundDevices.length > 0) {
+                    activationTimesHtml = boundDevices.map(deviceId => {
+                        const activationTime = deviceActivations[deviceId];
+                        if (activationTime) {
+                            const date = new Date(activationTime);
+                            return `${deviceId}<br><small style="color: #6b7280;">${date.toLocaleString('zh-CN')}</small>`;
+                        }
+                        return `${deviceId}<br><small style="color: #9ca3af;">未记录</small>`;
+                    }).join('<br><br>');
+                }
+                
+                const boundCount = boundDevices.length;
+                const isFull = boundCount >= license.max_devices;
+                const createdDate = new Date(license.created_at).toLocaleString('zh-CN');
+
+                // 限制激活码显示长度为10个字符
+                const displayKey = license.key.length > 10 ? license.key.substring(0, 10) + '...' : license.key;
+                
+                return `
+                    <tr>
+                        <td style="text-align: center;">
+                            <input type="checkbox" class="license-checkbox" value="${license.id}" onchange="updateSelectedCount()" style="width: 18px; height: 18px; cursor: pointer;">
+                        </td>
+                        <td>${license.id}</td>
+                        <td>
+                            <div class="license-key-cell" title="${license.key}">
+                                <span class="license-key-text">${displayKey}</span>
+                                <button class="copy-btn" onclick="copyLicenseKey('${license.key}')" title="复制激活码">
+                                    <span>复制</span>
+                                </button>
+                            </div>
+                        </td>
+                        <td>${license.max_devices}</td>
+                        <td class="device-list" title="${boundDevices.join(', ') || '无'}">
+                            ${boundDevices.length > 0 ? boundDevices.join(', ') : '无'}
+                        </td>
+                        <td style="font-size: 12px; line-height: 1.6; max-width: 300px;">
+                            ${activationTimesHtml}
+                        </td>
+                        <td>
+                            <span class="badge ${isFull ? 'badge-warning' : 'badge-success'}">
+                                ${boundCount} / ${license.max_devices}
+                            </span>
+                        </td>
+                        <td>${createdDate}</td>
+                        <td>
+                            <div class="actions">
+                                <button class="btn btn-primary btn-sm" onclick="editLicense(${license.id})">编辑</button>
+                                <button class="btn btn-danger btn-sm" onclick="deleteLicense(${license.id}, '${license.key}')">删除</button>
+                            </div>
+                        </td>
+                    </tr>
+                `;
+            }).join('');
+        }
+
+        // 渲染分页
+        function renderPagination() {
+            const pagination = document.getElementById('pagination');
+            const totalPages = Math.ceil(total / pageSize);
+
+            if (totalPages <= 1) {
+                pagination.innerHTML = '';
+                return;
+            }
+
+            pagination.innerHTML = `
+                <button onclick="loadLicenses(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
+                    上一页
+                </button>
+                <span class="page-info">第 ${currentPage} / ${totalPages} 页 (共 ${total} 条)</span>
+                <button onclick="loadLicenses(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>
+                    下一页
+                </button>
+            `;
+        }
+
+        // 打开创建 Modal
+        function openCreateModal() {
+            editingId = null;
+            document.getElementById('modal-title').textContent = '创建 License';
+            document.getElementById('license-id').value = '';
+            document.getElementById('license-key').value = '';
+            document.getElementById('license-max-devices').value = '2';
+            document.getElementById('license-bound-devices').value = '';
+            document.getElementById('license-key').disabled = false;
+            document.getElementById('licenseModal').classList.add('show');
+        }
+
+        // 编辑 License
+        async function editLicense(id) {
+            try {
+                const response = await apiRequest(`${API_BASE}/licenses/${id}`);
+                if (!response) return;
+                
+                const result = await response.json();
+
+                if (result.code === 0) {
+                    const license = result.data;
+                    editingId = id;
+                    document.getElementById('modal-title').textContent = '编辑 License';
+                    document.getElementById('license-id').value = id;
+                    document.getElementById('license-key').value = license.key;
+                    document.getElementById('license-max-devices').value = license.max_devices;
+                    document.getElementById('license-bound-devices').value = license.bound_devices || '[]';
+                    document.getElementById('license-key').disabled = false;
+                    document.getElementById('licenseModal').classList.add('show');
+                } else {
+                    showToast('加载失败: ' + result.msg, 'error');
+                }
+            } catch (error) {
+                showToast('请求失败: ' + error.message, 'error');
+            }
+        }
+
+        // 关闭 Modal
+        function closeModal() {
+            document.getElementById('licenseModal').classList.remove('show');
+        }
+
+        // 提交表单
+        async function handleSubmit(event) {
+            event.preventDefault();
+
+            const id = document.getElementById('license-id').value;
+            const key = document.getElementById('license-key').value;
+            const maxDevices = parseInt(document.getElementById('license-max-devices').value);
+            const boundDevices = document.getElementById('license-bound-devices').value || '[]';
+
+            // 验证 boundDevices 是否为有效 JSON
+            try {
+                JSON.parse(boundDevices);
+            } catch (e) {
+                showToast('已绑定设备必须是有效的 JSON 数组格式', 'error');
+                return;
+            }
+
+            try {
+                let response;
+                if (editingId) {
+                    // 更新
+                    const updateData = {
+                        max_devices: maxDevices
+                    };
+                    if (boundDevices) {
+                        updateData.bound_devices = boundDevices;
+                    }
+                    response = await apiRequest(`${API_BASE}/licenses/${id}`, {
+                        method: 'PUT',
+                        body: JSON.stringify(updateData)
+                    });
+                } else {
+                    // 创建
+                    response = await apiRequest(`${API_BASE}/licenses`, {
+                        method: 'POST',
+                        body: JSON.stringify({
+                            key: key,
+                            max_devices: maxDevices,
+                            bound_devices: boundDevices
+                        })
+                    });
+                }
+
+                if (!response) return;
+                
+                const result = await response.json();
+
+                if (result.code === 0) {
+                    showToast(editingId ? '更新成功' : '创建成功', 'success');
+                    closeModal();
+                    loadLicenses(currentPage);
+                } else {
+                    showToast('操作失败: ' + result.msg, 'error');
+                }
+            } catch (error) {
+                showToast('请求失败: ' + error.message, 'error');
+            }
+        }
+
+        // 删除 License
+        async function deleteLicense(id, key) {
+            const confirmed = await showConfirmDialog(
+                `确定要删除 License "${key}" 吗?此操作不可恢复!`,
+                '确认删除',
+                '删除',
+                'danger'
+            );
+            
+            if (!confirmed) {
+                return;
+            }
+
+            try {
+                const response = await apiRequest(`${API_BASE}/licenses/${id}`, {
+                    method: 'DELETE'
+                });
+                if (!response) return;
+                
+                const result = await response.json();
+
+                if (result.code === 0) {
+                    showToast('删除成功', 'success');
+                    loadLicenses(currentPage);
+                } else {
+                    showToast('删除失败: ' + result.msg, 'error');
+                }
+            } catch (error) {
+                showToast('请求失败: ' + error.message, 'error');
+            }
+        }
+
+        // 打开批量生成 Modal
+        function openBatchModal() {
+            document.getElementById('batch-prefix').value = 'VIP';
+            document.getElementById('batch-count').value = '10';
+            document.getElementById('batch-max-devices').value = '2';
+            document.getElementById('batchModal').classList.add('show');
+        }
+
+        // 关闭批量生成 Modal
+        function closeBatchModal() {
+            document.getElementById('batchModal').classList.remove('show');
+        }
+
+        // 批量生成提交
+        async function handleBatchSubmit(event) {
+            event.preventDefault();
+
+            const prefix = document.getElementById('batch-prefix').value.trim();
+            const count = parseInt(document.getElementById('batch-count').value);
+            const maxDevices = parseInt(document.getElementById('batch-max-devices').value);
+
+            if (!prefix) {
+                showToast('请输入激活码前缀', 'warning');
+                return;
+            }
+
+            if (count <= 0 || count > 1000) {
+                showToast('生成数量必须在 1-1000 之间', 'warning');
+                return;
+            }
+
+            const confirmed = await showConfirmDialog(
+                `确定要批量生成 ${count} 个激活码吗?`,
+                '确认批量生成',
+                '生成',
+                'primary'
+            );
+            
+            if (!confirmed) {
+                return;
+            }
+
+            try {
+                const response = await apiRequest(`${API_BASE}/licenses/batch`, {
+                    method: 'POST',
+                    body: JSON.stringify({
+                        prefix: prefix,
+                        count: count,
+                        max_devices: maxDevices
+                    })
+                });
+                if (!response) return;
+
+                const result = await response.json();
+
+                if (result.code === 0) {
+                    showToast(result.msg, 'success', 4000);
+                    closeBatchModal();
+                    loadLicenses(1); // 重新加载第一页
+                } else {
+                    showToast('批量生成失败: ' + result.msg, 'error');
+                }
+            } catch (error) {
+                showToast('请求失败: ' + error.message, 'error');
+            }
+        }
+
+        // 点击 Modal 外部关闭
+        window.onclick = function(event) {
+            const licenseModal = document.getElementById('licenseModal');
+            const batchModal = document.getElementById('batchModal');
+            const confirmDialog = document.getElementById('confirmDialog');
+            
+            if (event.target === licenseModal) {
+                closeModal();
+            }
+            if (event.target === batchModal) {
+                closeBatchModal();
+            }
+            if (event.target === confirmDialog) {
+                closeConfirmDialog(false);
+            }
+        }
+    </script>
+</body>
+</html>
+

+ 268 - 0
web/login.html

@@ -0,0 +1,268 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>License 管理平台 - 登录</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 20px;
+        }
+
+        .login-container {
+            background: white;
+            border-radius: 10px;
+            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+            padding: 40px;
+            width: 100%;
+            max-width: 400px;
+            animation: slideDown 0.3s;
+        }
+
+        @keyframes slideDown {
+            from {
+                transform: translateY(-50px);
+                opacity: 0;
+            }
+            to {
+                transform: translateY(0);
+                opacity: 1;
+            }
+        }
+
+        .login-header {
+            text-align: center;
+            margin-bottom: 30px;
+        }
+
+        .login-header h1 {
+            color: #333;
+            font-size: 28px;
+            margin-bottom: 10px;
+        }
+
+        .login-header p {
+            color: #6b7280;
+            font-size: 14px;
+        }
+
+        .form-group {
+            margin-bottom: 20px;
+        }
+
+        .form-group label {
+            display: block;
+            margin-bottom: 8px;
+            color: #374151;
+            font-weight: 500;
+            font-size: 14px;
+        }
+
+        .form-group input {
+            width: 100%;
+            padding: 12px;
+            border: 1px solid #d1d5db;
+            border-radius: 5px;
+            font-size: 14px;
+            transition: all 0.3s;
+        }
+
+        .form-group input:focus {
+            outline: none;
+            border-color: #667eea;
+            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+        }
+
+        .btn {
+            width: 100%;
+            padding: 12px;
+            border: none;
+            border-radius: 5px;
+            font-size: 16px;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.3s;
+        }
+
+        .btn-primary {
+            background: #667eea;
+            color: white;
+        }
+
+        .btn-primary:hover {
+            background: #5568d3;
+            transform: translateY(-2px);
+            box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
+        }
+
+        .btn-primary:active {
+            transform: translateY(0);
+        }
+
+        .btn-primary:disabled {
+            background: #9ca3af;
+            cursor: not-allowed;
+            transform: none;
+        }
+
+        .error-message {
+            background: #fee2e2;
+            color: #dc2626;
+            padding: 12px;
+            border-radius: 5px;
+            margin-bottom: 20px;
+            font-size: 14px;
+            display: none;
+        }
+
+        .error-message.show {
+            display: block;
+        }
+
+        .loading {
+            display: none;
+            text-align: center;
+            color: #6b7280;
+            margin-top: 10px;
+        }
+
+        .loading.show {
+            display: block;
+        }
+    </style>
+</head>
+<body>
+    <div class="login-container">
+        <div class="login-header">
+            <h1>🔑 License 管理平台</h1>
+            <p>请输入 Token 登录</p>
+        </div>
+
+        <div class="error-message" id="error-message"></div>
+
+        <form id="login-form" onsubmit="handleLogin(event)">
+            <div class="form-group">
+                <label for="token">Token</label>
+                <input 
+                    type="password" 
+                    id="token" 
+                    name="token" 
+                    required 
+                    placeholder="请输入认证 Token"
+                    autocomplete="off"
+                >
+            </div>
+            <button type="submit" class="btn btn-primary" id="login-btn">
+                登录
+            </button>
+            <div class="loading" id="loading">登录中...</div>
+        </form>
+    </div>
+
+    <script>
+        const API_BASE = 'http://localhost:8080/api';
+
+        // 检查是否已登录
+        window.onload = () => {
+            const token = localStorage.getItem('auth_token');
+            if (token) {
+                // 验证token是否有效
+                verifyToken(token);
+            }
+        };
+
+        // 验证token
+        async function verifyToken(token) {
+            try {
+                const response = await fetch(`${API_BASE}/login`, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({ token: token })
+                });
+
+                const result = await response.json();
+                if (result.code === 0) {
+                    // token有效,跳转到管理页面
+                    window.location.href = '/web/index.html';
+                } else {
+                    // token无效,清除本地存储
+                    localStorage.removeItem('auth_token');
+                }
+            } catch (error) {
+                // 网络错误,清除本地存储
+                localStorage.removeItem('auth_token');
+            }
+        }
+
+        // 处理登录
+        async function handleLogin(event) {
+            event.preventDefault();
+
+            const tokenInput = document.getElementById('token');
+            const token = tokenInput.value.trim();
+            const loginBtn = document.getElementById('login-btn');
+            const loading = document.getElementById('loading');
+            const errorMsg = document.getElementById('error-message');
+
+            if (!token) {
+                showError('请输入 Token');
+                return;
+            }
+
+            // 显示加载状态
+            loginBtn.disabled = true;
+            loading.classList.add('show');
+            errorMsg.classList.remove('show');
+
+            try {
+                const response = await fetch(`${API_BASE}/login`, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json'
+                    },
+                    body: JSON.stringify({ token: token })
+                });
+
+                const result = await response.json();
+
+                if (result.code === 0) {
+                    // 登录成功,保存token
+                    localStorage.setItem('auth_token', token);
+                    // 跳转到管理页面
+                    window.location.href = '/web/index.html';
+                } else {
+                    showError(result.msg || '登录失败');
+                }
+            } catch (error) {
+                showError('网络错误: ' + error.message);
+            } finally {
+                loginBtn.disabled = false;
+                loading.classList.remove('show');
+            }
+        }
+
+        // 显示错误信息
+        function showError(message) {
+            const errorMsg = document.getElementById('error-message');
+            errorMsg.textContent = message;
+            errorMsg.classList.add('show');
+        }
+    </script>
+</body>
+</html>
+
+