3
0

2 کامیت‌ها 3950c09ea0 ... 4e6532c09a

نویسنده SHA1 پیام تاریخ
  Carl 4e6532c09a Feature init license admin 2 هفته پیش
  Carl 3950c09ea0 Feature init license admin 2 هفته پیش
20فایلهای تغییر یافته به همراه1046 افزوده شده و 45 حذف شده
  1. 53 0
      .dockerignore
  2. 1 1
      .gitignore
  3. 95 0
      BUILD.md
  4. 366 0
      DOCKER.md
  5. 44 0
      Dockerfile
  6. 42 13
      README.md
  7. 109 0
      build.sh
  8. 122 0
      database/README.md
  9. 57 7
      database/database.go
  10. 31 0
      database/init.sql
  11. 23 0
      database/schema.sql
  12. 72 0
      docker-compose.yml
  13. 8 5
      go.mod
  14. 10 8
      go.sum
  15. 6 6
      handlers/license.go
  16. 1 1
      handlers/verify.go
  17. 1 1
      main.go
  18. 1 1
      models/license.go
  19. 2 1
      web/index.html
  20. 2 1
      web/login.html

+ 53 - 0
.dockerignore

@@ -0,0 +1,53 @@
+# Git 相关
+.git
+.gitignore
+.gitattributes
+
+# 构建产物(注意:license-admin 需要包含在镜像中,所以不忽略)
+main
+*.exe
+*.dll
+*.so
+*.dylib
+
+# 数据库文件
+*.db
+*.sqlite
+*.sqlite3
+
+# 开发文件
+.vscode
+.idea
+*.swp
+*.swo
+*~
+
+# 文档
+README.md
+*.md
+
+# 测试文件
+*_test.go
+testdata/
+
+# 临时文件
+*.tmp
+*.log
+.DS_Store
+Thumbs.db
+
+# Docker 相关
+Dockerfile
+.dockerignore
+docker-compose.yml
+.docker/
+
+# CI/CD
+.github
+.gitlab-ci.yml
+.travis.yml
+
+# 其他
+.env
+.env.local
+

+ 1 - 1
.gitignore

@@ -25,5 +25,5 @@ license-admin
 .DS_Store
 Thumbs.db
 
-
+license-admin.tar
 

+ 95 - 0
BUILD.md

@@ -0,0 +1,95 @@
+# 快速构建指南
+
+## 🚀 快速开始
+
+### 前置要求
+
+**注意:** 需要先编译好 Linux x86_64 二进制文件。
+
+### 1. 使用构建脚本(最简单,自动编译)
+
+```bash
+# 给脚本添加执行权限(首次运行)
+chmod +x build.sh
+
+# 构建镜像(如果二进制文件不存在会自动编译)
+./build.sh
+```
+
+### 2. 手动编译后构建
+
+```bash
+# 1. 编译二进制文件
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o license-admin .
+
+# 2. 构建 Docker 镜像
+docker build --platform linux/amd64 -t license-admin:latest .
+```
+
+### 3. 使用 Docker Compose
+
+```bash
+docker-compose build
+```
+
+## 📦 运行容器
+
+### 使用 Docker Compose(推荐)
+
+```bash
+# 设置环境变量(可选)
+export DB_PASSWORD=your-password
+export AUTH_TOKEN=your-secret-token
+
+# 启动服务(自动启动 MySQL)
+docker-compose up -d
+```
+
+### 连接外部 MySQL
+
+```bash
+docker run -d \
+  -p 8080:8080 \
+  -e DB_HOST=host.docker.internal \
+  -e DB_PORT=3306 \
+  -e DB_USER=root \
+  -e DB_PASSWORD=your-password \
+  -e DB_NAME=license_admin \
+  -e AUTH_TOKEN=your-secret-token \
+  --name license-admin \
+  license-admin:latest
+```
+
+### 连接 Docker 网络中的 MySQL
+
+```bash
+docker run -d \
+  -p 8080:8080 \
+  -e DB_HOST=mysql \
+  -e DB_PORT=3306 \
+  -e DB_USER=root \
+  -e DB_PASSWORD=your-password \
+  -e DB_NAME=license_admin \
+  -e AUTH_TOKEN=your-secret-token \
+  --name license-admin \
+  --network license-admin-network \
+  license-admin:latest
+```
+
+## ✅ 验证
+
+```bash
+# 检查容器状态
+docker ps | grep license-admin
+
+# 检查健康状态
+curl http://localhost:8080/health
+
+# 查看日志
+docker logs license-admin
+```
+
+## 📚 详细文档
+
+更多详细信息请查看 [DOCKER.md](./DOCKER.md)
+

+ 366 - 0
DOCKER.md

@@ -0,0 +1,366 @@
+# Docker 构建和使用指南
+
+本文档说明如何将 License Admin 项目打包成 Linux x86_64 Docker 镜像并运行。
+
+## 📦 构建 Docker 镜像
+
+### 前置要求
+
+**重要:** Dockerfile 已简化为仅复制已构建的二进制文件,因此需要先编译好二进制文件。
+
+### 方法一:使用构建脚本(推荐)
+
+构建脚本会自动检查并编译二进制文件(如果不存在):
+
+```bash
+# 给脚本添加执行权限
+chmod +x build.sh
+
+# 构建 latest 版本(会自动编译二进制文件)
+./build.sh
+
+# 构建指定版本
+./build.sh v1.0.0
+
+# 构建并指定注册表(用于推送到 Docker Hub 或其他注册表)
+./build.sh v1.0.0 your-registry.com
+```
+
+### 方法二:手动编译后构建镜像
+
+```bash
+# 1. 先编译 Linux x86_64 二进制文件
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o license-admin .
+
+# 2. 构建 Docker 镜像
+docker build --platform linux/amd64 -t license-admin:latest .
+
+# 构建指定版本
+docker build --platform linux/amd64 -t license-admin:v1.0.0 .
+```
+
+### 方法三:使用 Docker Compose
+
+```bash
+# 构建并启动
+docker-compose up -d --build
+
+# 仅构建
+docker-compose build
+```
+
+## 🚀 运行容器
+
+### 基本运行(需要先启动 MySQL)
+
+```bash
+# 方式1: 连接外部 MySQL(宿主机)
+docker run -d \
+  --name license-admin \
+  -p 8080:8080 \
+  -e DB_HOST=host.docker.internal \
+  -e DB_PORT=3306 \
+  -e DB_USER=root \
+  -e DB_PASSWORD=your-password \
+  -e DB_NAME=license_admin \
+  license-admin:latest
+```
+
+### 自定义认证 Token 和数据库配置
+
+```bash
+docker run -d \
+  --name license-admin \
+  -p 8080:8080 \
+  -e DB_HOST=mysql \
+  -e DB_PORT=3306 \
+  -e DB_USER=root \
+  -e DB_PASSWORD=your-password \
+  -e DB_NAME=license_admin \
+  -e AUTH_TOKEN=your-secret-token \
+  license-admin:latest
+```
+
+### 环境变量说明
+
+| 变量名 | 说明 | 默认值 | 必填 |
+|--------|------|--------|------|
+| `DB_HOST` | MySQL 主机地址 | `mysql` | 是 |
+| `DB_PORT` | MySQL 端口 | `3306` | 否 |
+| `DB_USER` | MySQL 用户名 | `root` | 否 |
+| `DB_PASSWORD` | MySQL 密码 | `password` | 是 |
+| `DB_NAME` | 数据库名称 | `license_admin` | 否 |
+| `AUTH_TOKEN` | 认证 Token | `admin-token-123456` | 否 |
+| `PORT` | 应用端口 | `8080` | 否 |
+
+### 使用 Docker Compose
+
+```bash
+# 创建数据目录
+mkdir -p ./data
+
+# 设置环境变量(可选)
+export AUTH_TOKEN=your-secret-token
+
+# 启动服务
+docker-compose up -d
+
+# 查看日志
+docker-compose logs -f
+
+# 停止服务
+docker-compose down
+```
+
+## 🔍 查看和管理
+
+### 查看容器状态
+
+```bash
+docker ps | grep license-admin
+```
+
+### 查看日志
+
+```bash
+# 实时日志
+docker logs -f license-admin
+
+# 最近 100 行日志
+docker logs --tail 100 license-admin
+```
+
+### 进入容器
+
+```bash
+docker exec -it license-admin sh
+```
+
+### 停止和删除容器
+
+```bash
+# 停止容器
+docker stop license-admin
+
+# 删除容器
+docker rm license-admin
+
+# 停止并删除
+docker rm -f license-admin
+```
+
+## 📤 推送镜像到注册表
+
+### 推送到 Docker Hub
+
+```bash
+# 登录 Docker Hub
+docker login
+
+# 标记镜像
+docker tag license-admin:latest your-username/license-admin:latest
+
+# 推送镜像
+docker push your-username/license-admin:latest
+```
+
+### 推送到私有注册表
+
+```bash
+# 标记镜像
+docker tag license-admin:latest registry.example.com/license-admin:latest
+
+# 推送镜像
+docker push registry.example.com/license-admin:latest
+```
+
+## 🔧 配置说明
+
+### 环境变量
+
+| 变量名 | 说明 | 默认值 |
+|--------|------|--------|
+| `AUTH_TOKEN` | 认证 Token | `admin-token-123456` |
+| `PORT` | 服务端口 | `8080` |
+
+### MySQL 数据库配置
+
+项目使用 MySQL 数据库,需要配置以下环境变量:
+
+```bash
+docker run -d \
+  --name license-admin \
+  -p 8080:8080 \
+  -e DB_HOST=your-mysql-host \
+  -e DB_PORT=3306 \
+  -e DB_USER=root \
+  -e DB_PASSWORD=your-password \
+  -e DB_NAME=license_admin \
+  license-admin:latest
+```
+
+### 数据持久化
+
+MySQL 数据通过 Docker volume 持久化。使用 Docker Compose 时会自动创建 `mysql_data` volume。
+
+### 端口映射
+
+默认端口为 8080,可以通过 `-p` 参数映射到其他端口:
+
+```bash
+-p 9000:8080  # 将容器 8080 端口映射到宿主机 9000 端口
+```
+
+## 🏥 健康检查
+
+镜像内置了健康检查,每 30 秒检查一次:
+
+```bash
+# 查看健康状态
+docker inspect --format='{{.State.Health.Status}}' license-admin
+```
+
+## 🐛 故障排查
+
+### 容器无法启动
+
+1. 查看容器日志:
+   ```bash
+   docker logs license-admin
+   ```
+
+2. 检查端口是否被占用:
+   ```bash
+   netstat -tuln | grep 8080
+   ```
+
+### 数据库连接问题
+
+如果遇到数据库连接问题:
+
+1. 检查 MySQL 服务是否运行:
+   ```bash
+   docker ps | grep mysql
+   ```
+
+2. 检查数据库连接配置:
+   ```bash
+   docker exec license-admin env | grep DB_
+   ```
+
+3. 测试 MySQL 连接:
+   ```bash
+   docker exec -it license-admin-mysql mysql -u root -p
+   ```
+
+4. 查看应用日志:
+   ```bash
+   docker logs license-admin
+   ```
+
+### 无法访问服务
+
+1. 检查容器是否运行:
+   ```bash
+   docker ps | grep license-admin
+   ```
+
+2. 检查端口映射:
+   ```bash
+   docker port license-admin
+   ```
+
+3. 检查防火墙设置
+
+## 📝 生产环境建议
+
+1. **使用强密码 Token**
+   ```bash
+   -e AUTH_TOKEN=$(openssl rand -hex 32)
+   ```
+
+2. **使用 HTTPS**
+   - 在容器前放置反向代理(如 Nginx)
+   - 配置 SSL 证书
+
+3. **定期备份数据库**
+   ```bash
+   # 备份 MySQL 数据库
+   docker exec license-admin-mysql mysqldump -u root -p license_admin > backup_$(date +%Y%m%d).sql
+   ```
+
+4. **资源限制**
+   ```bash
+   docker run -d \
+     --name license-admin \
+     --memory="512m" \
+     --cpus="1.0" \
+     -p 8080:8080 \
+     license-admin:latest
+   ```
+
+5. **使用 Docker Compose 管理**
+   - 便于配置管理
+   - 支持自动重启
+   - 便于扩展
+
+## 🔐 安全建议
+
+1. **不要使用默认 Token**
+   - 生产环境必须修改 `AUTH_TOKEN`
+   - 使用强随机密码
+
+2. **限制网络访问**
+   - 使用防火墙规则
+   - 仅暴露必要端口
+
+3. **定期更新镜像**
+   - 关注安全更新
+   - 定期重建镜像
+
+4. **数据加密**
+   - 考虑对数据库文件进行加密
+   - 使用加密的 volume
+
+## 📊 监控
+
+### 查看资源使用
+
+```bash
+docker stats license-admin
+```
+
+### 查看容器信息
+
+```bash
+docker inspect license-admin
+```
+
+## 🎯 快速开始示例
+
+```bash
+# 1. 构建镜像
+./build.sh
+
+# 2. 创建数据目录
+mkdir -p ./data
+
+# 3. 运行容器
+docker run -d \
+  --name license-admin \
+  -p 8080:8080 \
+  -v $(pwd)/data:/app/data \
+  -e AUTH_TOKEN=my-secret-token-123 \
+  license-admin:latest
+
+# 4. 访问服务
+curl http://localhost:8080/health
+
+# 5. 打开浏览器
+open http://localhost:8080
+```
+
+---
+
+**注意:** 首次运行会自动创建数据库文件。确保挂载的数据目录有写权限。
+

+ 44 - 0
Dockerfile

@@ -0,0 +1,44 @@
+# 简化的 Dockerfile - 仅用于运行已构建的二进制文件
+FROM alpine:latest
+
+# 安装必要的运行时依赖
+RUN apk add --no-cache ca-certificates
+
+# 创建非 root 用户
+RUN addgroup -g 1000 appuser && \
+    adduser -D -u 1000 -G appuser appuser
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制已构建的二进制文件(需要在构建镜像前先编译好)
+COPY license-admin .
+
+# 复制 web 静态文件
+COPY web ./web
+
+# 设置权限
+RUN chown -R appuser:appuser /app
+
+# 切换到非 root 用户
+USER appuser
+
+# 暴露端口
+EXPOSE 8080
+
+# 设置环境变量(MySQL 配置)
+ENV AUTH_TOKEN=admin-token-123456
+ENV PORT=8080
+ENV DB_HOST=mysql
+ENV DB_PORT=3306
+ENV DB_USER=root
+ENV DB_PASSWORD=password
+ENV DB_NAME=license_admin
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
+
+# 启动应用
+CMD ["./license-admin"]
+

+ 42 - 13
README.md

@@ -13,7 +13,7 @@
 - 🎨 **现代化 UI** - 美观易用的 Web 管理界面
 
 ### 技术特性
-- 使用 SQLite 文件数据库,部署简单
+- 使用 MySQL 数据库,支持高并发
 - RESTful API 设计
 - 响应式前端界面
 - 支持 CORS(Chrome 插件调用)
@@ -22,6 +22,7 @@
 
 ### 环境要求
 - Go 1.21 或更高版本
+- MySQL 5.7+ 或 MySQL 8.0+
 - 现代浏览器(Chrome、Firefox、Safari、Edge)
 
 ### 安装步骤
@@ -37,13 +38,26 @@ cd license-admin
 go mod download
 ```
 
-3. **配置 Token(可选)**
+3. **配置数据库**
+```bash
+# 设置 MySQL 连接信息
+export DB_HOST=localhost
+export DB_PORT=3306
+export DB_USER=root
+export DB_PASSWORD=your-password
+export DB_NAME=license_admin
+
+# 初始化数据库(可选,应用会自动创建表)
+mysql -u root -p < database/init.sql
+```
+
+4. **配置 Token(可选)**
 ```bash
 # 设置认证 Token(默认:admin-token-123456)
 export AUTH_TOKEN=your-secret-token
 ```
 
-4. **启动服务**
+5. **启动服务**
 ```bash
 go run main.go
 # 或编译后运行
@@ -51,7 +65,7 @@ go build -o license-admin
 ./license-admin
 ```
 
-5. **访问管理平台**
+6. **访问管理平台**
 ```
 浏览器打开:http://localhost:8080
 ```
@@ -244,13 +258,21 @@ license-admin/
 
 ### 环境变量
 
-- `AUTH_TOKEN` - 认证 Token(默认:`admin-token-123456`)
+| 变量名 | 说明 | 默认值 |
+|--------|------|--------|
+| `AUTH_TOKEN` | 认证 Token | `admin-token-123456` |
+| `DB_HOST` | MySQL 主机地址 | `localhost` |
+| `DB_PORT` | MySQL 端口 | `3306` |
+| `DB_USER` | MySQL 用户名 | `root` |
+| `DB_PASSWORD` | MySQL 密码 | `password` |
+| `DB_NAME` | 数据库名称 | `license_admin` |
 
 ### 数据库
 
-- 使用 SQLite 文件数据库
-- 数据库文件:`license.db`
-- 首次启动自动创建表结构
+- 使用 MySQL 数据库(5.7+ 或 8.0+)
+- 需要手动创建表结构(使用 database/init.sql 或 database/schema.sql)
+- 数据库初始化脚本:`database/init.sql`
+- 表结构脚本:`database/schema.sql`
 
 ### 端口配置
 
@@ -271,8 +293,10 @@ license-admin/
    - 使用环境变量或密钥管理服务
 
 3. **数据库安全**
-   - 定期备份 `license.db` 文件
-   - 设置适当的文件权限
+   - 使用强密码保护 MySQL root 账户
+   - 创建专用数据库用户,仅授予必要权限
+   - 定期备份数据库:`mysqldump -u root -p license_admin > backup.sql`
+   - 启用 MySQL SSL 连接(生产环境)
 
 ## 📝 开发说明
 
@@ -285,7 +309,8 @@ license-admin/
 
 - `github.com/gin-gonic/gin` - Web 框架
 - `gorm.io/gorm` - ORM 框架
-- `gorm.io/driver/sqlite` - SQLite 驱动
+- `gorm.io/driver/mysql` - MySQL 驱动
+- `github.com/go-sql-driver/mysql` - MySQL 驱动底层实现
 
 ### 开发模式
 
@@ -302,8 +327,12 @@ go test ./...
 
 ## 🐛 常见问题
 
-### 1. 数据库文件不存在
-首次运行会自动创建 `license.db` 文件,确保有写入权限。
+### 1. 数据库连接失败
+检查:
+- MySQL 服务是否运行
+- 数据库连接信息是否正确(DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME)
+- 数据库用户是否有创建数据库的权限(首次运行需要)
+- 查看应用日志:`docker logs license-admin` 或应用运行日志
 
 ### 2. 端口被占用
 修改 `main.go` 中的端口号,或停止占用 8080 端口的程序。

+ 109 - 0
build.sh

@@ -0,0 +1,109 @@
+#!/bin/bash
+
+# License Admin Docker 构建脚本
+# 用于构建 Linux x86_64 Docker 镜像
+# 注意:需要先编译好二进制文件
+
+set -e
+
+# 颜色输出
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# 配置
+IMAGE_NAME="license-admin"
+VERSION="${1:-latest}"
+REGISTRY="${2:-}"
+
+echo -e "${GREEN}========================================${NC}"
+echo -e "${GREEN}License Admin Docker 构建脚本${NC}"
+echo -e "${GREEN}========================================${NC}"
+echo ""
+
+# 检查 Docker 是否安装
+if ! command -v docker &> /dev/null; then
+    echo -e "${RED}错误: Docker 未安装,请先安装 Docker${NC}"
+    exit 1
+fi
+
+# 检查二进制文件是否存在
+if [ ! -f "./license-admin" ]; then
+    echo -e "${YELLOW}警告: 未找到二进制文件 license-admin${NC}"
+    echo -e "${YELLOW}正在编译二进制文件...${NC}"
+    
+    # 编译 Linux x86_64 二进制文件
+    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o license-admin .
+    
+    if [ $? -ne 0 ]; then
+        echo -e "${RED}错误: 编译失败${NC}"
+        exit 1
+    fi
+    
+    echo -e "${GREEN}✓ 编译成功${NC}"
+    echo ""
+fi
+
+# 显示构建信息
+echo -e "${YELLOW}构建信息:${NC}"
+echo "  镜像名称: ${IMAGE_NAME}"
+echo "  版本标签: ${VERSION}"
+if [ -n "$REGISTRY" ]; then
+    echo "  注册表: ${REGISTRY}"
+    FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${VERSION}"
+else
+    FULL_IMAGE_NAME="${IMAGE_NAME}:${VERSION}"
+fi
+echo "  完整镜像名: ${FULL_IMAGE_NAME}"
+echo ""
+
+# 构建 Docker 镜像
+echo -e "${YELLOW}开始构建 Docker 镜像...${NC}"
+docker build \
+    --platform linux/amd64 \
+    -t "${FULL_IMAGE_NAME}" \
+    -t "${IMAGE_NAME}:latest" \
+    .
+
+if [ $? -eq 0 ]; then
+    echo ""
+    echo -e "${GREEN}✓ 构建成功!${NC}"
+    echo ""
+    echo -e "${YELLOW}镜像信息:${NC}"
+    docker images "${IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}"
+    echo ""
+    echo -e "${YELLOW}使用方法:${NC}"
+    echo "  运行容器(需要先启动 MySQL):"
+    echo "    docker run -d -p 8080:8080 \\"
+    echo "      -e DB_HOST=mysql \\"
+    echo "      -e DB_PORT=3306 \\"
+    echo "      -e DB_USER=root \\"
+    echo "      -e DB_PASSWORD=your-password \\"
+    echo "      -e DB_NAME=license_admin \\"
+    echo "      --name license-admin ${FULL_IMAGE_NAME}"
+    echo ""
+    echo "  运行容器(连接外部 MySQL):"
+    echo "    docker run -d -p 8080:8080 \\"
+    echo "      -e DB_HOST=host.docker.internal \\"
+    echo "      -e DB_PORT=3306 \\"
+    echo "      -e DB_USER=root \\"
+    echo "      -e DB_PASSWORD=your-password \\"
+    echo "      -e DB_NAME=license_admin \\"
+    echo "      -e AUTH_TOKEN=your-token \\"
+    echo "      --name license-admin ${FULL_IMAGE_NAME}"
+    echo ""
+    echo "  使用 Docker Compose(推荐,自动启动 MySQL):"
+    echo "    docker-compose up -d"
+    echo ""
+    if [ -n "$REGISTRY" ]; then
+        echo -e "${YELLOW}推送到注册表:${NC}"
+        echo "    docker push ${FULL_IMAGE_NAME}"
+        echo ""
+    fi
+else
+    echo ""
+    echo -e "${RED}✗ 构建失败!${NC}"
+    exit 1
+fi
+

+ 122 - 0
database/README.md

@@ -0,0 +1,122 @@
+# 数据库配置说明
+
+## MySQL 数据库配置
+
+本项目使用 MySQL 作为数据库。以下是配置说明。
+
+## 环境变量
+
+| 变量名 | 说明 | 默认值 |
+|--------|------|--------|
+| `DB_HOST` | MySQL 主机地址 | `localhost` |
+| `DB_PORT` | MySQL 端口 | `3306` |
+| `DB_USER` | MySQL 用户名 | `root` |
+| `DB_PASSWORD` | MySQL 密码 | `password` |
+| `DB_NAME` | 数据库名称 | `license_admin` |
+
+## SQL 文件说明
+
+### init.sql
+完整的数据库初始化脚本,包括:
+- 创建数据库(如果不存在)
+- 创建表结构
+- 插入测试数据
+
+**使用方法:**
+```bash
+mysql -u root -p < database/init.sql
+```
+
+### schema.sql
+仅包含表结构定义,不包含数据和数据库创建语句。
+
+**使用方法:**
+```bash
+mysql -u root -p license_admin < database/schema.sql
+```
+
+## 手动创建数据库
+
+### 1. 登录 MySQL
+
+```bash
+mysql -u root -p
+```
+
+### 2. 创建数据库
+
+```sql
+CREATE DATABASE IF NOT EXISTS `license_admin` 
+  DEFAULT CHARACTER SET utf8mb4 
+  DEFAULT COLLATE utf8mb4_unicode_ci;
+```
+
+### 3. 使用数据库
+
+```sql
+USE `license_admin`;
+```
+
+### 4. 导入表结构
+
+```bash
+mysql -u root -p license_admin < database/schema.sql
+```
+
+或者使用 `init.sql`(会自动创建数据库):
+
+```bash
+mysql -u root -p < database/init.sql
+```
+
+## Docker 环境
+
+使用 Docker Compose 时,MySQL 会自动初始化:
+
+```bash
+docker-compose up -d
+```
+
+数据库会自动创建并初始化。
+
+## 表结构
+
+### licenses 表
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| `id` | bigint unsigned | 主键,自增 |
+| `key` | varchar(255) | 激活码,唯一索引 |
+| `bound_devices` | text | 已绑定设备列表(JSON 数组) |
+| `device_activations` | text | 设备激活时间(JSON 对象) |
+| `max_devices` | int | 最大设备数,默认 2 |
+| `created_at` | datetime(3) | 创建时间 |
+| `updated_at` | datetime(3) | 更新时间 |
+
+## 数据备份
+
+### 备份数据库
+
+```bash
+mysqldump -u root -p license_admin > backup_$(date +%Y%m%d).sql
+```
+
+### 恢复数据库
+
+```bash
+mysql -u root -p license_admin < backup_20240113.sql
+```
+
+## 连接测试
+
+```bash
+mysql -h localhost -P 3306 -u root -p license_admin
+```
+
+## 注意事项
+
+1. **字符集**:数据库使用 `utf8mb4` 字符集,支持完整的 UTF-8 字符(包括 emoji)
+2. **时区**:连接字符串中设置了 `parseTime=True&loc=Local`,确保时间正确解析
+3. **连接池**:应用会自动配置连接池,最大空闲连接 10,最大打开连接 100
+4. **表结构管理**:表结构需要手动创建和维护,应用不会自动迁移表结构
+

+ 57 - 7
database/database.go

@@ -1,10 +1,13 @@
 package database
 
 import (
-	"license-admin/models"
+	"fmt"
+	"os"
+	"time"
 
-	"gorm.io/driver/sqlite"
+	"gorm.io/driver/mysql"
 	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
 )
 
 var DB *gorm.DB
@@ -12,15 +15,62 @@ var DB *gorm.DB
 // InitDB 初始化数据库连接
 func InitDB() error {
 	var err error
-	DB, err = gorm.Open(sqlite.Open("license.db"), &gorm.Config{})
+
+	// 从环境变量获取 MySQL 连接信息
+	dbHost := os.Getenv("DB_HOST")
+	if dbHost == "" {
+		dbHost = "localhost"
+	}
+
+	dbPort := os.Getenv("DB_PORT")
+	if dbPort == "" {
+		dbPort = "3306"
+	}
+
+	dbUser := os.Getenv("DB_USER")
+	if dbUser == "" {
+		dbUser = "root"
+	}
+
+	dbPassword := os.Getenv("DB_PASSWORD")
+	if dbPassword == "" {
+		dbPassword = "password"
+	}
+
+	dbName := os.Getenv("DB_NAME")
+	if dbName == "" {
+		dbName = "license_admin"
+	}
+
+	// 构建 DSN (Data Source Name)
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+		dbUser, dbPassword, dbHost, dbPort, dbName)
+
+	// 配置 GORM
+	config := &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Info),
+	}
+
+	// 连接数据库,如果数据库不存在会自动创建(需要用户有 CREATE DATABASE 权限)
+	DB, err = gorm.Open(mysql.Open(dsn), config)
 	if err != nil {
-		return err
+		return fmt.Errorf("连接数据库失败: %v", err)
 	}
 
-	// 自动迁移数据库表
-	err = DB.AutoMigrate(&models.License{})
+	// 获取底层 SQL 连接以设置连接池
+	sqlDB, err := DB.DB()
 	if err != nil {
-		return err
+		return fmt.Errorf("获取数据库连接失败: %v", err)
+	}
+
+	// 设置连接池参数
+	sqlDB.SetMaxIdleConns(10)                  // 设置空闲连接池中连接的最大数量
+	sqlDB.SetMaxOpenConns(100)                 // 设置打开数据库连接的最大数量
+	sqlDB.SetConnMaxLifetime(time.Hour)        // 设置连接可复用的最大时间
+
+	// 测试数据库连接
+	if err = sqlDB.Ping(); err != nil {
+		return fmt.Errorf("数据库连接测试失败: %v", err)
 	}
 
 	return nil

+ 31 - 0
database/init.sql

@@ -0,0 +1,31 @@
+-- License Admin 数据库初始化脚本
+-- MySQL 版本
+
+-- 创建数据库(如果不存在)
+CREATE DATABASE IF NOT EXISTS `license_admin` 
+  DEFAULT CHARACTER SET utf8mb4 
+  DEFAULT COLLATE utf8mb4_unicode_ci;
+
+-- 使用数据库
+USE `license_admin`;
+
+-- 创建 licenses 表
+CREATE TABLE IF NOT EXISTS `licenses` (
+  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `license_key` varchar(255) NOT NULL COMMENT '激活码',
+  `bound_devices` text COMMENT '已绑定设备列表(JSON数组)',
+  `device_activations` text COMMENT '设备激活时间(JSON对象)',
+  `max_devices` int NOT NULL DEFAULT '2' COMMENT '最大设备数',
+  `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
+  `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `idx_licenses_key` (`license_key`),
+  KEY `idx_licenses_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='许可证表';
+
+-- 插入测试数据(可选)
+INSERT INTO `licenses` (`license_key`, `bound_devices`, `device_activations`, `max_devices`, `created_at`, `updated_at`)
+VALUES 
+  ('TEST-KEY-123456', '[]', '{}', 2, NOW(), NOW())
+ON DUPLICATE KEY UPDATE `updated_at` = NOW();
+

+ 23 - 0
database/schema.sql

@@ -0,0 +1,23 @@
+-- License Admin 数据库表结构
+-- MySQL 版本
+-- 此文件仅包含表结构定义,不包含数据
+
+USE `license_admin`;
+
+-- 删除表(如果存在,用于重新创建)
+DROP TABLE IF EXISTS `licenses`;
+
+-- 创建 licenses 表
+CREATE TABLE `licenses` (
+  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `license_key` varchar(255) NOT NULL COMMENT '激活码',
+  `bound_devices` text COMMENT '已绑定设备列表(JSON数组字符串)',
+  `device_activations` text COMMENT '设备激活时间(JSON对象字符串)',
+  `max_devices` int NOT NULL DEFAULT '2' COMMENT '最大设备数',
+  `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
+  `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `idx_licenses_key` (`license_key`),
+  KEY `idx_licenses_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='许可证表';
+

+ 72 - 0
docker-compose.yml

@@ -0,0 +1,72 @@
+version: '3.8'
+
+services:
+  # MySQL 数据库服务
+  mysql:
+    image: mysql:8.0
+    container_name: license-admin-mysql
+    environment:
+      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-password}
+      MYSQL_DATABASE: ${DB_NAME:-license_admin}
+      MYSQL_USER: ${DB_USER:-license_user}
+      MYSQL_PASSWORD: ${DB_PASSWORD:-password}
+    ports:
+      - "${DB_PORT:-3306}:3306"
+    volumes:
+      # 持久化 MySQL 数据
+      - mysql_data:/var/lib/mysql
+      # 初始化 SQL 脚本
+      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
+    restart: unless-stopped
+    command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    networks:
+      - license-admin-network
+
+  # License Admin 应用服务
+  license-admin:
+    build:
+      context: .
+      dockerfile: Dockerfile
+      platforms:
+        - linux/amd64
+    image: license-admin:latest
+    container_name: license-admin
+    ports:
+      - "8080:8080"
+    environment:
+      # 认证 Token,生产环境请修改
+      - AUTH_TOKEN=${AUTH_TOKEN:-admin-token-123456}
+      # 端口配置
+      - PORT=8080
+      # MySQL 数据库配置
+      - DB_HOST=mysql
+      - DB_PORT=3306
+      - DB_USER=${DB_USER:-root}
+      - DB_PASSWORD=${DB_PASSWORD:-password}
+      - DB_NAME=${DB_NAME:-license_admin}
+    depends_on:
+      mysql:
+        condition: service_healthy
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
+      interval: 30s
+      timeout: 3s
+      retries: 3
+      start_period: 10s
+    networks:
+      - license-admin-network
+
+volumes:
+  mysql_data:
+    driver: local
+
+networks:
+  license-admin-network:
+    driver: bridge
+

+ 8 - 5
go.mod

@@ -1,14 +1,17 @@
 module license-admin
 
-go 1.21
+go 1.24.0
+
+toolchain go1.24.7
 
 require (
 	github.com/gin-gonic/gin v1.9.1
-	gorm.io/driver/sqlite v1.5.4
-	gorm.io/gorm v1.25.5
+	gorm.io/driver/mysql v1.6.0
+	gorm.io/gorm v1.31.1
 )
 
 require (
+	filippo.io/edwards25519 v1.1.0 // indirect
 	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
@@ -16,6 +19,7 @@ require (
 	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/go-sql-driver/mysql v1.9.3 // 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
@@ -23,7 +27,6 @@ require (
 	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
@@ -33,7 +36,7 @@ require (
 	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
+	golang.org/x/text v0.33.0 // indirect
 	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 10 - 8
go.sum

@@ -1,3 +1,5 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 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=
@@ -21,6 +23,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 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/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
 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=
@@ -40,8 +44,6 @@ 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=
@@ -77,8 +79,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
 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/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
 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=
@@ -89,8 +91,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 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=
+gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
+gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
+gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
+gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 6 - 6
handlers/license.go

@@ -55,7 +55,7 @@ func CreateLicense(db *gorm.DB) gin.HandlerFunc {
 
 		// 检查 Key 是否已存在
 		var existingLicense models.License
-		result := db.Where("key = ?", req.Key).First(&existingLicense)
+		result := db.Where("license_key = ?", req.Key).First(&existingLicense)
 		if result.Error == nil {
 			// Key 已存在
 			c.JSON(http.StatusBadRequest, LicenseResponse{
@@ -83,7 +83,7 @@ func CreateLicense(db *gorm.DB) gin.HandlerFunc {
 
 		// 创建新的 License
 		license := models.License{
-			Key:          req.Key,
+			LicenseKey:   req.Key,
 			MaxDevices:   req.MaxDevices,
 			BoundDevices: req.BoundDevices,
 		}
@@ -237,7 +237,7 @@ func UpdateLicense(db *gorm.DB) gin.HandlerFunc {
 		if req.Key != "" {
 			// 检查新 Key 是否与其他 License 冲突
 			var existingLicense models.License
-			checkResult := db.Where("key = ? AND id != ?", req.Key, id).First(&existingLicense)
+			checkResult := db.Where("license_key = ? AND id != ?", req.Key, id).First(&existingLicense)
 			if checkResult.Error == nil {
 				c.JSON(http.StatusBadRequest, LicenseResponse{
 					Code: 400,
@@ -252,7 +252,7 @@ func UpdateLicense(db *gorm.DB) gin.HandlerFunc {
 				})
 				return
 			}
-			license.Key = req.Key
+			license.LicenseKey = req.Key
 		}
 
 		if req.MaxDevices != nil {
@@ -465,7 +465,7 @@ func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
 
 			// 检查 Key 是否已存在
 			var existingLicense models.License
-			result := db.Where("key = ?", key).First(&existingLicense)
+			result := db.Where("license_key = ?", key).First(&existingLicense)
 			if result.Error == nil {
 				// Key 已存在,跳过并记录
 				failedKeys = append(failedKeys, key)
@@ -479,7 +479,7 @@ func BatchCreateLicense(db *gorm.DB) gin.HandlerFunc {
 
 			// 创建新的 License
 			license := models.License{
-				Key:          key,
+				LicenseKey:   key,
 				MaxDevices:   req.MaxDevices,
 				BoundDevices: "[]",
 			}

+ 1 - 1
handlers/verify.go

@@ -49,7 +49,7 @@ func VerifyLicense(db *gorm.DB) gin.HandlerFunc {
 
 		// 在数据库中查找该 key
 		var license models.License
-		result := db.Where("key = ?", req.Key).First(&license)
+		result := db.Where("license_key = ?", req.Key).First(&license)
 		if result.Error != nil {
 			// 如果不存在,返回 404 或错误信息
 			if result.Error == gorm.ErrRecordNotFound {

+ 1 - 1
main.go

@@ -23,7 +23,7 @@ func seedData(db *gorm.DB) error {
 	// 如果数据库为空,插入测试数据
 	if count == 0 {
 		testLicense := models.License{
-			Key:          "TEST-KEY-123456",
+			LicenseKey:   "TEST-KEY-123456",
 			BoundDevices: "[]", // 空数组
 			MaxDevices:   2,
 		}

+ 1 - 1
models/license.go

@@ -8,7 +8,7 @@ import (
 // License 许可证模型
 type License struct {
 	ID              uint      `gorm:"primarykey" json:"id"`
-	Key             string    `gorm:"uniqueIndex;not null" json:"key"`
+	LicenseKey      string    `gorm:"column:license_key;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"`

+ 2 - 1
web/index.html

@@ -630,7 +630,8 @@
     </div>
 
     <script>
-        const API_BASE = 'http://localhost:8080/api';
+        // 使用相对路径,自动适配当前域名
+        const API_BASE = '/api';
         let currentPage = 1;
         let pageSize = 10;
         let total = 0;

+ 2 - 1
web/login.html

@@ -172,7 +172,8 @@
     </div>
 
     <script>
-        const API_BASE = 'http://localhost:8080/api';
+        // 使用相对路径,自动适配当前域名
+        const API_BASE = '/api';
 
         // 检查是否已登录
         window.onload = () => {