first commit
commit
421cfb8cfa
cmd
config
data
examples
internal
scripts
web
static
|
@ -0,0 +1,4 @@
|
|||
* text=auto eol=lf
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
*.ps1 text eol=crlf
|
|
@ -0,0 +1,8 @@
|
|||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||
<entry key="Project Default">
|
||||
<profile-state>
|
||||
<expanded-state>
|
||||
<State />
|
||||
</expanded-state>
|
||||
<selected-state>
|
||||
<State>
|
||||
<id>用户定义</id>
|
||||
</State>
|
||||
</selected-state>
|
||||
</profile-state>
|
||||
</entry>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/licserver.iml" filepath="$PROJECT_DIR$/.idea/licserver.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,519 @@
|
|||
swagger: '2.0'
|
||||
info:
|
||||
title: 授权验证管理平台 API
|
||||
version: '1.0.0'
|
||||
description: 授权验证管理平台的所有 API 接口文档
|
||||
host: localhost:8080
|
||||
basePath: /api
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
BearerAuth:
|
||||
type: apiKey
|
||||
name: Authorization
|
||||
in: header
|
||||
description: 'Bearer {token}'
|
||||
|
||||
tags:
|
||||
- name: 用户认证
|
||||
description: 用户登录和认证相关接口
|
||||
- name: 设备管理
|
||||
description: 设备型号和设备管理相关接口
|
||||
- name: 授权管理
|
||||
description: 授权码管理相关接口
|
||||
|
||||
paths:
|
||||
/login:
|
||||
post:
|
||||
tags:
|
||||
- 用户认证
|
||||
summary: 用户登录
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 用户名
|
||||
password:
|
||||
type: string
|
||||
description: 密码
|
||||
captcha:
|
||||
type: string
|
||||
description: 验证码
|
||||
captchaId:
|
||||
type: string
|
||||
description: 验证码ID
|
||||
responses:
|
||||
200:
|
||||
description: 登录成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
|
||||
/captcha:
|
||||
get:
|
||||
tags:
|
||||
- 用户认证
|
||||
summary: 获取验证码
|
||||
responses:
|
||||
200:
|
||||
description: 获取成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
captchaId:
|
||||
type: string
|
||||
imageBase64:
|
||||
type: string
|
||||
|
||||
/devices/models:
|
||||
get:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 获取设备型号列表
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
- name: limit
|
||||
in: query
|
||||
type: integer
|
||||
- name: model_name
|
||||
in: query
|
||||
type: string
|
||||
- name: device_type
|
||||
in: query
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: 获取成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
count:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/DeviceModel'
|
||||
|
||||
post:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 创建设备型号
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/DeviceModel'
|
||||
responses:
|
||||
200:
|
||||
description: 创建成功
|
||||
|
||||
/devices/registered:
|
||||
get:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 获取已注册设备列表
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
- name: limit
|
||||
in: query
|
||||
type: integer
|
||||
- name: uid
|
||||
in: query
|
||||
type: string
|
||||
- name: device_model
|
||||
in: query
|
||||
type: string
|
||||
- name: status
|
||||
in: query
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: 获取成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
count:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Device'
|
||||
|
||||
/devices/{uid}/license:
|
||||
post:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 绑定授权码
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: uid
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
license_code:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: 绑定成功
|
||||
|
||||
delete:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 解绑授权码
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: uid
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: 解绑成功
|
||||
|
||||
/licenses:
|
||||
get:
|
||||
tags:
|
||||
- 授权管理
|
||||
summary: 获取授权码列表
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
- name: limit
|
||||
in: query
|
||||
type: integer
|
||||
- name: status
|
||||
in: query
|
||||
type: string
|
||||
description: 授权码状态(unused/used/expired/revoked)
|
||||
- name: license_type
|
||||
in: query
|
||||
type: string
|
||||
description: 授权类型(time/count/permanent)
|
||||
- name: batch_no
|
||||
in: query
|
||||
type: string
|
||||
description: 批次号
|
||||
responses:
|
||||
200:
|
||||
description: 获取成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
count:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/LicenseCode'
|
||||
|
||||
post:
|
||||
tags:
|
||||
- 授权管理
|
||||
summary: 生成授权码
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
license_type:
|
||||
type: string
|
||||
enum: [time, count, permanent]
|
||||
description: 授权类型
|
||||
duration:
|
||||
type: integer
|
||||
description: 有效期(分钟)
|
||||
max_uses:
|
||||
type: integer
|
||||
description: 最大使用次数
|
||||
count:
|
||||
type: integer
|
||||
description: 生成数量
|
||||
remark:
|
||||
type: string
|
||||
description: 备注说明
|
||||
responses:
|
||||
200:
|
||||
description: 生成成功
|
||||
|
||||
/licenses/{id}/logs:
|
||||
get:
|
||||
tags:
|
||||
- 授权管理
|
||||
summary: 获取授权码操作日志
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
type: integer
|
||||
- name: limit
|
||||
in: query
|
||||
type: integer
|
||||
- name: action
|
||||
in: query
|
||||
type: string
|
||||
description: 操作类型(create/use/verify/revoke)
|
||||
- name: status
|
||||
in: query
|
||||
type: string
|
||||
description: 状态(success/failed)
|
||||
responses:
|
||||
200:
|
||||
description: 获取成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
count:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/LicenseLog'
|
||||
|
||||
/licenses/{code}/revoke:
|
||||
post:
|
||||
tags:
|
||||
- 授权管理
|
||||
summary: 撤销授权码
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: code
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: 撤销成功
|
||||
|
||||
/licenses/batch/revoke:
|
||||
post:
|
||||
tags:
|
||||
- 授权管理
|
||||
summary: 批量撤销授权码
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
codes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: 授权码列表
|
||||
responses:
|
||||
200:
|
||||
description: 批量撤销成功
|
||||
|
||||
/devices/register:
|
||||
post:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 设备注册
|
||||
parameters:
|
||||
- in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- uid
|
||||
- device_model
|
||||
- license_code
|
||||
properties:
|
||||
uid:
|
||||
type: string
|
||||
description: 设备UID
|
||||
device_model:
|
||||
type: string
|
||||
description: 设备型号
|
||||
license_code:
|
||||
type: string
|
||||
description: 授权码
|
||||
responses:
|
||||
200:
|
||||
description: 注册成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
400:
|
||||
description: 注册失败
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
|
||||
definitions:
|
||||
DeviceModel:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
model_name:
|
||||
type: string
|
||||
device_type:
|
||||
type: string
|
||||
company:
|
||||
type: string
|
||||
remark:
|
||||
type: string
|
||||
device_count:
|
||||
type: integer
|
||||
status:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Device:
|
||||
type: object
|
||||
properties:
|
||||
uid:
|
||||
type: string
|
||||
device_model:
|
||||
type: string
|
||||
device_type:
|
||||
type: string
|
||||
company:
|
||||
type: string
|
||||
register_time:
|
||||
type: string
|
||||
format: date-time
|
||||
expire_time:
|
||||
type: string
|
||||
format: date-time
|
||||
license_type:
|
||||
type: string
|
||||
license_code:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
start_count:
|
||||
type: integer
|
||||
last_active_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
LicenseCode:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
license_type:
|
||||
type: string
|
||||
description: 授权类型(time/count/permanent)
|
||||
duration:
|
||||
type: integer
|
||||
description: 有效期(分钟)
|
||||
max_uses:
|
||||
type: integer
|
||||
description: 最大使用次数
|
||||
used_count:
|
||||
type: integer
|
||||
description: 已使用次数
|
||||
status:
|
||||
type: string
|
||||
description: 状态(unused/used/expired/revoked)
|
||||
used_by:
|
||||
type: string
|
||||
description: 使用设备UID
|
||||
used_at:
|
||||
type: string
|
||||
format: date-time
|
||||
created_by:
|
||||
type: integer
|
||||
batch_no:
|
||||
type: string
|
||||
remark:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
LicenseLog:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
license_id:
|
||||
type: integer
|
||||
device_uid:
|
||||
type: string
|
||||
action:
|
||||
type: string
|
||||
description: 操作类型(create/use/verify/revoke)
|
||||
ip:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
description: 状态(success/failed)
|
||||
message:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
|
@ -0,0 +1,112 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"licserver/internal/api"
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/service"
|
||||
"licserver/internal/utils"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 初始化管理员账号
|
||||
func initAdminUser(db *gorm.DB) error {
|
||||
var count int64
|
||||
db.Model(&model.User{}).Where("role = ?", "admin").Count(&count)
|
||||
|
||||
// 如果没有管理员账号,创建默认管理员
|
||||
if count == 0 {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admin := model.User{
|
||||
Username: "admin",
|
||||
Password: string(hashedPassword),
|
||||
Email: "admin@example.com",
|
||||
Role: "admin",
|
||||
}
|
||||
|
||||
if err := db.Create(&admin).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("已创建默认管理员账号:admin/admin")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
config, err := utils.LoadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("无法加载配置: %v", err)
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
db, err := utils.InitDB(&config.Database)
|
||||
if err != nil {
|
||||
log.Fatalf("无法初始化数据库: %v", err)
|
||||
}
|
||||
|
||||
// 初始化管理员账号
|
||||
if err := initAdminUser(db); err != nil {
|
||||
log.Fatalf("初始化管理员账号失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化服务
|
||||
licenseService := service.NewLicenseService(db)
|
||||
userService := service.NewUserService(db, config)
|
||||
deviceService := service.NewDeviceService(db, licenseService)
|
||||
monitorService := service.NewMonitorService(db)
|
||||
uploadService := service.NewUploadService(db, config)
|
||||
siteService := service.NewSiteService(config)
|
||||
tokenService := service.NewTokenService(db)
|
||||
|
||||
// 初始化处理器
|
||||
userHandler := api.NewUserHandler(userService)
|
||||
deviceHandler := api.NewDeviceHandler(deviceService)
|
||||
monitorHandler := api.NewMonitorHandler(monitorService)
|
||||
uploadHandler := api.NewUploadHandler(uploadService)
|
||||
siteHandler := api.NewSiteHandler(siteService)
|
||||
tokenHandler := api.NewTokenHandler(tokenService)
|
||||
licenseHandler := api.NewLicenseHandler(licenseService)
|
||||
|
||||
// 设置路由
|
||||
router := api.SetupRouter(
|
||||
userHandler,
|
||||
deviceHandler,
|
||||
monitorHandler,
|
||||
config,
|
||||
uploadHandler,
|
||||
siteHandler,
|
||||
tokenHandler,
|
||||
licenseHandler,
|
||||
)
|
||||
|
||||
// 创建必要的目录
|
||||
dirs := []string{
|
||||
"data",
|
||||
"uploads",
|
||||
"uploads/site",
|
||||
"static/uploads",
|
||||
"static/uploads/site",
|
||||
"web/static/lib",
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Fatalf("创建目录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
addr := fmt.Sprintf(":%s", config.Server.Port)
|
||||
log.Printf("服务器启动在 %s", addr)
|
||||
if err := router.Run(addr); err != nil {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
server:
|
||||
port: 8081
|
||||
mode: debug # release/debug
|
||||
|
||||
database:
|
||||
type: sqlite3
|
||||
path: ./data/license.db
|
||||
|
||||
jwt:
|
||||
secret: your-secret-key
|
||||
expire: 24h # token过期时间
|
||||
|
||||
email:
|
||||
host: smtp.example.com
|
||||
port: 587
|
||||
username: your-email@example.com
|
||||
password: your-password
|
||||
|
||||
upload:
|
||||
path: ./uploads
|
||||
|
||||
site:
|
||||
title: "授权验证管理平台"
|
||||
description: "专业的软件授权和设备管理平台"
|
||||
base_url: "http://localhost:8080"
|
||||
icp: "京ICP备XXXXXXXX号-1"
|
||||
copyright: "© 2024 Your Company Name. All rights reserved."
|
||||
logo: "/static/images/logo.png"
|
||||
favicon: "/static/images/favicon.ico"
|
|
@ -0,0 +1,7 @@
|
|||
$WshShell = New-Object -comObject WScript.Shell
|
||||
$Shortcut = $WshShell.CreateShortcut("$env:USERPROFILE\Desktop\启动授权验证平台.lnk")
|
||||
$Shortcut.TargetPath = "powershell.exe"
|
||||
$Shortcut.Arguments = "-ExecutionPolicy Bypass -NoProfile -File `"$PWD\start.ps1`""
|
||||
$Shortcut.WorkingDirectory = $PWD
|
||||
$Shortcut.IconLocation = "$PWD\licserver.exe,0"
|
||||
$Shortcut.Save()
|
Binary file not shown.
|
@ -0,0 +1,74 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
ServerURL = "http://localhost:8080"
|
||||
DeviceUID = "test-device-001"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 使用授权码注册设备
|
||||
if err := registerDevice(); err != nil {
|
||||
fmt.Printf("设备注册失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 验证设备状态
|
||||
if err := validateDevice(); err != nil {
|
||||
fmt.Printf("设备验证失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("设备注册和验证成功")
|
||||
}
|
||||
|
||||
func registerDevice() error {
|
||||
data := map[string]interface{}{
|
||||
"uid": DeviceUID,
|
||||
"device_type": "software",
|
||||
"device_model": "test-model",
|
||||
"company": "test-company",
|
||||
"license_code": "your-license-code", // 替换为实际的授权码
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Post(ServerURL+"/api/devices/register",
|
||||
"application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("注册失败: %s", string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDevice() error {
|
||||
resp, err := http.Get(ServerURL + "/api/devices/validate/" + DeviceUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("验证失败: %s", string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
module licserver
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/shirou/gopsutil/v3 v3.24.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // 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.22.1 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // 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.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,172 @@
|
|||
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
|
||||
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
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.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
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.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
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/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
|
||||
github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
@ -0,0 +1,45 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDashboardHandler(db *gorm.DB) *DashboardHandler {
|
||||
return &DashboardHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
var stats struct {
|
||||
TotalDevices int64 `json:"total_devices"`
|
||||
TotalLicenses int64 `json:"total_licenses"`
|
||||
TodayNew int64 `json:"today_new"`
|
||||
OnlineDevices int64 `json:"online_devices"`
|
||||
}
|
||||
|
||||
// 获取设备总数
|
||||
h.db.Model(&model.Device{}).Count(&stats.TotalDevices)
|
||||
|
||||
// 获取授权码总数
|
||||
h.db.Model(&model.LicenseCode{}).Count(&stats.TotalLicenses)
|
||||
|
||||
// 获取今日新增设备数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
h.db.Model(&model.Device{}).Where("DATE(created_at) = ?", today).Count(&stats.TodayNew)
|
||||
|
||||
// 获取在线设备数(最近30分钟内有活动的设备)
|
||||
thirtyMinutesAgo := time.Now().Add(-30 * time.Minute)
|
||||
h.db.Model(&model.Device{}).
|
||||
Where("last_active_at > ?", thirtyMinutesAgo).
|
||||
Count(&stats.OnlineDevices)
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
|
@ -0,0 +1,434 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"strconv"
|
||||
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DeviceHandler struct {
|
||||
deviceService *service.DeviceService
|
||||
}
|
||||
|
||||
func NewDeviceHandler(deviceService *service.DeviceService) *DeviceHandler {
|
||||
|
||||
return &DeviceHandler{deviceService: deviceService}
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) CreateDevice(c *gin.Context) {
|
||||
|
||||
var input service.DeviceCreateInput
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.deviceService.CreateDevice(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设备创建成功"})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) GetDevices(c *gin.Context) {
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
|
||||
params := &service.DeviceQueryParams{
|
||||
|
||||
UID: c.Query("uid"),
|
||||
|
||||
DeviceType: c.Query("deviceType"),
|
||||
|
||||
Company: c.Query("company"),
|
||||
|
||||
LicenseType: c.Query("licenseType"),
|
||||
|
||||
Status: c.Query("status"),
|
||||
|
||||
Page: page,
|
||||
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
devices, total, err := h.deviceService.GetDevices(params)
|
||||
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "获取设备列表成功",
|
||||
"count": total,
|
||||
"data": devices,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UpdateStartCount(c *gin.Context) {
|
||||
|
||||
uid := c.Param("uid")
|
||||
|
||||
if err := h.deviceService.UpdateStartCount(uid); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "启动次数更新成功"})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UpdateDevice(c *gin.Context) {
|
||||
|
||||
uid := c.Param("uid")
|
||||
|
||||
var updates map[string]interface{}
|
||||
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.deviceService.UpdateDevice(uid, updates); err != nil {
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设备更新成功"})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) DeleteDevice(c *gin.Context) {
|
||||
|
||||
uid := c.Param("uid")
|
||||
|
||||
if err := h.deviceService.DeleteDevice(uid); err != nil {
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设备删除成功"})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) RegisterDevice(c *gin.Context) {
|
||||
var input service.DeviceRegisterInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deviceService.RegisterDevice(&input, c.ClientIP()); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
status := "未激活"
|
||||
if input.LicenseCode != "" {
|
||||
status = "已激活"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": fmt.Sprintf("设备注册成功,当前状态:%s", status),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) ValidateDevice(c *gin.Context) {
|
||||
|
||||
uid := c.Param("uid")
|
||||
|
||||
if err := h.deviceService.ValidateDevice(uid); err != nil {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设备验证通过"})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) BindLicense(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
var input struct {
|
||||
LicenseCode string `json:"license_code" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deviceService.BindLicense(uid, input.LicenseCode); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "授权码绑定成功",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UnbindLicense(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
|
||||
if err := h.deviceService.UnbindLicense(uid); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "授权码解绑成功",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) GetLicenseInfo(c *gin.Context) {
|
||||
|
||||
deviceUID := c.Param("uid")
|
||||
|
||||
device, err := h.deviceService.GetLicenseInfo(deviceUID)
|
||||
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, device)
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) CheckLicenseStatus(c *gin.Context) {
|
||||
|
||||
deviceUID := c.Param("uid")
|
||||
|
||||
status, err := h.deviceService.CheckLicenseStatus(deviceUID)
|
||||
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
|
||||
"status": status,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) CheckUpdate(c *gin.Context) {
|
||||
deviceUID := c.Param("uid")
|
||||
currentVersion := c.Query("version")
|
||||
|
||||
update, err := h.deviceService.CheckUpdate(deviceUID, currentVersion)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, update)
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) CreateDeviceModel(c *gin.Context) {
|
||||
var model model.DeviceModel
|
||||
if err := c.ShouldBindJSON(&model); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
model.CreatedBy = c.GetUint("userID")
|
||||
model.Status = "active"
|
||||
|
||||
if err := h.deviceService.CreateDeviceModel(&model); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "设备型号创建成功",
|
||||
"data": model,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) GetDeviceModels(c *gin.Context) {
|
||||
modelName := c.Query("model_name")
|
||||
deviceType := c.Query("device_type")
|
||||
company := c.Query("company")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
models, total, err := h.deviceService.GetDeviceModels(modelName, deviceType, company, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"count": total,
|
||||
"data": models,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UpdateDeviceModel(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var model model.DeviceModel
|
||||
if err := c.ShouldBindJSON(&model); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deviceService.UpdateDeviceModel(uint(id), &model); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "设备型号更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) DeleteDeviceModel(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deviceService.DeleteDeviceModel(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "设备型号删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) BatchDeleteDeviceModels(c *gin.Context) {
|
||||
var input struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deviceService.BatchDeleteDeviceModels(input.IDs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "设备型号批量删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) GetRegisteredDevices(c *gin.Context) {
|
||||
uid := c.Query("uid")
|
||||
deviceModel := c.Query("device_model")
|
||||
status := c.Query("status")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
devices, total, err := h.deviceService.GetRegisteredDevices(uid, deviceModel, status, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"count": total,
|
||||
"data": devices,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) GetDeviceLogs(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
logs, total, err := h.deviceService.GetDeviceLogs(uid, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"count": total,
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
// GetDashboardStats 获取仪表盘统计数据
|
||||
func (h *DeviceHandler) GetDashboardStats(c *gin.Context) {
|
||||
stats, err := h.deviceService.GetDashboardStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": stats,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"licserver/internal/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LicenseHandler struct {
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewLicenseHandler(licenseService *service.LicenseService) *LicenseHandler {
|
||||
return &LicenseHandler{licenseService: licenseService}
|
||||
}
|
||||
|
||||
// 创建授权码
|
||||
func (h *LicenseHandler) CreateLicenses(c *gin.Context) {
|
||||
var input service.LicenseCreateInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
licenses, err := h.licenseService.CreateLicenses(&input, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "授权码创建成功",
|
||||
"data": licenses,
|
||||
})
|
||||
}
|
||||
|
||||
// 使用授权码
|
||||
func (h *LicenseHandler) UseLicense(c *gin.Context) {
|
||||
var input struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
DeviceUID string `json:"device_uid" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
license, err := h.licenseService.UseLicense(input.Code, input.DeviceUID, c.ClientIP())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "授权码使用成功",
|
||||
"data": license,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取授权码列表
|
||||
func (h *LicenseHandler) GetLicenses(c *gin.Context) {
|
||||
status := c.Query("status")
|
||||
licenseType := c.Query("license_type")
|
||||
batchNo := c.Query("batch_no")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
licenses, total, err := h.licenseService.GetLicenses(status, licenseType, batchNo, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回格式符合 layui table 的要求
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"count": total,
|
||||
"data": licenses,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取授权码使用日志
|
||||
func (h *LicenseHandler) GetLicenseLogs(c *gin.Context) {
|
||||
licenseID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的授权码ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否为导出请求
|
||||
if c.Query("export") == "1" {
|
||||
data, err := h.licenseService.ExportLogs(uint(licenseID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
filename := fmt.Sprintf("license_logs_%d_%s.csv", licenseID, time.Now().Format("20060102150405"))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
c.Data(http.StatusOK, "text/csv", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 常规日志查询
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
|
||||
logs, total, err := h.licenseService.GetLicenseLogs(uint(licenseID), page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "获取授权码使用日志成功",
|
||||
"count": total,
|
||||
"data": logs,
|
||||
})
|
||||
}
|
||||
|
||||
// 添加撤销授权码的处理方法
|
||||
func (h *LicenseHandler) RevokeLicense(c *gin.Context) {
|
||||
code := c.Param("code")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
if err := h.licenseService.RevokeLicense(code, userID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "授权码撤销成功",
|
||||
})
|
||||
}
|
||||
|
||||
// 添加批量撤销处理方法
|
||||
func (h *LicenseHandler) RevokeLicenses(c *gin.Context) {
|
||||
var input struct {
|
||||
Codes []string `json:"codes" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
if err := h.licenseService.RevokeLicenses(input.Codes, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "授权码批量撤销成功",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MonitorHandler struct {
|
||||
monitorService *service.MonitorService
|
||||
}
|
||||
|
||||
func NewMonitorHandler(monitorService *service.MonitorService) *MonitorHandler {
|
||||
return &MonitorHandler{monitorService: monitorService}
|
||||
}
|
||||
|
||||
func (h *MonitorHandler) GetSystemStatus(c *gin.Context) {
|
||||
status, err := h.monitorService.GetSystemStatus()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/middleware"
|
||||
"licserver/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRouter(
|
||||
userHandler *UserHandler,
|
||||
deviceHandler *DeviceHandler,
|
||||
monitorHandler *MonitorHandler,
|
||||
config *utils.Config,
|
||||
uploadHandler *UploadHandler,
|
||||
siteHandler *SiteHandler,
|
||||
tokenHandler *TokenHandler,
|
||||
licenseHandler *LicenseHandler,
|
||||
) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 添加错误处理中间件
|
||||
r.Use(middleware.ErrorHandler())
|
||||
|
||||
// 静态文件服务
|
||||
r.Static("/static", "./web/static")
|
||||
|
||||
// 首页和登录页面
|
||||
r.StaticFile("/", "./web/templates/index.html")
|
||||
r.StaticFile("/login", "./web/templates/login.html")
|
||||
|
||||
// Admin页面路由组
|
||||
admin := r.Group("/admin")
|
||||
admin.Use(middleware.JWTAuth(&config.JWT))
|
||||
{
|
||||
// 使用StaticFile处理包含Layui模板的页面
|
||||
admin.StaticFile("/dashboard", "./web/templates/admin/dashboard.html")
|
||||
admin.StaticFile("/devices", "./web/templates/admin/devices.html")
|
||||
admin.StaticFile("/device-files", "./web/templates/admin/device-files.html")
|
||||
admin.StaticFile("/device-license", "./web/templates/admin/device-license.html")
|
||||
admin.StaticFile("/licenses", "./web/templates/admin/licenses.html")
|
||||
admin.StaticFile("/license-logs", "./web/templates/admin/license-logs.html")
|
||||
admin.StaticFile("/tokens", "./web/templates/admin/tokens.html")
|
||||
admin.StaticFile("/token-logs", "./web/templates/admin/token-logs.html")
|
||||
admin.StaticFile("/monitor", "./web/templates/admin/monitor.html")
|
||||
admin.StaticFile("/site-settings", "./web/templates/admin/site-settings.html")
|
||||
admin.StaticFile("/users", "./web/templates/admin/users.html")
|
||||
admin.StaticFile("/user-edit", "./web/templates/admin/user-edit.html")
|
||||
admin.StaticFile("/change-password", "./web/templates/admin/change-password.html")
|
||||
}
|
||||
|
||||
// API路由
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 公开API
|
||||
api.GET("/captcha", userHandler.GetCaptcha)
|
||||
api.POST("/captcha/verify", userHandler.VerifyCaptcha)
|
||||
api.POST("/login", userHandler.Login)
|
||||
api.POST("/register", userHandler.Register)
|
||||
api.POST("/reset-password", userHandler.ResetPassword)
|
||||
api.POST("/reset-password/confirm", userHandler.ResetPasswordWithToken)
|
||||
api.POST("/captcha/register", userHandler.SendRegisterCaptcha)
|
||||
api.POST("/captcha/reset-password", userHandler.SendResetPasswordCaptcha)
|
||||
api.POST("/validate-token", tokenHandler.ValidateToken)
|
||||
|
||||
// 需要认证的API
|
||||
authorized := api.Group("")
|
||||
authorized.Use(middleware.JWTAuth(&config.JWT))
|
||||
{
|
||||
// 设备型号管理
|
||||
authorized.POST("/devices/models", middleware.AdminRequired(), deviceHandler.CreateDeviceModel)
|
||||
authorized.GET("/devices/models", deviceHandler.GetDeviceModels)
|
||||
authorized.PUT("/devices/models/:id", middleware.AdminRequired(), deviceHandler.UpdateDeviceModel)
|
||||
authorized.DELETE("/devices/models/:id", middleware.AdminRequired(), deviceHandler.DeleteDeviceModel)
|
||||
authorized.POST("/devices/models/batch", middleware.AdminRequired(), deviceHandler.BatchDeleteDeviceModels)
|
||||
|
||||
// 设备管理
|
||||
authorized.POST("/devices/register", deviceHandler.RegisterDevice)
|
||||
authorized.GET("/devices/registered", deviceHandler.GetRegisteredDevices)
|
||||
authorized.POST("/devices/:uid/license", middleware.AdminRequired(), deviceHandler.BindLicense)
|
||||
authorized.DELETE("/devices/:uid/license", middleware.AdminRequired(), deviceHandler.UnbindLicense)
|
||||
authorized.GET("/devices/:uid/logs", deviceHandler.GetDeviceLogs)
|
||||
|
||||
// 其他API路由...
|
||||
// 用户管理
|
||||
authorized.GET("/users", middleware.AdminRequired(), userHandler.GetUsers)
|
||||
authorized.POST("/users", middleware.AdminRequired(), userHandler.CreateUser)
|
||||
authorized.GET("/users/:id", middleware.AdminRequired(), userHandler.GetUserInfo)
|
||||
authorized.PUT("/users/:id", middleware.AdminRequired(), userHandler.UpdateUser)
|
||||
authorized.DELETE("/users/:id", middleware.AdminRequired(), userHandler.DeleteUser)
|
||||
authorized.GET("/users/profile", userHandler.GetProfile)
|
||||
authorized.PUT("/users/profile", userHandler.UpdateProfile)
|
||||
authorized.POST("/users/change-password", userHandler.ChangePassword)
|
||||
|
||||
// 系统监控
|
||||
authorized.GET("/monitor/status", middleware.AdminRequired(), monitorHandler.GetSystemStatus)
|
||||
|
||||
// 站点设置
|
||||
authorized.GET("/site/settings", middleware.AdminRequired(), siteHandler.GetSettings)
|
||||
authorized.PUT("/site/settings", middleware.AdminRequired(), siteHandler.UpdateSettings)
|
||||
|
||||
// Token管理
|
||||
authorized.POST("/tokens", middleware.AdminRequired(), tokenHandler.CreateToken)
|
||||
authorized.GET("/tokens", tokenHandler.GetTokens)
|
||||
authorized.GET("/tokens/:id/logs", tokenHandler.GetTokenLogs)
|
||||
authorized.DELETE("/tokens/:token", middleware.AdminRequired(), tokenHandler.RevokeToken)
|
||||
|
||||
// 授权码管理
|
||||
authorized.POST("/licenses", middleware.AdminRequired(), licenseHandler.CreateLicenses)
|
||||
authorized.GET("/licenses", licenseHandler.GetLicenses)
|
||||
authorized.GET("/licenses/:id/logs", licenseHandler.GetLicenseLogs)
|
||||
authorized.POST("/licenses/use", licenseHandler.UseLicense)
|
||||
|
||||
// 仪表盘统计
|
||||
authorized.GET("/dashboard/stats", deviceHandler.GetDashboardStats)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/service"
|
||||
"licserver/internal/utils"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SiteHandler struct {
|
||||
siteService *service.SiteService
|
||||
}
|
||||
|
||||
func NewSiteHandler(siteService *service.SiteService) *SiteHandler {
|
||||
return &SiteHandler{siteService: siteService}
|
||||
}
|
||||
|
||||
func (h *SiteHandler) GetSettings(c *gin.Context) {
|
||||
settings := h.siteService.GetSettings()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": settings,
|
||||
"title": settings.Title,
|
||||
"description": settings.Description,
|
||||
"base_url": settings.BaseURL,
|
||||
"icp": settings.ICP,
|
||||
"copyright": settings.Copyright,
|
||||
"logo": settings.Logo,
|
||||
"favicon": settings.Favicon,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SiteHandler) UpdateSettings(c *gin.Context) {
|
||||
var settings utils.SiteConfig
|
||||
if err := c.ShouldBindJSON(&settings); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.siteService.ValidateSettings(settings); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.siteService.UpdateSettings(settings); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "站点设置更新成功",
|
||||
"data": settings,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SiteHandler) BackupSettings(c *gin.Context) {
|
||||
settings := h.siteService.GetSettings()
|
||||
c.Header("Content-Disposition", "attachment; filename=site_settings.json")
|
||||
c.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
func (h *SiteHandler) RestoreSettings(c *gin.Context) {
|
||||
var settings utils.SiteConfig
|
||||
if err := c.ShouldBindJSON(&settings); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.siteService.ValidateSettings(settings); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.siteService.UpdateSettings(settings); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "站点设置恢复成功",
|
||||
"data": settings,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/service"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type TokenHandler struct {
|
||||
tokenService *service.TokenService
|
||||
}
|
||||
|
||||
func NewTokenHandler(tokenService *service.TokenService) *TokenHandler {
|
||||
return &TokenHandler{tokenService: tokenService}
|
||||
}
|
||||
|
||||
func (h *TokenHandler) CreateToken(c *gin.Context) {
|
||||
var input struct {
|
||||
DeviceUID string `json:"device_uid" binding:"required"`
|
||||
TokenType string `json:"token_type" binding:"required,oneof=api device"`
|
||||
ExpireDays int `json:"expire_days" binding:"required,min=1"`
|
||||
IPList []string `json:"ip_list"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
expireTime := time.Now().AddDate(0, 0, input.ExpireDays)
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
token, err := h.tokenService.CreateToken(
|
||||
input.DeviceUID,
|
||||
input.TokenType,
|
||||
expireTime,
|
||||
input.IPList,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, token)
|
||||
}
|
||||
|
||||
func (h *TokenHandler) ValidateToken(c *gin.Context) {
|
||||
token := c.GetHeader("X-Access-Token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供访问令牌"})
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
accessToken, err := h.tokenService.ValidateToken(token, clientIP)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, accessToken)
|
||||
}
|
||||
|
||||
func (h *TokenHandler) RevokeToken(c *gin.Context) {
|
||||
token := c.Param("token")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
if err := h.tokenService.RevokeToken(token, userID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "令牌已撤销"})
|
||||
}
|
||||
|
||||
func (h *TokenHandler) GetTokens(c *gin.Context) {
|
||||
deviceUID := c.Query("device_uid")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
tokens, total, err := h.tokenService.GetTokens(deviceUID, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "获取令牌列表成功",
|
||||
"count": total,
|
||||
"data": tokens,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TokenHandler) GetTokenLogs(c *gin.Context) {
|
||||
tokenID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的令牌ID"})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
logs, total, err := h.tokenService.GetTokenLogs(uint(tokenID), page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "获取令牌使用日志成功",
|
||||
"count": total,
|
||||
"data": logs,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"licserver/internal/service"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
uploadService *service.UploadService
|
||||
}
|
||||
|
||||
func NewUploadHandler(uploadService *service.UploadService) *UploadHandler {
|
||||
return &UploadHandler{uploadService: uploadService}
|
||||
}
|
||||
|
||||
func (h *UploadHandler) UploadFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到上传文件"})
|
||||
return
|
||||
}
|
||||
|
||||
deviceUID := c.PostForm("device_uid")
|
||||
description := c.PostForm("description")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
upload, err := h.uploadService.UploadFile(file, userID, deviceUID, description)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, upload)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) DownloadFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件ID"})
|
||||
return
|
||||
}
|
||||
|
||||
file, err := h.uploadService.DownloadFile(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(file.FilePath, file.FileName)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) DeleteFile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
if err := h.uploadService.DeleteFile(uint(id), userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "文件删除成功"})
|
||||
}
|
||||
|
||||
func (h *UploadHandler) GetDeviceFiles(c *gin.Context) {
|
||||
deviceUID := c.Param("uid")
|
||||
files, err := h.uploadService.GetDeviceFiles(deviceUID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, files)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) UploadChunk(c *gin.Context) {
|
||||
file, err := c.FormFile("chunk")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到上传文件"})
|
||||
return
|
||||
}
|
||||
|
||||
fileHash := c.PostForm("fileHash")
|
||||
if fileHash == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供文件哈希"})
|
||||
return
|
||||
}
|
||||
|
||||
chunkNumber, err := strconv.Atoi(c.PostForm("chunkNumber"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的分片序号"})
|
||||
return
|
||||
}
|
||||
|
||||
totalChunks, err := strconv.Atoi(c.PostForm("totalChunks"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的总分片数"})
|
||||
return
|
||||
}
|
||||
|
||||
totalSize, err := strconv.ParseInt(c.PostForm("totalSize"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件大小"})
|
||||
return
|
||||
}
|
||||
|
||||
filename := c.PostForm("filename")
|
||||
deviceUID := c.PostForm("deviceUID")
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
err = h.uploadService.UploadChunk(
|
||||
file,
|
||||
fileHash,
|
||||
chunkNumber,
|
||||
totalChunks,
|
||||
totalSize,
|
||||
filename,
|
||||
userID,
|
||||
deviceUID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否所有分片都已上传
|
||||
completed, err := h.uploadService.CheckUploadStatus(fileHash)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "分片上传成功",
|
||||
"completed": completed,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UploadHandler) MergeChunks(c *gin.Context) {
|
||||
fileHash := c.PostForm("fileHash")
|
||||
if fileHash == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未提供文件哈希"})
|
||||
return
|
||||
}
|
||||
|
||||
upload, err := h.uploadService.MergeChunks(fileHash)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "文件合并成功",
|
||||
"file": upload,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UploadHandler) UploadSiteFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到上传文件"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
allowedExts := map[string]bool{
|
||||
".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
|
||||
".ico": true, ".svg": true,
|
||||
}
|
||||
if !allowedExts[ext] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
filename := fmt.Sprintf("site_%s%s", time.Now().Format("20060102150405"), ext)
|
||||
|
||||
// 构建目标目录路径
|
||||
uploadDir := filepath.Join("web", "static", "images")
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建完整的文件路径
|
||||
filePath := filepath.Join(uploadDir, filename)
|
||||
|
||||
// 保存文件
|
||||
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回文件URL(使用正斜杠作为URL路径分隔符)
|
||||
fileURL := "/" + strings.Join([]string{"static", "images", filename}, "/")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": fileURL,
|
||||
"message": "文件上传成功",
|
||||
})
|
||||
}
|
|
@ -0,0 +1,424 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/service"
|
||||
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
|
||||
return &UserHandler{userService: userService}
|
||||
|
||||
}
|
||||
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
|
||||
Password string `json:"password" binding:"required"`
|
||||
|
||||
Captcha string `json:"captcha" binding:"required"`
|
||||
|
||||
CaptchaId string `json:"captchaId" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// 验证验证码
|
||||
if !h.userService.GetCaptchaService().VerifyImageCaptcha(input.CaptchaId, input.Captcha) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.userService.Login(input.Username, input.Password)
|
||||
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// 设置 cookie
|
||||
// c.SetCookie("token", token, 86400, "/", "", false, true) // 24小时过期,httpOnly=true
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
|
||||
}
|
||||
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
|
||||
Captcha string `json:"captcha" binding:"required,len=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.userService.Register(input.Username, input.Password, input.Email, input.Captcha); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "注册成功"})
|
||||
|
||||
}
|
||||
|
||||
func (h *UserHandler) ResetPasswordWithToken(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.userService.ResetPasswordWithToken(input.Token, input.NewPassword); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"})
|
||||
|
||||
}
|
||||
|
||||
func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
|
||||
Captcha string `json:"captcha" binding:"required,len=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.userService.ResetPassword(input.Email, input.Captcha); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "重置密码邮件已发送"})
|
||||
|
||||
}
|
||||
|
||||
func (h *UserHandler) SendRegisterCaptcha(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.userService.SendRegisterCaptcha(input.Email); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "验证码已发送"})
|
||||
|
||||
}
|
||||
|
||||
func (h *UserHandler) SendResetPasswordCaptcha(c *gin.Context) {
|
||||
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err := h.userService.SendResetPasswordCaptcha(input.Email); err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "验证码已发送"})
|
||||
|
||||
}
|
||||
|
||||
// 在 UserHandler 中添加以下方法
|
||||
|
||||
// 获取图片验证码
|
||||
func (h *UserHandler) GetCaptcha(c *gin.Context) {
|
||||
id, b64s, err := h.userService.GetCaptchaService().GenerateImageCaptcha()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "生成验证码失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"captchaId": id,
|
||||
"imageBase64": b64s,
|
||||
})
|
||||
}
|
||||
|
||||
// 验证图片验证码
|
||||
func (h *UserHandler) VerifyCaptcha(c *gin.Context) {
|
||||
var input struct {
|
||||
CaptchaId string `json:"captcha_id" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if !h.userService.GetCaptchaService().VerifyImageCaptcha(input.CaptchaId, input.Code) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "验证成功"})
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
func (h *UserHandler) GetUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
username := c.Query("username")
|
||||
role := c.Query("role")
|
||||
|
||||
users, total, err := h.userService.GetUsers(username, role, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "获取用户列表成功",
|
||||
"count": total,
|
||||
"data": users,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var input struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Role string `json:"role" binding:"required,oneof=admin user"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if c.GetString("role") != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.userService.CreateUser(input.Username, input.Password, input.Email, input.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户创建成功"})
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Role string `json:"role" binding:"required,oneof=admin user"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if c.GetString("role") != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.userService.UpdateUser(uint(id), input.Email, input.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户更新成功"})
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if c.GetString("role") != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
return
|
||||
}
|
||||
|
||||
// 不能删除自己
|
||||
if uint(id) == c.GetUint("userID") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除自己"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.userService.DeleteUser(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "用户删除成功"})
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
func (h *UserHandler) GetUserInfo(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.GetUserByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
user, err := h.userService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
var input struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
err := h.userService.ChangePassword(userID, input.OldPassword, input.NewPassword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
|
||||
}
|
||||
|
||||
// 在 UserHandler 结构体中添加 UpdateProfile 方法
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
var input struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.GetUint("userID")
|
||||
if err := h.userService.UpdateProfile(userID, input.Email); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "个人信息更新成功"})
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"licserver/internal/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func JWTAuth(config *utils.JWTConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var token string
|
||||
|
||||
// 1. 首先从 cookie 中获取 token
|
||||
tokenCookie, err := c.Cookie("token")
|
||||
if err == nil {
|
||||
token = tokenCookie
|
||||
}
|
||||
|
||||
// 2. 如果 cookie 中没有,则从 header 中获取
|
||||
if token == "" {
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth != "" {
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||||
token = parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果 query 参数中有 token,也可以使用
|
||||
if token == "" {
|
||||
token = c.Query("token")
|
||||
}
|
||||
|
||||
// 如果都没有找到 token
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证信息"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 token
|
||||
claims, err := utils.ParseToken(token, config)
|
||||
if err != nil {
|
||||
// 如果 token 无效,清除 cookie
|
||||
// c.SetCookie("token", "", -1, "/", "", true, true)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储到上下文
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
// 如果是从 header 或 query 参数获取的 token,设置到 cookie 中
|
||||
if tokenCookie == "" {
|
||||
// 设置 cookie,过期时间与 token 一致
|
||||
// c.SetCookie("token", token, int(claims.ExpiresAt.Unix()-claims.IssuedAt.Unix()), "/", "", false, true)
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AdminRequired 检查用户是否为管理员
|
||||
func AdminRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists || role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"licserver/internal/utils"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// 只处理第一个错误
|
||||
if len(c.Errors) > 0 {
|
||||
err := c.Errors[0].Err
|
||||
var statusCode int
|
||||
var response utils.ErrorResponse
|
||||
|
||||
switch err {
|
||||
case utils.ErrUnauthorized:
|
||||
statusCode = http.StatusUnauthorized
|
||||
response = utils.ErrorResponse{
|
||||
Code: 401,
|
||||
Message: "未授权的访问",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
case utils.ErrForbidden:
|
||||
statusCode = http.StatusForbidden
|
||||
response = utils.ErrorResponse{
|
||||
Code: 403,
|
||||
Message: "禁止访问",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
case utils.ErrNotFound:
|
||||
statusCode = http.StatusNotFound
|
||||
response = utils.ErrorResponse{
|
||||
Code: 404,
|
||||
Message: "资源不存在",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
case utils.ErrInvalidInput:
|
||||
statusCode = http.StatusBadRequest
|
||||
response = utils.ErrorResponse{
|
||||
Code: 400,
|
||||
Message: "无效的输入",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
response = utils.ErrorResponse{
|
||||
Code: 500,
|
||||
Message: "服务器内部错误",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(statusCode, response)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Captcha struct {
|
||||
gorm.Model
|
||||
Code string `gorm:"size:6"` // 验证码
|
||||
Type string `gorm:"size:20"` // 验证码类型:register/login/reset
|
||||
Target string `gorm:"size:255"` // 目标(邮箱或手机号)
|
||||
ExpiresAt time.Time `gorm:"index"` // 过期时间
|
||||
Used bool `gorm:"default:false"` // 是否已使用
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UploadChunk struct {
|
||||
gorm.Model
|
||||
FileHash string `gorm:"size:64;index"` // 完整文件的哈希值
|
||||
ChunkNumber int `gorm:"index"` // 分片序号
|
||||
ChunkSize int64 `gorm:""` // 分片大小
|
||||
ChunkPath string `gorm:"size:255"` // 分片存储路径
|
||||
TotalChunks int `gorm:""` // 总分片数
|
||||
TotalSize int64 `gorm:""` // 文件总大小
|
||||
Filename string `gorm:"size:255"` // 原始文件名
|
||||
FileType string `gorm:"size:50"` // 文件类型
|
||||
UploadedBy uint `gorm:"index"` // 上传者ID
|
||||
DeviceUID string `gorm:"size:255;index"` // 关联的设备UID
|
||||
Completed bool `gorm:"default:false"` // 是否已完成合并
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeviceLog struct {
|
||||
gorm.Model
|
||||
DeviceUID string `gorm:"index" json:"device_uid"` // 设备UID
|
||||
Action string `json:"action"` // 操作类型
|
||||
Message string `json:"message"` // 详细信息
|
||||
Status string `json:"status"` // 状态:success/failed
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeviceModel struct {
|
||||
gorm.Model
|
||||
ModelName string `gorm:"uniqueIndex" json:"model_name" form:"model_name"` // 设备型号名称
|
||||
DeviceType string `gorm:"size:50" json:"device_type" form:"device_type"` // 设备类型
|
||||
Company string `gorm:"size:255" json:"company" form:"company"` // 所属公司
|
||||
Remark string `gorm:"size:500" json:"remark" form:"remark"` // 备注说明
|
||||
DeviceCount int `gorm:"-" json:"device_count"` // 设备数量(非数据库字段)
|
||||
CreatedBy uint `gorm:"index" json:"created_by"` // 创建者ID
|
||||
CurrentVersion string `gorm:"size:50" json:"current_version"` // 当前版本
|
||||
UpdateURL string `gorm:"size:255" json:"update_url"` // 更新地址
|
||||
UpdateDesc string `gorm:"size:500" json:"update_desc"` // 更新说明
|
||||
Status string `gorm:"size:20;default:active" json:"status"` // 状态:active/disabled
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LicenseCode struct {
|
||||
gorm.Model
|
||||
Code string `gorm:"uniqueIndex" json:"code"` // 授权码
|
||||
LicenseType string `gorm:"size:20" json:"license_type"` // 授权类型:time/count/permanent
|
||||
Duration int `json:"duration"` // 授权时长(分钟),仅当类型为time时有效
|
||||
MaxUses int `json:"max_uses"` // 最大使用次数,仅当类型为count时有效
|
||||
UsedCount int `gorm:"default:0" json:"used_count"` // 已使用次数
|
||||
Status string `gorm:"size:20" json:"status"` // 状态:unused/used/expired/revoked
|
||||
UsedBy string `gorm:"index" json:"used_by"` // 使用此授权码的设备UID
|
||||
UsedAt time.Time `json:"used_at"` // 使用时间
|
||||
CreatedBy uint `gorm:"index" json:"created_by"` // 创建者ID
|
||||
BatchNo string `gorm:"size:50;index" json:"batch_no"` // 批次号
|
||||
Remark string `gorm:"size:500" json:"remark"` // 备注
|
||||
BindCount int `gorm:"default:-1" json:"bind_count"` // 可绑定次数,-1表示无限制,0表示不能绑定
|
||||
}
|
||||
|
||||
type LicenseLog struct {
|
||||
gorm.Model
|
||||
LicenseID uint `gorm:"index" json:"license_id"` // 关联的授权码ID
|
||||
DeviceUID string `gorm:"index" json:"device_uid"` // 设备UID
|
||||
Action string `gorm:"size:20" json:"action"` // 操作类型:create/use/verify
|
||||
IP string `gorm:"size:50" json:"ip"` // 操作IP
|
||||
Status string `gorm:"size:20" json:"status"` // 状态:success/failed
|
||||
Message string `gorm:"size:500" json:"message"` // 详细信息
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
gorm.Model
|
||||
UID string `gorm:"uniqueIndex" json:"uid"`
|
||||
IPPort string `json:"ip_port"`
|
||||
ChipID string `json:"chip_id"`
|
||||
DeviceType string `json:"device_type"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
Company string `json:"company"`
|
||||
RegisterTime time.Time `json:"register_time"`
|
||||
ExpireTime time.Time `json:"expire_time"`
|
||||
LicenseType string `json:"license_type"`
|
||||
StartCount int `json:"start_count"`
|
||||
Status string `json:"status"`
|
||||
LicenseCode string `json:"license_code"`
|
||||
MaxUses int `json:"max_uses"`
|
||||
Duration int `json:"duration"`
|
||||
LastActiveAt time.Time `json:"last_active_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string `gorm:"uniqueIndex" json:"username"`
|
||||
Password string `json:"-"`
|
||||
Email string `gorm:"uniqueIndex" json:"email"`
|
||||
Role string `json:"role"`
|
||||
LastLogin time.Time `json:"last_login"`
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type SystemStatus struct {
|
||||
CPU struct {
|
||||
Usage float64 `json:"usage"` // CPU使用率
|
||||
|
||||
LoadAvg []float64 `json:"load_avg"` // 系统负载
|
||||
|
||||
CoreCount int `json:"core_count"` // CPU核心数
|
||||
|
||||
ModelName string `json:"model_name"` // CPU型号
|
||||
|
||||
MHz float64 `json:"mhz"` // CPU频率
|
||||
|
||||
} `json:"cpu"`
|
||||
|
||||
Memory struct {
|
||||
Total uint64 `json:"total"` // 总内存
|
||||
|
||||
Used uint64 `json:"used"` // 已用内存
|
||||
|
||||
Free uint64 `json:"free"` // 空闲内存
|
||||
|
||||
UsageRate float64 `json:"usage_rate"` // 使用率
|
||||
|
||||
SwapTotal uint64 `json:"swap_total"` // 交换分区总大小
|
||||
|
||||
SwapUsed uint64 `json:"swap_used"` // 交换分区已用
|
||||
|
||||
SwapFree uint64 `json:"swap_free"` // 交换分区空闲
|
||||
|
||||
SwapUsageRate float64 `json:"swap_usage_rate"` // 交换分区使用率
|
||||
|
||||
} `json:"memory"`
|
||||
|
||||
Disk struct {
|
||||
Partitions []DiskPartition `json:"partitions"` // 磁盘分区信息
|
||||
|
||||
} `json:"disk"`
|
||||
|
||||
Network struct {
|
||||
Interfaces []NetworkInterface `json:"interfaces"` // 网络接口信息
|
||||
|
||||
} `json:"network"`
|
||||
|
||||
Process struct {
|
||||
Total int `json:"total"` // 进程总数
|
||||
|
||||
List []ProcessInfo `json:"list"` // 进程列表(Top N)
|
||||
|
||||
} `json:"process"`
|
||||
|
||||
Host struct {
|
||||
Hostname string `json:"hostname"` // 主机名
|
||||
|
||||
OS string `json:"os"` // 操作系统
|
||||
|
||||
Platform string `json:"platform"` // 平台
|
||||
|
||||
PlatformVersion string `json:"platform_version"` // 平台版本
|
||||
|
||||
KernelVersion string `json:"kernel_version"` // 内核版本
|
||||
|
||||
BootTime time.Time `json:"boot_time"` // 启动时间
|
||||
|
||||
} `json:"host"`
|
||||
|
||||
System struct {
|
||||
Uptime time.Duration `json:"uptime"` // 系统运行时间
|
||||
|
||||
CurrentTime time.Time `json:"current_time"` // 当前时间
|
||||
|
||||
ActiveUsers int `json:"active_users"` // 活跃用户数
|
||||
|
||||
TotalDevices int `json:"total_devices"` // 设备总数
|
||||
|
||||
} `json:"system"`
|
||||
}
|
||||
|
||||
type DiskPartition struct {
|
||||
Device string `json:"device"` // 设备名
|
||||
|
||||
Mountpoint string `json:"mountpoint"` // 挂载点
|
||||
|
||||
Fstype string `json:"fstype"` // 文件系统类型
|
||||
|
||||
Total uint64 `json:"total"` // 总空间
|
||||
|
||||
Used uint64 `json:"used"` // 已用空间
|
||||
|
||||
Free uint64 `json:"free"` // 空闲空间
|
||||
|
||||
UsageRate float64 `json:"usage_rate"` // 使用率
|
||||
|
||||
}
|
||||
|
||||
type NetworkInterface struct {
|
||||
Name string `json:"name"` // 接口名称
|
||||
|
||||
BytesSent uint64 `json:"bytes_sent"` // 发送字节数
|
||||
|
||||
BytesRecv uint64 `json:"bytes_recv"` // 接收字节数
|
||||
|
||||
PacketsSent uint64 `json:"packets_sent"` // 发送包数
|
||||
|
||||
PacketsRecv uint64 `json:"packets_recv"` // 接收包数
|
||||
|
||||
Addrs []string `json:"addrs"` // IP地址列表,改为字符串数组
|
||||
|
||||
}
|
||||
|
||||
type ProcessInfo struct {
|
||||
PID int `json:"pid"` // 进程ID
|
||||
|
||||
Name string `json:"name"` // 进程名称
|
||||
|
||||
CPU float64 `json:"cpu"` // CPU使用率
|
||||
|
||||
Memory float64 `json:"memory"` // 内存使用率
|
||||
|
||||
Created int64 `json:"created"` // 创建时间
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PasswordResetToken struct {
|
||||
gorm.Model
|
||||
UserID uint `gorm:"index"`
|
||||
Token string `gorm:"uniqueIndex"`
|
||||
ExpiresAt time.Time
|
||||
Used bool
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccessToken struct {
|
||||
gorm.Model
|
||||
Token string `gorm:"uniqueIndex" json:"token"` // 访问令牌
|
||||
DeviceUID string `gorm:"index" json:"device_uid"` // 关联的设备UID
|
||||
Type string `gorm:"size:20" json:"type"` // 令牌类型:api/device
|
||||
Status string `gorm:"size:20" json:"status"` // 状态:active/revoked
|
||||
ExpireTime time.Time `json:"expire_time"` // 过期时间
|
||||
LastUsed time.Time `json:"last_used"` // 最后使用时间
|
||||
UsageCount int `gorm:"default:0" json:"usage_count"` // 使用次数
|
||||
IPList string `gorm:"type:text" json:"ip_list"` // 允许的IP列表,逗号分隔
|
||||
CreatedBy uint `gorm:"index" json:"created_by"` // 创建者ID
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (AccessToken) TableName() string {
|
||||
return "access_tokens"
|
||||
}
|
||||
|
||||
type TokenLog struct {
|
||||
gorm.Model
|
||||
TokenID uint `gorm:"index" json:"token_id"` // 关联的令牌ID
|
||||
Action string `gorm:"size:20" json:"action"` // 操作类型:create/use/revoke
|
||||
IP string `gorm:"size:50" json:"ip"` // 操作IP
|
||||
UserAgent string `gorm:"size:255" json:"user_agent"` // User-Agent
|
||||
Status string `gorm:"size:20" json:"status"` // 状态:success/failed
|
||||
Message string `gorm:"size:500" json:"message"` // 详细信息
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (TokenLog) TableName() string {
|
||||
return "token_logs"
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileUpload struct {
|
||||
gorm.Model
|
||||
FileName string `gorm:"size:255" json:"file_name"` // 文件名
|
||||
FilePath string `gorm:"size:255" json:"file_path"` // 文件路径
|
||||
FileSize int64 `json:"file_size"` // 文件大小
|
||||
FileType string `gorm:"size:50" json:"file_type"` // 文件类型
|
||||
UploadedBy uint `gorm:"index" json:"uploaded_by"` // 上传者ID
|
||||
DeviceModel string `gorm:"size:255;index" json:"device_model"` // 设备型号
|
||||
Version string `gorm:"size:50" json:"version"` // 文件版本
|
||||
Description string `gorm:"size:500" json:"description"` // 文件描述
|
||||
IsUpdate bool `gorm:"default:false" json:"is_update"` // 是否为更新文件
|
||||
Downloads int `gorm:"default:0" json:"downloads"` // 下载次数
|
||||
LastDownload time.Time `json:"last_download"` // 最后下载时间
|
||||
MD5 string `gorm:"size:32" json:"md5"` // 文件MD5值
|
||||
ForceUpdate bool `gorm:"default:false" json:"force_update"` // 是否强制更新
|
||||
DeviceUID string `gorm:"size:255;index" json:"device_uid"` // 关联的设备UID
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mojocn/base64Captcha"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CaptchaService struct {
|
||||
db *gorm.DB
|
||||
emailConfig *utils.EmailConfig
|
||||
store base64Captcha.Store
|
||||
}
|
||||
|
||||
func NewCaptchaService(db *gorm.DB, emailConfig *utils.EmailConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
db: db,
|
||||
emailConfig: emailConfig,
|
||||
store: base64Captcha.DefaultMemStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CaptchaService) SendEmailCaptcha(email, captchaType string) error {
|
||||
// 检查是否存在未过期的验证码
|
||||
var count int64
|
||||
s.db.Model(&model.Captcha{}).
|
||||
Where("target = ? AND type = ? AND expires_at > ? AND used = ?",
|
||||
email, captchaType, time.Now(), false).
|
||||
Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("请勿频繁发送验证码")
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
code, err := utils.GenerateCaptcha()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存验证码
|
||||
captcha := model.Captcha{
|
||||
Code: code,
|
||||
Type: captchaType,
|
||||
Target: email,
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
Used: false,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&captcha).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送验证码邮件
|
||||
emailService := utils.NewEmailService(s.emailConfig)
|
||||
content := utils.GenerateEmailCaptchaContent(code, email, captchaType)
|
||||
return emailService.SendEmail(email, "验证码", content)
|
||||
}
|
||||
|
||||
func (s *CaptchaService) VerifyCaptcha(target, captchaType, code string) error {
|
||||
var captcha model.Captcha
|
||||
err := s.db.Where("target = ? AND type = ? AND code = ? AND used = ? AND expires_at > ?",
|
||||
target, captchaType, code, false, time.Now()).
|
||||
First(&captcha).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("验证码无效或已过期")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
return s.db.Model(&captcha).Update("used", true).Error
|
||||
}
|
||||
|
||||
// 生成图片验证码
|
||||
func (s *CaptchaService) GenerateImageCaptcha() (string, string, error) {
|
||||
driver := base64Captcha.NewDriverDigit(80, 240, 6, 0.7, 80)
|
||||
c := base64Captcha.NewCaptcha(driver, s.store)
|
||||
id, b64s, err := c.Generate()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 确保返回的base64字符串不包含前缀
|
||||
b64s = strings.TrimPrefix(b64s, "data:image/png;base64,")
|
||||
|
||||
return id, b64s, nil
|
||||
}
|
||||
|
||||
// 验证图片验证码
|
||||
func (s *CaptchaService) VerifyImageCaptcha(id, code string) bool {
|
||||
return s.store.Verify(id, code, true)
|
||||
}
|
|
@ -0,0 +1,721 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"licserver/internal/model"
|
||||
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeviceService struct {
|
||||
db *gorm.DB
|
||||
|
||||
licenseService *LicenseService
|
||||
}
|
||||
|
||||
func NewDeviceService(db *gorm.DB, licenseService *LicenseService) *DeviceService {
|
||||
|
||||
return &DeviceService{
|
||||
|
||||
db: db,
|
||||
|
||||
licenseService: licenseService,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type DeviceRegisterInput struct {
|
||||
UID string `json:"uid" binding:"required"`
|
||||
DeviceModel string `json:"device_model" binding:"required"`
|
||||
LicenseCode string `json:"license_code"`
|
||||
}
|
||||
|
||||
func (s *DeviceService) RegisterDevice(input *DeviceRegisterInput, ip string) error {
|
||||
// 检查设备型号是否存在且处于启用状态
|
||||
var deviceModel model.DeviceModel
|
||||
if err := s.db.Where("model_name = ? AND status = ?", input.DeviceModel, "active").First(&deviceModel).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("设备型号不存在或已禁用")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查设备是否已注册
|
||||
var count int64
|
||||
s.db.Model(&model.Device{}).Where("uid = ?", input.UID).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("设备已注册")
|
||||
}
|
||||
|
||||
// 创建设备记录
|
||||
device := &model.Device{
|
||||
UID: input.UID,
|
||||
DeviceType: deviceModel.DeviceType,
|
||||
DeviceModel: input.DeviceModel,
|
||||
Company: deviceModel.Company,
|
||||
RegisterTime: time.Now(),
|
||||
Status: "inactive",
|
||||
LastActiveAt: time.Now(),
|
||||
StartCount: 0,
|
||||
}
|
||||
|
||||
// 如果提供了授权码,进行授权绑定
|
||||
if input.LicenseCode != "" {
|
||||
license, err := s.licenseService.GetLicenseByCode(input.LicenseCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if license.Status != "unused" {
|
||||
return errors.New("授权码已被使用")
|
||||
}
|
||||
|
||||
device.Status = "active"
|
||||
device.LicenseCode = license.Code
|
||||
device.LicenseType = license.LicenseType
|
||||
device.MaxUses = license.MaxUses
|
||||
device.Duration = license.Duration
|
||||
|
||||
if license.LicenseType == "time" {
|
||||
device.ExpireTime = time.Now().Add(time.Duration(license.Duration) * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 创建设备记录
|
||||
if err := tx.Create(device).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果有授权码,更新授权码状态
|
||||
if device.LicenseCode != "" {
|
||||
if err := tx.Model(&model.LicenseCode{}).Where("code = ?", device.LicenseCode).
|
||||
Updates(map[string]interface{}{
|
||||
"status": "used",
|
||||
"used_by": input.UID,
|
||||
"used_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 记录设备日志
|
||||
logMsg := "设备注册成功"
|
||||
if device.LicenseCode != "" {
|
||||
logMsg += fmt.Sprintf(",使用授权码: %s", device.LicenseCode)
|
||||
}
|
||||
log := model.DeviceLog{
|
||||
DeviceUID: input.UID,
|
||||
Action: "register",
|
||||
Message: logMsg,
|
||||
Status: "success",
|
||||
}
|
||||
if err := tx.Create(&log).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeviceService) ValidateDevice(uid string) error {
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", uid).First(&device).Error; err != nil {
|
||||
return errors.New("设备未注册")
|
||||
}
|
||||
|
||||
// 更新最后活跃时间
|
||||
device.LastActiveAt = time.Now()
|
||||
|
||||
// 如果设备已激活,检查授权状态
|
||||
if device.Status == "active" {
|
||||
if device.LicenseCode != "" {
|
||||
if err := s.licenseService.CheckLicenseValidity(device.LicenseCode); err != nil {
|
||||
device.Status = "expired"
|
||||
s.db.Save(&device)
|
||||
return errors.New("设备授权已过期")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.db.Save(&device).Error
|
||||
}
|
||||
|
||||
func (s *DeviceService) GetDevices(params *DeviceQueryParams) ([]model.Device, int64, error) {
|
||||
|
||||
var devices []model.Device
|
||||
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.Device{})
|
||||
|
||||
if params.UID != "" {
|
||||
|
||||
query = query.Where("uid LIKE ?", "%"+params.UID+"%")
|
||||
|
||||
}
|
||||
|
||||
if params.DeviceType != "" {
|
||||
|
||||
query = query.Where("device_type = ?", params.DeviceType)
|
||||
|
||||
}
|
||||
|
||||
if params.Company != "" {
|
||||
|
||||
query = query.Where("company LIKE ?", "%"+params.Company+"%")
|
||||
|
||||
}
|
||||
|
||||
if params.LicenseType != "" {
|
||||
|
||||
query = query.Where("license_type = ?", params.LicenseType)
|
||||
|
||||
}
|
||||
|
||||
if params.Status != "" {
|
||||
|
||||
query = query.Where("status = ?", params.Status)
|
||||
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
if params.Page > 0 && params.PageSize > 0 {
|
||||
|
||||
offset := (params.Page - 1) * params.PageSize
|
||||
|
||||
query = query.Offset(offset).Limit(params.PageSize)
|
||||
|
||||
}
|
||||
|
||||
err := query.Find(&devices).Error
|
||||
|
||||
return devices, total, err
|
||||
|
||||
}
|
||||
|
||||
func (s *DeviceService) UpdateStartCount(uid string) error {
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", uid).First(&device).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新启动次数和最后活跃时间
|
||||
device.StartCount++
|
||||
device.LastActiveAt = time.Now()
|
||||
|
||||
// 如果设备已激活,检查授权状态
|
||||
if device.Status == "active" {
|
||||
// 检查授权码有效性
|
||||
if device.LicenseCode != "" {
|
||||
if err := s.licenseService.CheckLicenseValidity(device.LicenseCode); err != nil {
|
||||
device.Status = "expired"
|
||||
}
|
||||
}
|
||||
// 检查次数限制
|
||||
if device.LicenseType == "count" && device.StartCount >= device.MaxUses {
|
||||
device.Status = "expired"
|
||||
}
|
||||
}
|
||||
|
||||
// 记录设备日志
|
||||
log := model.DeviceLog{
|
||||
DeviceUID: uid,
|
||||
Action: "start",
|
||||
Message: fmt.Sprintf("设备启动,当前次数:%d", device.StartCount),
|
||||
Status: "success",
|
||||
}
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(&device).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&log).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DeviceService) UpdateDevice(uid string, updates map[string]interface{}) error {
|
||||
|
||||
return s.db.Model(&model.Device{}).Where("uid = ?", uid).Updates(updates).Error
|
||||
|
||||
}
|
||||
|
||||
func (s *DeviceService) DeleteDevice(uid string) error {
|
||||
|
||||
return s.db.Where("uid = ?", uid).Delete(&model.Device{}).Error
|
||||
|
||||
}
|
||||
|
||||
func (s *DeviceService) GetLicenseInfo(deviceUID string) (*model.Device, error) {
|
||||
|
||||
var device model.Device
|
||||
|
||||
if err := s.db.Where("uid = ?", deviceUID).First(&device).Error; err != nil {
|
||||
|
||||
return nil, errors.New("设备不存在")
|
||||
|
||||
}
|
||||
|
||||
return &device, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *DeviceService) CheckLicenseStatus(deviceUID string) (string, error) {
|
||||
|
||||
var device model.Device
|
||||
|
||||
if err := s.db.Where("uid = ?", deviceUID).First(&device).Error; err != nil {
|
||||
|
||||
return "", errors.New("设备不存在")
|
||||
|
||||
}
|
||||
|
||||
if device.LicenseCode == "" {
|
||||
|
||||
return "未授权", nil
|
||||
|
||||
}
|
||||
|
||||
if device.Status != "active" {
|
||||
|
||||
return device.Status, nil
|
||||
|
||||
}
|
||||
|
||||
switch device.LicenseType {
|
||||
|
||||
case "时间段":
|
||||
|
||||
if time.Now().After(device.ExpireTime) {
|
||||
|
||||
device.Status = "expired"
|
||||
|
||||
s.db.Save(&device)
|
||||
|
||||
return "已过期", nil
|
||||
|
||||
}
|
||||
|
||||
case "启动次数":
|
||||
|
||||
if device.StartCount >= device.MaxUses {
|
||||
|
||||
device.Status = "expired"
|
||||
|
||||
s.db.Save(&device)
|
||||
|
||||
return "已达到使用上限", nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return "正常", nil
|
||||
|
||||
}
|
||||
|
||||
type DeviceQueryParams struct {
|
||||
UID string
|
||||
DeviceType string
|
||||
Company string
|
||||
LicenseType string
|
||||
Status string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type DeviceCreateInput struct {
|
||||
UID string `json:"uid" binding:"required"`
|
||||
DeviceType string `json:"device_type" binding:"required"`
|
||||
DeviceModel string `json:"device_model" binding:"required"`
|
||||
Company string `json:"company"`
|
||||
}
|
||||
|
||||
func (s *DeviceService) CreateDevice(input *DeviceCreateInput) error {
|
||||
// 检查设备UID是否已存在
|
||||
var count int64
|
||||
if err := s.db.Model(&model.Device{}).Where("uid = ?", input.UID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("设备UID已存在")
|
||||
}
|
||||
|
||||
// 创建设备记录
|
||||
device := &model.Device{
|
||||
UID: input.UID,
|
||||
DeviceType: input.DeviceType,
|
||||
DeviceModel: input.DeviceModel,
|
||||
Company: input.Company,
|
||||
RegisterTime: time.Now(),
|
||||
Status: "inactive", // 初始状态为未激活
|
||||
}
|
||||
|
||||
return s.db.Create(device).Error
|
||||
}
|
||||
|
||||
// 添加更新检查方法
|
||||
func (s *DeviceService) CheckUpdate(deviceUID, currentVersion string) (*model.FileUpload, error) {
|
||||
// 获取设备信息
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", deviceUID).First(&device).Error; err != nil {
|
||||
return nil, errors.New("设备不存在")
|
||||
}
|
||||
|
||||
// 检查设备状态
|
||||
if device.Status != "active" {
|
||||
return nil, errors.New("设备未激活或已过期")
|
||||
}
|
||||
|
||||
// 查找最新的更新文件
|
||||
var update model.FileUpload
|
||||
err := s.db.Where("device_model = ? AND is_update = ?", device.DeviceModel, true).
|
||||
Order("created_at DESC").
|
||||
First(&update).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // 没有可用更新
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 比较版本
|
||||
if update.Version <= currentVersion && !update.ForceUpdate {
|
||||
return nil, nil // 当前版本已是最新
|
||||
}
|
||||
|
||||
return &update, nil
|
||||
}
|
||||
|
||||
// 添加设备型号相关的方法
|
||||
|
||||
// CreateDeviceModel 创建设备型号
|
||||
func (s *DeviceService) CreateDeviceModel(model_ *model.DeviceModel) error {
|
||||
// 检查型号名称是否已存在
|
||||
var count int64
|
||||
s.db.Model(&model.DeviceModel{}).Where("model_name = ?", model_.ModelName).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("设备型号已存在")
|
||||
}
|
||||
|
||||
return s.db.Create(model_).Error
|
||||
}
|
||||
|
||||
// UpdateDeviceModel 更新设备型号
|
||||
func (s *DeviceService) UpdateDeviceModel(id uint, model_ *model.DeviceModel) error {
|
||||
// 检查型号名称是否被其他型号使用
|
||||
var count int64
|
||||
s.db.Model(&model.DeviceModel{}).Where("model_name = ? AND id != ?", model_.ModelName, id).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("设备型号已存在")
|
||||
}
|
||||
|
||||
return s.db.Model(&model.DeviceModel{}).Where("id = ?", id).Updates(model_).Error
|
||||
}
|
||||
|
||||
// DeleteDeviceModel 删除设备型号
|
||||
func (s *DeviceService) DeleteDeviceModel(id uint) error {
|
||||
// 检查是否有设备使用此型号
|
||||
var count int64
|
||||
s.db.Model(&model.DeviceModel{}).Where("device_model = ?", id).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("该型号下存在设备,无法删除")
|
||||
}
|
||||
|
||||
return s.db.Delete(&model.DeviceModel{}, id).Error
|
||||
}
|
||||
|
||||
// GetDeviceModels 获取设备型号列表
|
||||
func (s *DeviceService) GetDeviceModels(modelName, deviceType, company string, page, pageSize int) ([]model.DeviceModel, int64, error) {
|
||||
var models []model.DeviceModel
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.DeviceModel{})
|
||||
|
||||
if modelName != "" {
|
||||
query = query.Where("model_name LIKE ?", "%"+modelName+"%")
|
||||
}
|
||||
if deviceType != "" {
|
||||
query = query.Where("device_type = ?", deviceType)
|
||||
}
|
||||
if company != "" {
|
||||
query = query.Where("company LIKE ?", "%"+company+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
// 查询设备型号列表
|
||||
if err := query.Find(&models).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 查询每个型号下的设备数量
|
||||
for i := range models {
|
||||
var count int64
|
||||
s.db.Model(&model.Device{}).Where("device_model = ?", models[i].ModelName).Count(&count)
|
||||
models[i].DeviceCount = int(count)
|
||||
}
|
||||
|
||||
return models, total, nil
|
||||
}
|
||||
|
||||
// BatchDeleteDeviceModels 批量删除设备型号
|
||||
func (s *DeviceService) BatchDeleteDeviceModels(ids []uint) error {
|
||||
// 检查是否有设备使用这些型号
|
||||
var count int64
|
||||
s.db.Model(&model.Device{}).Where("device_model IN (?)", ids).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("选中的型号中存在正在使用的型号,无法删除")
|
||||
}
|
||||
|
||||
return s.db.Delete(&model.DeviceModel{}, ids).Error
|
||||
}
|
||||
|
||||
// GetRegisteredDevices 获取已注册设备列表
|
||||
func (s *DeviceService) GetRegisteredDevices(uid, deviceModel, status string, page, pageSize int) ([]model.Device, int64, error) {
|
||||
var devices []model.Device
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.Device{})
|
||||
|
||||
if uid != "" {
|
||||
query = query.Where("uid LIKE ?", "%"+uid+"%")
|
||||
}
|
||||
if deviceModel != "" {
|
||||
query = query.Where("device_model = ?", deviceModel)
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&devices).Error
|
||||
return devices, total, err
|
||||
}
|
||||
|
||||
// BindLicense 绑定授权码
|
||||
func (s *DeviceService) BindLicense(uid string, licenseCode string) error {
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", uid).First(&device).Error; err != nil {
|
||||
return errors.New("设备不存在")
|
||||
}
|
||||
|
||||
// 检查设备当前状态
|
||||
if device.LicenseCode != "" {
|
||||
return errors.New("设备已绑定授权码,请先解绑")
|
||||
}
|
||||
|
||||
// 验证授权码
|
||||
license, err := s.licenseService.GetLicenseByCode(licenseCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if license.Status != "unused" {
|
||||
return errors.New("授权码已被使用")
|
||||
}
|
||||
|
||||
// 根据授权类型处理
|
||||
switch license.LicenseType {
|
||||
case "time":
|
||||
if license.Duration <= 0 {
|
||||
return errors.New("无效的授权时长")
|
||||
}
|
||||
device.ExpireTime = time.Now().Add(time.Duration(license.Duration) * time.Minute)
|
||||
device.Duration = license.Duration
|
||||
device.MaxUses = 0
|
||||
case "count":
|
||||
if license.MaxUses <= 0 {
|
||||
return errors.New("无效的使用次数")
|
||||
}
|
||||
device.ExpireTime = time.Time{} // 清空过期时间
|
||||
device.Duration = 0
|
||||
device.MaxUses = license.MaxUses
|
||||
device.StartCount = 0 // 重置启动次数
|
||||
case "permanent":
|
||||
device.ExpireTime = time.Time{} // 清空过期时间
|
||||
device.Duration = 0
|
||||
device.MaxUses = 0
|
||||
default:
|
||||
return errors.New("无效的授权类型")
|
||||
}
|
||||
|
||||
// 更新设备基本信息
|
||||
device.LicenseCode = licenseCode
|
||||
device.LicenseType = license.LicenseType
|
||||
device.Status = "active"
|
||||
device.LastActiveAt = time.Now()
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 更新设备信息
|
||||
if err := tx.Save(&device).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新授权码状态
|
||||
if err := tx.Model(&model.LicenseCode{}).Where("code = ?", licenseCode).
|
||||
Updates(map[string]interface{}{
|
||||
"status": "used",
|
||||
"used_by": uid,
|
||||
"used_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录设备日志
|
||||
log := model.DeviceLog{
|
||||
DeviceUID: uid,
|
||||
Action: "bind_license",
|
||||
Message: fmt.Sprintf("绑定%s授权码: %s", getLicenseTypeText(license.LicenseType), licenseCode),
|
||||
Status: "success",
|
||||
}
|
||||
if err := tx.Create(&log).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// 获取授权类型的中文描述
|
||||
func getLicenseTypeText(licenseType string) string {
|
||||
switch licenseType {
|
||||
case "time":
|
||||
return "时间"
|
||||
case "count":
|
||||
return "次数"
|
||||
case "permanent":
|
||||
return "永久"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// UnbindLicense 解绑授权码
|
||||
func (s *DeviceService) UnbindLicense(uid string) error {
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", uid).First(&device).Error; err != nil {
|
||||
return errors.New("设备不存在")
|
||||
}
|
||||
|
||||
if device.LicenseCode == "" {
|
||||
return errors.New("设备未绑定授权码")
|
||||
}
|
||||
|
||||
oldLicenseCode := device.LicenseCode
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 更新设备信息
|
||||
if err := tx.Model(&device).Updates(map[string]interface{}{
|
||||
"license_code": "",
|
||||
"license_type": "",
|
||||
"status": "inactive",
|
||||
"expire_time": nil,
|
||||
"max_uses": 0,
|
||||
"duration": 0,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新授权码状态
|
||||
if err := tx.Model(&model.LicenseCode{}).Where("code = ?", oldLicenseCode).
|
||||
Updates(map[string]interface{}{
|
||||
"status": "unused",
|
||||
"used_by": "",
|
||||
"used_at": nil,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录设备日志
|
||||
log := model.DeviceLog{
|
||||
DeviceUID: uid,
|
||||
Action: "unbind_license",
|
||||
Message: fmt.Sprintf("解绑授权码: %s", oldLicenseCode),
|
||||
Status: "success",
|
||||
}
|
||||
if err := tx.Create(&log).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetDeviceLogs 获取设备日志
|
||||
func (s *DeviceService) GetDeviceLogs(uid string, page, pageSize int) ([]model.DeviceLog, int64, error) {
|
||||
var logs []model.DeviceLog
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.DeviceLog{}).Where("device_uid = ?", uid)
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
// DashboardStats 仪表盘统计数据
|
||||
type DashboardStats struct {
|
||||
TotalDevices int64 `json:"total_devices"` // 设备总数
|
||||
TotalLicenses int64 `json:"total_licenses"` // 授权码总数
|
||||
TodayNew int64 `json:"today_new"` // 今日新增
|
||||
OnlineDevices int64 `json:"online_devices"` // 在线设备
|
||||
ActiveDevices int64 `json:"active_devices"` // 激活设备
|
||||
ExpiredDevices int64 `json:"expired_devices"` // 过期设备
|
||||
}
|
||||
|
||||
// GetDashboardStats 获取仪表盘统计数据
|
||||
func (s *DeviceService) GetDashboardStats() (*DashboardStats, error) {
|
||||
var stats DashboardStats
|
||||
|
||||
// 获取设备总数
|
||||
s.db.Model(&model.Device{}).Count(&stats.TotalDevices)
|
||||
|
||||
// 获取授权码总数
|
||||
s.db.Model(&model.LicenseCode{}).Count(&stats.TotalLicenses)
|
||||
|
||||
// 获取今日新增设备数
|
||||
today := time.Now().Format("2006-01-02")
|
||||
s.db.Model(&model.Device{}).Where("DATE(register_time) = ?", today).Count(&stats.TodayNew)
|
||||
|
||||
// 获取在线设备数(最近30分钟内有活动的设备)
|
||||
thirtyMinutesAgo := time.Now().Add(-30 * time.Minute)
|
||||
s.db.Model(&model.Device{}).Where("last_active_at > ?", thirtyMinutesAgo).Count(&stats.OnlineDevices)
|
||||
|
||||
// 获取激活设备数
|
||||
s.db.Model(&model.Device{}).Where("status = ?", "active").Count(&stats.ActiveDevices)
|
||||
|
||||
// 获取过期设备数
|
||||
s.db.Model(&model.Device{}).Where("status = ?", "expired").Count(&stats.ExpiredDevices)
|
||||
|
||||
return &stats, nil
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/utils"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDeviceService_RegisterDevice(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
licenseService := NewLicenseService(db)
|
||||
deviceService := NewDeviceService(db, licenseService)
|
||||
|
||||
// 创建测试设备型号
|
||||
deviceModel := &model.DeviceModel{
|
||||
ModelName: "test-model",
|
||||
DeviceType: "software",
|
||||
Company: "test-company",
|
||||
Status: "active",
|
||||
CreatedBy: 1,
|
||||
}
|
||||
err := db.Create(deviceModel).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 创建测试授权码
|
||||
licenses, err := licenseService.CreateLicenses(&LicenseCreateInput{
|
||||
LicenseType: "time",
|
||||
Duration: 30,
|
||||
Count: 1,
|
||||
Remark: "test",
|
||||
}, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, licenses, 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input *DeviceRegisterInput
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "正常注册设备",
|
||||
input: &DeviceRegisterInput{
|
||||
UID: "test-device-001",
|
||||
DeviceModel: "test-model",
|
||||
LicenseCode: licenses[0].Code,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "重复注册设备",
|
||||
input: &DeviceRegisterInput{
|
||||
UID: "test-device-001",
|
||||
DeviceModel: "test-model",
|
||||
LicenseCode: licenses[0].Code,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "使用无效授权码",
|
||||
input: &DeviceRegisterInput{
|
||||
UID: "test-device-002",
|
||||
DeviceModel: "test-model",
|
||||
LicenseCode: "invalid-code",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "使用不存在的设备型号",
|
||||
input: &DeviceRegisterInput{
|
||||
UID: "test-device-003",
|
||||
DeviceModel: "non-existent-model",
|
||||
LicenseCode: licenses[0].Code,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := deviceService.RegisterDevice(tt.input, "127.0.0.1")
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证设备是否正确注册
|
||||
var device model.Device
|
||||
err = db.Where("uid = ?", tt.input.UID).First(&device).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, deviceModel.DeviceType, device.DeviceType)
|
||||
assert.Equal(t, "active", device.Status)
|
||||
assert.Equal(t, tt.input.LicenseCode, device.LicenseCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceService_ValidateDevice(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
licenseService := NewLicenseService(db)
|
||||
deviceService := NewDeviceService(db, licenseService)
|
||||
|
||||
// 创建测试设备型号
|
||||
deviceModel := &model.DeviceModel{
|
||||
ModelName: "test-model",
|
||||
DeviceType: "software",
|
||||
Company: "test-company",
|
||||
CreatedBy: 1,
|
||||
}
|
||||
err := db.Create(deviceModel).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 创建测试授权码
|
||||
licenses, err := licenseService.CreateLicenses(&LicenseCreateInput{
|
||||
LicenseType: "time",
|
||||
Duration: 30,
|
||||
Count: 1,
|
||||
Remark: "test",
|
||||
}, 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 注册测试设备
|
||||
device := &model.Device{
|
||||
UID: "test-device-001",
|
||||
DeviceType: deviceModel.DeviceType,
|
||||
DeviceModel: deviceModel.ModelName,
|
||||
Company: deviceModel.Company,
|
||||
RegisterTime: time.Now(),
|
||||
Status: "active",
|
||||
LicenseCode: licenses[0].Code,
|
||||
LicenseType: "time",
|
||||
Duration: 30,
|
||||
ExpireTime: time.Now().Add(30 * 24 * time.Hour),
|
||||
}
|
||||
err = db.Create(device).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uid string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "验证正常设备",
|
||||
uid: "test-device-001",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "验证不存在的设备",
|
||||
uid: "non-existent-device",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "验证过期设备",
|
||||
uid: "expired-device",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 创建过期设备
|
||||
expiredDevice := &model.Device{
|
||||
UID: "expired-device",
|
||||
DeviceType: deviceModel.DeviceType,
|
||||
DeviceModel: deviceModel.ModelName,
|
||||
Company: deviceModel.Company,
|
||||
RegisterTime: time.Now(),
|
||||
Status: "expired",
|
||||
LicenseCode: "expired-license",
|
||||
LicenseType: "time",
|
||||
Duration: 30,
|
||||
ExpireTime: time.Now().Add(-24 * time.Hour),
|
||||
}
|
||||
err = db.Create(expiredDevice).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := deviceService.ValidateDevice(tt.uid)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceService_UpdateStartCount(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
licenseService := NewLicenseService(db)
|
||||
deviceService := NewDeviceService(db, licenseService)
|
||||
|
||||
// 创建测试设备型号
|
||||
deviceModel := &model.DeviceModel{
|
||||
ModelName: "test-model",
|
||||
DeviceType: "software",
|
||||
Company: "test-company",
|
||||
CreatedBy: 1,
|
||||
}
|
||||
err := db.Create(deviceModel).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 创建测试设备
|
||||
device := &model.Device{
|
||||
UID: "test-device-001",
|
||||
DeviceType: deviceModel.DeviceType,
|
||||
DeviceModel: deviceModel.ModelName,
|
||||
Company: deviceModel.Company,
|
||||
RegisterTime: time.Now(),
|
||||
Status: "active",
|
||||
LicenseType: "count",
|
||||
MaxUses: 5,
|
||||
StartCount: 0,
|
||||
}
|
||||
err = db.Create(device).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 测试更新启动次数
|
||||
for i := 1; i <= 5; i++ {
|
||||
err = deviceService.UpdateStartCount(device.UID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证启动次数
|
||||
var updatedDevice model.Device
|
||||
err = db.First(&updatedDevice, "uid = ?", device.UID).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, i, updatedDevice.StartCount)
|
||||
|
||||
// 检查最后一次是否将状态更新为过期
|
||||
if i == 5 {
|
||||
assert.Equal(t, "expired", updatedDevice.Status)
|
||||
} else {
|
||||
assert.Equal(t, "active", updatedDevice.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试超出使用次数
|
||||
err = deviceService.UpdateStartCount(device.UID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDeviceService_CreateDeviceModel(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
licenseService := NewLicenseService(db)
|
||||
deviceService := NewDeviceService(db, licenseService)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model *model.DeviceModel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "创建有效设备型号",
|
||||
model: &model.DeviceModel{
|
||||
ModelName: "test-model",
|
||||
DeviceType: "software",
|
||||
Company: "test-company",
|
||||
Status: "active",
|
||||
CreatedBy: 1,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "重复的设备型号",
|
||||
model: &model.DeviceModel{
|
||||
ModelName: "test-model",
|
||||
DeviceType: "software",
|
||||
Company: "test-company",
|
||||
Status: "active",
|
||||
CreatedBy: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := deviceService.CreateDeviceModel(tt.model)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证创建的设备型号
|
||||
var savedModel model.DeviceModel
|
||||
err = db.Where("model_name = ?", tt.model.ModelName).First(&savedModel).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.model.DeviceType, savedModel.DeviceType)
|
||||
assert.Equal(t, tt.model.Company, savedModel.Company)
|
||||
assert.Equal(t, tt.model.Status, savedModel.Status)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,709 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"encoding/hex"
|
||||
|
||||
"errors"
|
||||
|
||||
"fmt"
|
||||
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
|
||||
"licserver/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLicenseService(db *gorm.DB) *LicenseService {
|
||||
|
||||
return &LicenseService{db: db}
|
||||
|
||||
}
|
||||
|
||||
// 生成授权码
|
||||
|
||||
func generateLicenseCode() (string, error) {
|
||||
|
||||
b := make([]byte, 16)
|
||||
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
||||
return "", err
|
||||
|
||||
}
|
||||
|
||||
return hex.EncodeToString(b), nil
|
||||
|
||||
}
|
||||
|
||||
// 创建授权码
|
||||
|
||||
type LicenseCreateInput struct {
|
||||
LicenseType string `json:"license_type" binding:"required"`
|
||||
|
||||
Duration int `json:"duration"` // 时间授权的有效期(分钟)
|
||||
|
||||
MaxUses int `json:"max_uses"` // 次数授权的使用次数
|
||||
|
||||
Count int `json:"count" binding:"required,min=1"` // 生成数量
|
||||
|
||||
Remark string `json:"remark"` // 备注
|
||||
|
||||
BindCount int `json:"bind_count"` // 可绑定次数,默认为-1(无限制)
|
||||
|
||||
}
|
||||
|
||||
func (s *LicenseService) CreateLicenses(input *LicenseCreateInput, createdBy uint) ([]model.LicenseCode, error) {
|
||||
|
||||
// 验证参数
|
||||
|
||||
input.LicenseType = strings.ToLower(input.LicenseType) // 转为小写
|
||||
|
||||
switch input.LicenseType {
|
||||
case "time":
|
||||
if input.Duration <= 0 {
|
||||
return nil, errors.New("时间授权必须指定有效期")
|
||||
}
|
||||
case "count":
|
||||
if input.MaxUses <= 0 {
|
||||
return nil, errors.New("次数授权必须指定使用次数")
|
||||
}
|
||||
case "permanent":
|
||||
// 永久授权不需要额外参数
|
||||
default:
|
||||
return nil, errors.New("无效的授权类型")
|
||||
}
|
||||
|
||||
// 如果未指定绑定次数,设置为默认值-1
|
||||
if input.BindCount == 0 {
|
||||
input.BindCount = -1
|
||||
}
|
||||
|
||||
// 生成批次号
|
||||
|
||||
batchNo := time.Now().Format("20060102150405")
|
||||
|
||||
licenses := make([]model.LicenseCode, 0, input.Count)
|
||||
|
||||
for i := 0; i < input.Count; i++ {
|
||||
|
||||
code, err := generateLicenseCode()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
license := model.LicenseCode{
|
||||
|
||||
Code: code,
|
||||
|
||||
LicenseType: input.LicenseType,
|
||||
|
||||
Duration: input.Duration,
|
||||
|
||||
MaxUses: input.MaxUses,
|
||||
|
||||
Status: "unused",
|
||||
|
||||
CreatedBy: createdBy,
|
||||
|
||||
BatchNo: batchNo,
|
||||
|
||||
Remark: input.Remark,
|
||||
|
||||
BindCount: input.BindCount,
|
||||
}
|
||||
|
||||
licenses = append(licenses, license)
|
||||
|
||||
}
|
||||
|
||||
// 批量创建授权码
|
||||
|
||||
if err := s.db.Create(&licenses).Error; err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
|
||||
}
|
||||
|
||||
// 验证并使用授权码
|
||||
|
||||
func (s *LicenseService) UseLicense(code, deviceUID, ip string) (*model.LicenseCode, error) {
|
||||
|
||||
var license model.LicenseCode
|
||||
|
||||
if err := s.db.Where("code = ?", code).First(&license).Error; err != nil {
|
||||
|
||||
return nil, errors.New("授权码不存在")
|
||||
|
||||
}
|
||||
|
||||
// 检查授权码状态
|
||||
|
||||
if license.Status != "unused" {
|
||||
|
||||
return nil, errors.New("授权码已被使用")
|
||||
|
||||
}
|
||||
|
||||
// 检查绑定次数
|
||||
if license.BindCount == 0 {
|
||||
return nil, errors.New("授权码已达到最大绑定次数限制")
|
||||
}
|
||||
|
||||
// 更新授权码状态
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"status": "used",
|
||||
"used_by": deviceUID,
|
||||
"used_at": time.Now(),
|
||||
}
|
||||
|
||||
// 如果不是无限制,减少绑定次数
|
||||
if license.BindCount > 0 {
|
||||
updates["bind_count"] = license.BindCount - 1
|
||||
}
|
||||
|
||||
if err := s.db.Model(&license).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录使用日志
|
||||
log := model.LicenseLog{
|
||||
LicenseID: license.ID,
|
||||
DeviceUID: deviceUID,
|
||||
Action: "use",
|
||||
IP: ip,
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 使用授权码", deviceUID),
|
||||
}
|
||||
|
||||
if err := s.db.Create(&log).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &license, nil
|
||||
|
||||
}
|
||||
|
||||
// 获取授权码列表
|
||||
|
||||
func (s *LicenseService) GetLicenses(status, licenseType, batchNo string, page, pageSize int) ([]model.LicenseCode, int64, error) {
|
||||
|
||||
var licenses []model.LicenseCode
|
||||
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.LicenseCode{})
|
||||
|
||||
if status != "" {
|
||||
|
||||
query = query.Where("status = ?", strings.ToLower(status))
|
||||
|
||||
}
|
||||
|
||||
if licenseType != "" {
|
||||
|
||||
query = query.Where("license_type = ?", strings.ToLower(licenseType))
|
||||
|
||||
}
|
||||
|
||||
if batchNo != "" {
|
||||
|
||||
query = query.Where("batch_no = ?", batchNo)
|
||||
|
||||
}
|
||||
|
||||
// 获取所有符合条件的授权码
|
||||
|
||||
var allLicenses []model.LicenseCode
|
||||
|
||||
if err := query.Find(&allLicenses).Error; err != nil {
|
||||
|
||||
return nil, 0, err
|
||||
|
||||
}
|
||||
|
||||
// 检查每个授权码的有效性
|
||||
|
||||
for i := range allLicenses {
|
||||
|
||||
if allLicenses[i].Status == "used" {
|
||||
|
||||
if err := s.CheckLicenseValidity(allLicenses[i].Code); err != nil {
|
||||
|
||||
// 如果检查失败,更新状态
|
||||
|
||||
s.db.Model(&allLicenses[i]).Update("status", "expired")
|
||||
|
||||
allLicenses[i].Status = "expired"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
total = int64(len(allLicenses))
|
||||
|
||||
// 分页
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
|
||||
start := (page - 1) * pageSize
|
||||
|
||||
end := start + pageSize
|
||||
|
||||
if start < len(allLicenses) {
|
||||
|
||||
if end > len(allLicenses) {
|
||||
|
||||
end = len(allLicenses)
|
||||
|
||||
}
|
||||
|
||||
licenses = allLicenses[start:end]
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
licenses = allLicenses
|
||||
|
||||
}
|
||||
|
||||
return licenses, total, nil
|
||||
|
||||
}
|
||||
|
||||
// 获取授权码使用日志
|
||||
|
||||
func (s *LicenseService) GetLicenseLogs(licenseID uint, page, pageSize int) ([]model.LicenseLog, int64, error) {
|
||||
|
||||
var logs []model.LicenseLog
|
||||
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.LicenseLog{}).Where("license_id = ?", licenseID)
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&logs).Error
|
||||
|
||||
return logs, total, err
|
||||
|
||||
}
|
||||
|
||||
// ExportLogs 导出授权码日志
|
||||
|
||||
func (s *LicenseService) ExportLogs(licenseID uint) ([]byte, error) {
|
||||
|
||||
logs, _, err := s.GetLicenseLogs(licenseID, 0, 0) // 获取所有日志
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 创建CSV内容
|
||||
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString("操作类,设备UID,IP地址,状态,详细信息,时间\n")
|
||||
|
||||
for _, log := range logs {
|
||||
|
||||
// 转换操作类型
|
||||
|
||||
action := map[string]string{
|
||||
|
||||
"create": "创建",
|
||||
|
||||
"use": "使用",
|
||||
|
||||
"verify": "验证",
|
||||
}[log.Action]
|
||||
|
||||
// 转换状态
|
||||
|
||||
status := map[string]string{
|
||||
|
||||
"success": "成功",
|
||||
|
||||
"failed": "失败",
|
||||
}[log.Status]
|
||||
|
||||
// 写入一行记录
|
||||
|
||||
content.WriteString(fmt.Sprintf("%s,%s,%s,%s,%s,%s\n",
|
||||
|
||||
action,
|
||||
|
||||
log.DeviceUID,
|
||||
|
||||
log.IP,
|
||||
|
||||
status,
|
||||
|
||||
log.Message,
|
||||
|
||||
log.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
return []byte(content.String()), nil
|
||||
|
||||
}
|
||||
|
||||
// 撤销授权码
|
||||
|
||||
func (s *LicenseService) RevokeLicense(code string, userID uint) error {
|
||||
|
||||
var license model.LicenseCode
|
||||
|
||||
if err := s.db.Where("code = ?", code).First(&license).Error; err != nil {
|
||||
|
||||
return errors.New("授权码不存在")
|
||||
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
|
||||
if license.CreatedBy != userID {
|
||||
|
||||
return errors.New("无权操作此授权码")
|
||||
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
|
||||
if err := s.db.Model(&license).Update("status", "revoked").Error; err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
|
||||
log := model.LicenseLog{
|
||||
|
||||
LicenseID: license.ID,
|
||||
|
||||
Action: "revoke",
|
||||
|
||||
Status: "success",
|
||||
|
||||
Message: "授权码已撤销",
|
||||
}
|
||||
|
||||
s.db.Create(&log)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 批量撤销授权码
|
||||
|
||||
func (s *LicenseService) RevokeLicenses(codes []string, userID uint) error {
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
for _, code := range codes {
|
||||
|
||||
var license model.LicenseCode
|
||||
|
||||
if err := tx.Where("code = ?", code).First(&license).Error; err != nil {
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
|
||||
if license.CreatedBy != userID {
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
|
||||
if err := tx.Model(&license).Update("status", "revoked").Error; err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
|
||||
log := model.LicenseLog{
|
||||
|
||||
LicenseID: license.ID,
|
||||
|
||||
Action: "revoke",
|
||||
|
||||
Status: "success",
|
||||
|
||||
Message: "授权码已撤销",
|
||||
}
|
||||
|
||||
tx.Create(&log)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 验证授权码
|
||||
|
||||
func (s *LicenseService) ValidateLicense(code string) (*model.LicenseCode, error) {
|
||||
|
||||
var license model.LicenseCode
|
||||
|
||||
if err := s.db.Where("code = ?", code).First(&license).Error; err != nil {
|
||||
|
||||
return nil, errors.New("无效的授权码")
|
||||
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
|
||||
if license.Status != "unused" {
|
||||
|
||||
return nil, errors.New("授权码已被使用或已撤销")
|
||||
|
||||
}
|
||||
|
||||
return &license, nil
|
||||
|
||||
}
|
||||
|
||||
// 导出授权码
|
||||
|
||||
func (s *LicenseService) ExportLicenses(codes []string) ([]byte, error) {
|
||||
|
||||
var licenses []model.LicenseCode
|
||||
|
||||
if err := s.db.Where("code IN ?", codes).Find(&licenses).Error; err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 创建CSV内容
|
||||
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString("授权码,授权类型,有效期(天),使用次数,状态,使用设备,使用时间,批次号,备注\n")
|
||||
|
||||
for _, license := range licenses {
|
||||
|
||||
// 转换授权类型
|
||||
|
||||
licenseType := map[string]string{
|
||||
|
||||
"time": "时间授权",
|
||||
|
||||
"count": "次数授权",
|
||||
|
||||
"permanent": "永久授权",
|
||||
}[license.LicenseType]
|
||||
|
||||
// 转换状态
|
||||
|
||||
status := map[string]string{
|
||||
|
||||
"unused": "未使用",
|
||||
|
||||
"used": "已使用",
|
||||
|
||||
"revoked": "已撤销",
|
||||
}[license.Status]
|
||||
|
||||
// 写入一行记录
|
||||
|
||||
content.WriteString(fmt.Sprintf("%s,%s,%d,%d,%s,%s,%s,%s,%s\n",
|
||||
|
||||
license.Code,
|
||||
|
||||
licenseType,
|
||||
|
||||
license.Duration,
|
||||
|
||||
license.MaxUses,
|
||||
|
||||
status,
|
||||
|
||||
license.UsedBy,
|
||||
|
||||
license.UsedAt.Format("2006-01-02 15:04:05"),
|
||||
|
||||
license.BatchNo,
|
||||
|
||||
license.Remark,
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
return []byte(content.String()), nil
|
||||
|
||||
}
|
||||
|
||||
// 获取授权码统计信息
|
||||
|
||||
func (s *LicenseService) GetLicenseStats() (map[string]interface{}, error) {
|
||||
|
||||
var stats struct {
|
||||
Total int64
|
||||
|
||||
Unused int64
|
||||
|
||||
Used int64
|
||||
|
||||
Revoked int64
|
||||
|
||||
Today int64
|
||||
|
||||
ThisWeek int64
|
||||
|
||||
ThisMonth int64
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Count(&stats.Total)
|
||||
|
||||
// 获取各状态数量
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "unused").Count(&stats.Unused)
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "used").Count(&stats.Used)
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "revoked").Count(&stats.Revoked)
|
||||
|
||||
// 获取今日创建数量
|
||||
|
||||
today := time.Now().Format("2006-01-02")
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Where("DATE(created_at) = ?", today).Count(&stats.Today)
|
||||
|
||||
// 获取本周创建数量
|
||||
|
||||
weekStart := time.Now().AddDate(0, 0, -int(time.Now().Weekday()))
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Where("created_at >= ?", weekStart).Count(&stats.ThisWeek)
|
||||
|
||||
// 获取本月创建数量
|
||||
|
||||
monthStart := time.Now().Format("2006-01") + "-01"
|
||||
|
||||
s.db.Model(&model.LicenseCode{}).Where("created_at >= ?", monthStart).Count(&stats.ThisMonth)
|
||||
|
||||
return map[string]interface{}{
|
||||
|
||||
"total": stats.Total,
|
||||
|
||||
"unused": stats.Unused,
|
||||
|
||||
"used": stats.Used,
|
||||
|
||||
"revoked": stats.Revoked,
|
||||
|
||||
"today": stats.Today,
|
||||
|
||||
"this_week": stats.ThisWeek,
|
||||
|
||||
"this_month": stats.ThisMonth,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// 添加检查授权码有效性的方法
|
||||
|
||||
func (s *LicenseService) CheckLicenseValidity(code string) error {
|
||||
|
||||
var license model.LicenseCode
|
||||
|
||||
if err := s.db.Where("code = ?", code).First(&license).Error; err != nil {
|
||||
|
||||
return errors.New("授权码不存在")
|
||||
|
||||
}
|
||||
|
||||
if license.Status != "unused" && license.Status != "used" {
|
||||
|
||||
return errors.New("授权码已被撤销或过期")
|
||||
|
||||
}
|
||||
|
||||
// 检查授权类型特定的限制
|
||||
|
||||
switch license.LicenseType {
|
||||
case "time":
|
||||
// 计算过期时间
|
||||
|
||||
expireTime := license.UsedAt.Add(time.Duration(license.Duration) * time.Minute)
|
||||
|
||||
if time.Now().After(expireTime) {
|
||||
|
||||
// 更新状态为过期
|
||||
|
||||
s.db.Model(&license).Update("status", "expired")
|
||||
|
||||
return errors.New("授权码已过期")
|
||||
|
||||
}
|
||||
case "count":
|
||||
if license.UsedCount >= license.MaxUses {
|
||||
|
||||
// 更新状态为过期
|
||||
|
||||
s.db.Model(&license).Update("status", "expired")
|
||||
|
||||
return errors.New("授权码使用次数已达上限")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// GetLicenseByCode 通过授权码获取授权信息
|
||||
func (s *LicenseService) GetLicenseByCode(code string) (*model.LicenseCode, error) {
|
||||
var license model.LicenseCode
|
||||
if err := s.db.Where("code = ?", code).First(&license).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("授权码不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &license, nil
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"licserver/internal/utils"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLicenseService_CreateLicenses(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
service := NewLicenseService(db)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input *LicenseCreateInput
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "创建时间授权码",
|
||||
input: &LicenseCreateInput{
|
||||
LicenseType: "time",
|
||||
Duration: 30,
|
||||
Count: 5,
|
||||
Remark: "test time license",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "创建次数授权码",
|
||||
input: &LicenseCreateInput{
|
||||
LicenseType: "count",
|
||||
MaxUses: 100,
|
||||
Count: 3,
|
||||
Remark: "test count license",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "创建永久授权码",
|
||||
input: &LicenseCreateInput{
|
||||
LicenseType: "permanent",
|
||||
Count: 1,
|
||||
Remark: "test permanent license",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "无效的授权类型",
|
||||
input: &LicenseCreateInput{
|
||||
LicenseType: "invalid",
|
||||
Count: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "时间授权无有效期",
|
||||
input: &LicenseCreateInput{
|
||||
LicenseType: "time",
|
||||
Duration: 0,
|
||||
Count: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
licenses, err := service.CreateLicenses(tt.input, 1)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, licenses, tt.input.Count)
|
||||
|
||||
for _, license := range licenses {
|
||||
assert.Equal(t, tt.input.LicenseType, license.LicenseType)
|
||||
assert.Equal(t, "unused", license.Status)
|
||||
assert.Equal(t, tt.input.Remark, license.Remark)
|
||||
|
||||
if tt.input.LicenseType == "time" {
|
||||
assert.Equal(t, tt.input.Duration, license.Duration)
|
||||
} else if tt.input.LicenseType == "count" {
|
||||
assert.Equal(t, tt.input.MaxUses, license.MaxUses)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseService_UseLicense(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
service := NewLicenseService(db)
|
||||
|
||||
// 创建测试授权码
|
||||
input := &LicenseCreateInput{
|
||||
LicenseType: "time",
|
||||
Duration: 30,
|
||||
Count: 1,
|
||||
Remark: "test",
|
||||
}
|
||||
|
||||
licenses, err := service.CreateLicenses(input, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, licenses, 1)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
code string
|
||||
deviceUID string
|
||||
ip string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "正常使用授权码",
|
||||
code: licenses[0].Code,
|
||||
deviceUID: "test-device-001",
|
||||
ip: "127.0.0.1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "使用不存在的授权码",
|
||||
code: "invalid-code",
|
||||
deviceUID: "test-device-002",
|
||||
ip: "127.0.0.1",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "重复使用授权码",
|
||||
code: licenses[0].Code,
|
||||
deviceUID: "test-device-003",
|
||||
ip: "127.0.0.1",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
license, err := service.UseLicense(tt.code, tt.deviceUID, tt.ip)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "used", license.Status)
|
||||
assert.Equal(t, tt.deviceUID, license.UsedBy)
|
||||
assert.NotZero(t, license.UsedAt)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,377 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
|
||||
"runtime"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/process"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MonitorService struct {
|
||||
db *gorm.DB
|
||||
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func NewMonitorService(db *gorm.DB) *MonitorService {
|
||||
|
||||
return &MonitorService{
|
||||
|
||||
db: db,
|
||||
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) GetSystemStatus() (*model.SystemStatus, error) {
|
||||
|
||||
status := &model.SystemStatus{}
|
||||
|
||||
// CPU信息
|
||||
|
||||
if err := s.getCPUInfo(status); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 内存信息
|
||||
|
||||
if err := s.getMemoryInfo(status); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 磁盘信息
|
||||
|
||||
if err := s.getDiskInfo(status); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 网络信息
|
||||
|
||||
if err := s.getNetworkInfo(status); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 进程信息
|
||||
|
||||
if err := s.getProcessInfo(status); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 主机信息
|
||||
|
||||
if err := s.getHostInfo(status); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 系统信息
|
||||
|
||||
s.getSystemInfo(status)
|
||||
|
||||
return status, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getCPUInfo(status *model.SystemStatus) error {
|
||||
|
||||
cpuPercent, err := cpu.Percent(time.Second, false)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// Windows 系统不支持 LoadAvg,设置为默认值
|
||||
|
||||
status.CPU.LoadAvg = []float64{0, 0, 0}
|
||||
|
||||
cpuInfo, err := cpu.Info()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
status.CPU.Usage = cpuPercent[0]
|
||||
|
||||
status.CPU.CoreCount = runtime.NumCPU()
|
||||
|
||||
if len(cpuInfo) > 0 {
|
||||
|
||||
status.CPU.ModelName = cpuInfo[0].ModelName
|
||||
|
||||
status.CPU.MHz = cpuInfo[0].Mhz
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getMemoryInfo(status *model.SystemStatus) error {
|
||||
|
||||
memInfo, err := mem.VirtualMemory()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
swapInfo, err := mem.SwapMemory()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
status.Memory.Total = memInfo.Total
|
||||
|
||||
status.Memory.Used = memInfo.Used
|
||||
|
||||
status.Memory.Free = memInfo.Free
|
||||
|
||||
status.Memory.UsageRate = memInfo.UsedPercent
|
||||
|
||||
status.Memory.SwapTotal = swapInfo.Total
|
||||
|
||||
status.Memory.SwapUsed = swapInfo.Used
|
||||
|
||||
status.Memory.SwapFree = swapInfo.Free
|
||||
|
||||
status.Memory.SwapUsageRate = swapInfo.UsedPercent
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getDiskInfo(status *model.SystemStatus) error {
|
||||
|
||||
partitions, err := disk.Partitions(true)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
status.Disk.Partitions = make([]model.DiskPartition, 0)
|
||||
|
||||
for _, partition := range partitions {
|
||||
|
||||
usage, err := disk.Usage(partition.Mountpoint)
|
||||
|
||||
if err != nil {
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
status.Disk.Partitions = append(status.Disk.Partitions, model.DiskPartition{
|
||||
|
||||
Device: partition.Device,
|
||||
|
||||
Mountpoint: partition.Mountpoint,
|
||||
|
||||
Fstype: partition.Fstype,
|
||||
|
||||
Total: usage.Total,
|
||||
|
||||
Used: usage.Used,
|
||||
|
||||
Free: usage.Free,
|
||||
|
||||
UsageRate: usage.UsedPercent,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getNetworkInfo(status *model.SystemStatus) error {
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
ioCounters, err := net.IOCounters(true)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
status.Network.Interfaces = make([]model.NetworkInterface, 0)
|
||||
|
||||
for _, iface := range interfaces {
|
||||
|
||||
var counter net.IOCountersStat
|
||||
|
||||
for _, io := range ioCounters {
|
||||
|
||||
if io.Name == iface.Name {
|
||||
|
||||
counter = io
|
||||
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 获取接口的地址列表
|
||||
|
||||
addrs := make([]string, 0)
|
||||
|
||||
for _, addr := range iface.Addrs {
|
||||
addrs = append(addrs, addr.String())
|
||||
}
|
||||
|
||||
status.Network.Interfaces = append(status.Network.Interfaces, model.NetworkInterface{
|
||||
|
||||
Name: iface.Name,
|
||||
|
||||
BytesSent: counter.BytesSent,
|
||||
|
||||
BytesRecv: counter.BytesRecv,
|
||||
|
||||
PacketsSent: counter.PacketsSent,
|
||||
|
||||
PacketsRecv: counter.PacketsRecv,
|
||||
|
||||
Addrs: addrs,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getProcessInfo(status *model.SystemStatus) error {
|
||||
|
||||
processes, err := process.Processes()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
status.Process.Total = len(processes)
|
||||
|
||||
status.Process.List = make([]model.ProcessInfo, 0)
|
||||
|
||||
for i := 0; i < 10 && i < len(processes); i++ {
|
||||
|
||||
p := processes[i]
|
||||
|
||||
name, _ := p.Name()
|
||||
|
||||
cpu, _ := p.CPUPercent()
|
||||
|
||||
mem, _ := p.MemoryPercent()
|
||||
|
||||
created, _ := p.CreateTime()
|
||||
|
||||
status.Process.List = append(status.Process.List, model.ProcessInfo{
|
||||
|
||||
PID: int(p.Pid),
|
||||
|
||||
Name: name,
|
||||
|
||||
CPU: cpu,
|
||||
|
||||
Memory: float64(mem),
|
||||
|
||||
Created: created,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getHostInfo(status *model.SystemStatus) error {
|
||||
|
||||
info, err := host.Info()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
status.Host.Hostname = info.Hostname
|
||||
|
||||
status.Host.OS = info.OS
|
||||
|
||||
status.Host.Platform = info.Platform
|
||||
|
||||
status.Host.PlatformVersion = info.PlatformVersion
|
||||
|
||||
status.Host.KernelVersion = info.KernelVersion
|
||||
|
||||
status.Host.BootTime = time.Unix(int64(info.BootTime), 0)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *MonitorService) getSystemInfo(status *model.SystemStatus) {
|
||||
|
||||
status.System.Uptime = time.Since(s.startTime)
|
||||
|
||||
status.System.CurrentTime = time.Now()
|
||||
|
||||
var activeUsers int64
|
||||
|
||||
s.db.Model(&model.User{}).Count(&activeUsers)
|
||||
|
||||
status.System.ActiveUsers = int(activeUsers)
|
||||
|
||||
var totalDevices int64
|
||||
|
||||
s.db.Model(&model.Device{}).Count(&totalDevices)
|
||||
|
||||
status.System.TotalDevices = int(totalDevices)
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"licserver/internal/utils"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type SiteService struct {
|
||||
config *utils.Config
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewSiteService(config *utils.Config) *SiteService {
|
||||
return &SiteService{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SiteService) GetSettings() utils.SiteConfig {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.config.Site
|
||||
}
|
||||
|
||||
func (s *SiteService) UpdateSettings(settings utils.SiteConfig) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 更新内存中的配置
|
||||
s.config.Site = settings
|
||||
|
||||
// 持久化配置到文件
|
||||
return utils.SaveConfig(s.config, "system", "更新站点设置")
|
||||
}
|
||||
|
||||
// ValidateSettings 验证站点设置
|
||||
func (s *SiteService) ValidateSettings(settings utils.SiteConfig) error {
|
||||
// 这里可以添加更多的验证逻辑
|
||||
if settings.Title == "" {
|
||||
return errors.New("站点标题不能为空")
|
||||
}
|
||||
if settings.BaseURL == "" {
|
||||
return errors.New("基础URL不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"licserver/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TokenService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTokenService(db *gorm.DB) *TokenService {
|
||||
return &TokenService{db: db}
|
||||
}
|
||||
|
||||
// 生成访问令牌
|
||||
func generateToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// 创建访问令牌
|
||||
func (s *TokenService) CreateToken(deviceUID, tokenType string, expireTime time.Time, ipList []string, createdBy uint) (*model.AccessToken, error) {
|
||||
// 验证设备是否存在
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", deviceUID).First(&device).Error; err != nil {
|
||||
return nil, errors.New("设备不存在")
|
||||
}
|
||||
|
||||
// 生成令牌
|
||||
token, err := generateToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建令牌记录
|
||||
accessToken := &model.AccessToken{
|
||||
Token: token,
|
||||
DeviceUID: deviceUID,
|
||||
Type: tokenType,
|
||||
Status: "active",
|
||||
ExpireTime: expireTime,
|
||||
IPList: strings.Join(ipList, ","),
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
|
||||
if err := s.db.Create(accessToken).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
tokenLog := &model.TokenLog{
|
||||
TokenID: accessToken.ID,
|
||||
Action: "create",
|
||||
Status: "success",
|
||||
Message: "创建访问令牌",
|
||||
}
|
||||
s.db.Create(tokenLog)
|
||||
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
// 验证令牌
|
||||
func (s *TokenService) ValidateToken(token, ip string) (*model.AccessToken, error) {
|
||||
var accessToken model.AccessToken
|
||||
if err := s.db.Where("token = ?", token).First(&accessToken).Error; err != nil {
|
||||
return nil, errors.New("无效的令牌")
|
||||
}
|
||||
|
||||
// 检查令牌状态
|
||||
if accessToken.Status != "active" {
|
||||
return nil, errors.New("令牌已被撤销")
|
||||
}
|
||||
|
||||
// 检查过期时间
|
||||
if time.Now().After(accessToken.ExpireTime) {
|
||||
return nil, errors.New("令牌已过期")
|
||||
}
|
||||
|
||||
// 检查IP限制
|
||||
if accessToken.IPList != "" {
|
||||
allowedIPs := strings.Split(accessToken.IPList, ",")
|
||||
allowed := false
|
||||
for _, allowedIP := range allowedIPs {
|
||||
if allowedIP == ip {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return nil, errors.New("IP地址不允许访问")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新使用记录
|
||||
s.db.Model(&accessToken).Updates(map[string]interface{}{
|
||||
"last_used": time.Now(),
|
||||
"usage_count": gorm.Expr("usage_count + 1"),
|
||||
})
|
||||
|
||||
// 记录日志
|
||||
tokenLog := &model.TokenLog{
|
||||
TokenID: accessToken.ID,
|
||||
Action: "use",
|
||||
IP: ip,
|
||||
Status: "success",
|
||||
}
|
||||
s.db.Create(tokenLog)
|
||||
|
||||
return &accessToken, nil
|
||||
}
|
||||
|
||||
// 撤销令牌
|
||||
func (s *TokenService) RevokeToken(token string, userID uint) error {
|
||||
var accessToken model.AccessToken
|
||||
if err := s.db.Where("token = ?", token).First(&accessToken).Error; err != nil {
|
||||
return errors.New("令牌不存在")
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if accessToken.CreatedBy != userID {
|
||||
return errors.New("无权操作此令牌")
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
if err := s.db.Model(&accessToken).Update("status", "revoked").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
tokenLog := &model.TokenLog{
|
||||
TokenID: accessToken.ID,
|
||||
Action: "revoke",
|
||||
Status: "success",
|
||||
Message: "撤销访问令牌",
|
||||
}
|
||||
s.db.Create(tokenLog)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取令牌列表
|
||||
func (s *TokenService) GetTokens(deviceUID string, page, pageSize int) ([]model.AccessToken, int64, error) {
|
||||
var tokens []model.AccessToken
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.AccessToken{})
|
||||
if deviceUID != "" {
|
||||
query = query.Where("device_uid = ?", deviceUID)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&tokens).Error
|
||||
return tokens, total, err
|
||||
}
|
||||
|
||||
// 获取令牌日志
|
||||
func (s *TokenService) GetTokenLogs(tokenID uint, page, pageSize int) ([]model.TokenLog, int64, error) {
|
||||
var logs []model.TokenLog
|
||||
var total int64
|
||||
|
||||
query := s.db.Model(&model.TokenLog{}).Where("token_id = ?", tokenID)
|
||||
query.Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").Find(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
|
@ -0,0 +1,463 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fmt"
|
||||
|
||||
"io"
|
||||
|
||||
"mime/multipart"
|
||||
|
||||
"os"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"sort"
|
||||
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
|
||||
"licserver/internal/model"
|
||||
|
||||
"licserver/internal/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"crypto/sha256"
|
||||
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
type UploadService struct {
|
||||
db *gorm.DB
|
||||
|
||||
config *utils.Config
|
||||
}
|
||||
|
||||
func NewUploadService(db *gorm.DB, config *utils.Config) *UploadService {
|
||||
|
||||
return &UploadService{
|
||||
|
||||
db: db,
|
||||
|
||||
config: config,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) UploadFile(file *multipart.FileHeader, userID uint, deviceUID, description string) (*model.FileUpload, error) {
|
||||
|
||||
// 生成唯一文件名
|
||||
|
||||
ext := filepath.Ext(file.Filename)
|
||||
|
||||
uniqueID := uuid.New().String()
|
||||
|
||||
fileName := fmt.Sprintf("%s%s", uniqueID, ext)
|
||||
|
||||
filePath := filepath.Join(s.config.Upload.Path, fileName)
|
||||
|
||||
// 确保上传目录存在
|
||||
|
||||
if err := os.MkdirAll(s.config.Upload.Path, 0755); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
|
||||
src, err := file.Open()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(filePath)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
|
||||
if _, err = io.Copy(dst, src); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 如果提供了设备UID,获取设备型号
|
||||
|
||||
var deviceModel string
|
||||
|
||||
if deviceUID != "" {
|
||||
|
||||
var device model.Device
|
||||
|
||||
if err := s.db.Where("uid = ?", deviceUID).First(&device).Error; err == nil {
|
||||
|
||||
deviceModel = device.DeviceModel
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 创建数据库记录
|
||||
|
||||
upload := &model.FileUpload{
|
||||
|
||||
FileName: file.Filename,
|
||||
|
||||
FilePath: filePath,
|
||||
|
||||
FileSize: file.Size,
|
||||
|
||||
FileType: strings.ToLower(ext),
|
||||
|
||||
UploadedBy: userID,
|
||||
|
||||
DeviceUID: deviceUID,
|
||||
|
||||
DeviceModel: deviceModel,
|
||||
|
||||
Description: description,
|
||||
}
|
||||
|
||||
if err := s.db.Create(upload).Error; err != nil {
|
||||
|
||||
os.Remove(filePath)
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) DownloadFile(id uint) (*model.FileUpload, error) {
|
||||
|
||||
var file model.FileUpload
|
||||
|
||||
if err := s.db.First(&file, id).Error; err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return &file, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) DeleteFile(id uint, userID uint) error {
|
||||
|
||||
var file model.FileUpload
|
||||
|
||||
if err := s.db.First(&file, id).Error; err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
if file.UploadedBy != userID {
|
||||
|
||||
return errors.New("无权删除此文件")
|
||||
|
||||
}
|
||||
|
||||
if err := os.Remove(file.FilePath); err != nil && !os.IsNotExist(err) {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
return s.db.Delete(&file).Error
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) GetDeviceFiles(deviceUID string) ([]model.FileUpload, error) {
|
||||
|
||||
var files []model.FileUpload
|
||||
|
||||
err := s.db.Where("device_uid = ?", deviceUID).Find(&files).Error
|
||||
|
||||
return files, err
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) UploadChunk(
|
||||
|
||||
file *multipart.FileHeader,
|
||||
|
||||
fileHash string,
|
||||
|
||||
chunkNumber int,
|
||||
|
||||
totalChunks int,
|
||||
|
||||
totalSize int64,
|
||||
|
||||
filename string,
|
||||
|
||||
userID uint,
|
||||
|
||||
deviceUID string,
|
||||
|
||||
) error {
|
||||
|
||||
// 创建分片存储目录
|
||||
|
||||
chunkDir := filepath.Join(s.config.Upload.Path, "chunks", fileHash)
|
||||
|
||||
if err := os.MkdirAll(chunkDir, 0755); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 保存分片文件
|
||||
|
||||
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("%d", chunkNumber))
|
||||
|
||||
src, err := file.Open()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(chunkPath)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
|
||||
if _, err = io.Copy(dst, src); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 记录分片信息
|
||||
|
||||
chunk := model.UploadChunk{
|
||||
|
||||
FileHash: fileHash,
|
||||
|
||||
ChunkNumber: chunkNumber,
|
||||
|
||||
ChunkSize: file.Size,
|
||||
|
||||
ChunkPath: chunkPath,
|
||||
|
||||
TotalChunks: totalChunks,
|
||||
|
||||
TotalSize: totalSize,
|
||||
|
||||
Filename: filename,
|
||||
|
||||
FileType: strings.ToLower(filepath.Ext(filename)),
|
||||
|
||||
UploadedBy: userID,
|
||||
|
||||
DeviceUID: deviceUID,
|
||||
}
|
||||
|
||||
return s.db.Create(&chunk).Error
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) CheckUploadStatus(fileHash string) (bool, error) {
|
||||
|
||||
var chunks []model.UploadChunk
|
||||
|
||||
if err := s.db.Where("file_hash = ?", fileHash).Find(&chunks).Error; err != nil {
|
||||
|
||||
return false, err
|
||||
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
|
||||
return false, nil
|
||||
|
||||
}
|
||||
|
||||
totalChunks := chunks[0].TotalChunks
|
||||
|
||||
return len(chunks) == totalChunks, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) MergeChunks(fileHash string) (*model.FileUpload, error) {
|
||||
|
||||
var chunks []model.UploadChunk
|
||||
|
||||
if err := s.db.Where("file_hash = ?", fileHash).Find(&chunks).Error; err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
|
||||
return nil, errors.New("未找到文件分片")
|
||||
|
||||
}
|
||||
|
||||
if len(chunks) != chunks[0].TotalChunks {
|
||||
|
||||
return nil, errors.New("文件分片不完整")
|
||||
|
||||
}
|
||||
|
||||
// 按分片序号排序
|
||||
|
||||
sort.Slice(chunks, func(i, j int) bool {
|
||||
|
||||
return chunks[i].ChunkNumber < chunks[j].ChunkNumber
|
||||
|
||||
})
|
||||
|
||||
// 创建最终文件
|
||||
|
||||
finalPath := filepath.Join(s.config.Upload.Path, fmt.Sprintf("%s%s", uuid.New().String(), chunks[0].FileType))
|
||||
|
||||
finalFile, err := os.Create(finalPath)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
defer finalFile.Close()
|
||||
|
||||
// 合并分片
|
||||
|
||||
hash := sha256.New()
|
||||
|
||||
for _, chunk := range chunks {
|
||||
|
||||
chunkFile, err := os.Open(chunk.ChunkPath)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
if _, err = io.Copy(finalFile, chunkFile); err != nil {
|
||||
|
||||
chunkFile.Close()
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
if _, err = io.Copy(hash, chunkFile); err != nil {
|
||||
|
||||
chunkFile.Close()
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
chunkFile.Close()
|
||||
|
||||
os.Remove(chunk.ChunkPath) // 删除已合并的分片
|
||||
|
||||
}
|
||||
|
||||
// 验证文件哈希
|
||||
|
||||
if hex.EncodeToString(hash.Sum(nil)) != fileHash {
|
||||
|
||||
os.Remove(finalPath)
|
||||
|
||||
return nil, errors.New("文件哈希验证失败")
|
||||
|
||||
}
|
||||
|
||||
// 创建文件记录
|
||||
|
||||
upload := &model.FileUpload{
|
||||
|
||||
FileName: chunks[0].Filename,
|
||||
|
||||
FilePath: finalPath,
|
||||
|
||||
FileSize: chunks[0].TotalSize,
|
||||
|
||||
FileType: chunks[0].FileType,
|
||||
|
||||
UploadedBy: chunks[0].UploadedBy,
|
||||
|
||||
DeviceUID: chunks[0].DeviceUID,
|
||||
}
|
||||
|
||||
if err := s.db.Create(upload).Error; err != nil {
|
||||
|
||||
os.Remove(finalPath)
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 清理分片记录
|
||||
|
||||
s.db.Where("file_hash = ?", fileHash).Delete(&model.UploadChunk{})
|
||||
|
||||
os.RemoveAll(filepath.Dir(chunks[0].ChunkPath))
|
||||
|
||||
return upload, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UploadService) CleanupExpiredChunks() error {
|
||||
|
||||
expireTime := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
var expiredChunks []model.UploadChunk
|
||||
|
||||
if err := s.db.Where("completed = ? AND created_at < ?", false, expireTime).Find(&expiredChunks).Error; err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
for _, chunk := range expiredChunks {
|
||||
|
||||
os.Remove(chunk.ChunkPath)
|
||||
|
||||
if len(chunk.ChunkPath) > 0 {
|
||||
|
||||
chunkDir := filepath.Dir(chunk.ChunkPath)
|
||||
|
||||
os.RemoveAll(chunkDir)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return s.db.Unscoped().Where("completed = ? AND created_at < ?", false, expireTime).Delete(&model.UploadChunk{}).Error
|
||||
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"fmt"
|
||||
|
||||
"time"
|
||||
|
||||
"licserver/internal/model"
|
||||
|
||||
"licserver/internal/utils"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
config *utils.Config
|
||||
captchaService *CaptchaService
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, config *utils.Config) *UserService {
|
||||
return &UserService{
|
||||
db: db,
|
||||
config: config,
|
||||
captchaService: NewCaptchaService(db, &config.Email),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) Register(username, password, email, captcha string) error {
|
||||
// 验证验证码
|
||||
if err := s.captchaService.VerifyCaptcha(email, "register", captcha); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
var count int64
|
||||
s.db.Model(&model.User{}).Where("username = ?", username).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("用户名已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
s.db.Model(&model.User{}).Where("email = ?", email).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("邮箱已被注册")
|
||||
}
|
||||
|
||||
// 原有的注册逻辑
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: username,
|
||||
Password: string(hashedPassword),
|
||||
Email: email,
|
||||
Role: "user",
|
||||
}
|
||||
|
||||
return s.db.Create(&user).Error
|
||||
}
|
||||
|
||||
func (s *UserService) Login(username, password string) (string, error) {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", errors.New("用户不存在")
|
||||
}
|
||||
return "", err
|
||||
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return "", errors.New("密码错误")
|
||||
}
|
||||
|
||||
// 生成 JWT token
|
||||
token, err := utils.GenerateToken(user.ID, user.Username, user.Role, &s.config.JWT)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
s.db.Model(&user).Update("last_login", gorm.Expr("CURRENT_TIMESTAMP"))
|
||||
return token, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UserService) GetUserByID(id uint) (*UserProfile, error) {
|
||||
var user model.User
|
||||
if err := s.db.First(&user, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserProfile{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateProfile(userID uint, email string) error {
|
||||
// 检查邮箱是否被其他用户使用
|
||||
var count int64
|
||||
if err := s.db.Model(&model.User{}).Where("email = ? AND id != ?", email, userID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
return s.db.Model(&model.User{}).Where("id = ?", userID).Update("email", email).Error
|
||||
}
|
||||
|
||||
func (s *UserService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
||||
var user model.User
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)); err != nil {
|
||||
return errors.New("旧密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.db.Model(&user).Update("password", string(hashedPassword)).Error
|
||||
}
|
||||
|
||||
func (s *UserService) ResetPassword(email, captcha string) error {
|
||||
// 验证验证码
|
||||
if err := s.captchaService.VerifyCaptcha(email, "reset", captcha); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 原有的重置密码逻辑
|
||||
var user model.User
|
||||
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
return errors.New("邮箱不存在")
|
||||
}
|
||||
|
||||
// 生成重置令牌
|
||||
token, err := utils.GenerateResetToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存重置令牌
|
||||
resetToken := model.PasswordResetToken{
|
||||
UserID: user.ID,
|
||||
Token: token,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
Used: false,
|
||||
}
|
||||
if err := s.db.Create(&resetToken).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送重置邮件
|
||||
emailService := utils.NewEmailService(&s.config.Email)
|
||||
resetLink := fmt.Sprintf("http://localhost:%s/reset-password?token=%s", s.config.Server.Port, token)
|
||||
emailBody := fmt.Sprintf(`
|
||||
<h3>密码重置</h3>
|
||||
<p>您好,%s</p>
|
||||
<p>请点击以下链接重置您的密码:</p>
|
||||
<p><a href="%s">重置密码</a></p>
|
||||
<p>此链接将在24小时后失效。</p>
|
||||
<p>如果您没有请求重置密码,请忽略此邮件。</p>
|
||||
`, user.Username, resetLink)
|
||||
return emailService.SendEmail(user.Email, "密码重置", emailBody)
|
||||
|
||||
}
|
||||
|
||||
func (s *UserService) ValidateResetToken(token string) (*model.User, error) {
|
||||
var resetToken model.PasswordResetToken
|
||||
if err := s.db.Where("token = ? AND used = ? AND expires_at > ?",
|
||||
token, false, time.Now()).First(&resetToken).Error; err != nil {
|
||||
return nil, errors.New("无效或已过期的重置令牌")
|
||||
}
|
||||
|
||||
var user model.User
|
||||
if err := s.db.First(&user, resetToken.UserID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *UserService) ResetPasswordWithToken(token, newPassword string) error {
|
||||
user, err := s.ValidateResetToken(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用事务确保原子性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 标记令牌为已使用
|
||||
if err := tx.Model(&model.PasswordResetToken{}).
|
||||
Where("token = ?", token).
|
||||
Update("used", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserService) SendRegisterCaptcha(email string) error {
|
||||
// 检查邮箱是否已被注册
|
||||
var count int64
|
||||
s.db.Model(&model.User{}).Where("email = ?", email).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("邮箱已被注册")
|
||||
}
|
||||
|
||||
return s.captchaService.SendEmailCaptcha(email, "register")
|
||||
}
|
||||
|
||||
func (s *UserService) SendResetPasswordCaptcha(email string) error {
|
||||
var user model.User
|
||||
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
return errors.New("邮箱不存在")
|
||||
}
|
||||
|
||||
return s.captchaService.SendEmailCaptcha(email, "reset")
|
||||
}
|
||||
|
||||
func (s *UserService) GetCaptchaService() *CaptchaService {
|
||||
return s.captchaService
|
||||
}
|
||||
|
||||
// GetUsers 获取用户列表
|
||||
func (s *UserService) GetUsers(username, role string, page, pageSize int) ([]UserProfile, int64, error) {
|
||||
var users []model.User
|
||||
var total int64
|
||||
var profiles []UserProfile
|
||||
|
||||
query := s.db.Model(&model.User{})
|
||||
|
||||
if username != "" {
|
||||
query = query.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
if role != "" {
|
||||
query = query.Where("role = ?", role)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
query = query.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
if err := query.Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 转换为 UserProfile
|
||||
for _, user := range users {
|
||||
profiles = append(profiles, UserProfile{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
return profiles, total, nil
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户
|
||||
func (s *UserService) CreateUser(username, password, email, role string) error {
|
||||
// 检查用户名是否已存在
|
||||
var count int64
|
||||
s.db.Model(&model.User{}).Where("username = ?", username).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("用户名已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
s.db.Model(&model.User{}).Where("email = ?", email).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("邮箱已被注册")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := model.User{
|
||||
Username: username,
|
||||
Password: string(hashedPassword),
|
||||
Email: email,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
return s.db.Create(&user).Error
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
func (s *UserService) UpdateUser(id uint, email, role string) error {
|
||||
// 检查邮箱是否被其他用户使用
|
||||
var count int64
|
||||
s.db.Model(&model.User{}).Where("email = ? AND id != ?", email, id).Count(&count)
|
||||
if count > 0 {
|
||||
return errors.New("邮箱已被其他用户使用")
|
||||
}
|
||||
|
||||
return s.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"email": email,
|
||||
"role": role,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (s *UserService) DeleteUser(id uint) error {
|
||||
// 检查是否为最后一个管理员
|
||||
var adminCount int64
|
||||
s.db.Model(&model.User{}).Where("role = ?", "admin").Count(&adminCount)
|
||||
|
||||
var user model.User
|
||||
if err := s.db.First(&user, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Role == "admin" && adminCount <= 1 {
|
||||
return errors.New("不能删除最后一个管理员")
|
||||
}
|
||||
|
||||
return s.db.Delete(&model.User{}, id).Error
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/utils"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserService_Register(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
config := utils.TestConfig()
|
||||
userService := NewUserService(db, config)
|
||||
|
||||
// 测试正常注册
|
||||
err := userService.Register("testuser", "password123", "test@example.com", "123456")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var user model.User
|
||||
err = db.Where("username = ?", "testuser").First(&user).Error
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "testuser", user.Username)
|
||||
assert.Equal(t, "test@example.com", user.Email)
|
||||
assert.Equal(t, "user", user.Role)
|
||||
|
||||
// 测试重复用户名
|
||||
err = userService.Register("testuser", "password123", "test2@example.com", "123456")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "用户名已存在")
|
||||
|
||||
// 测试重复邮箱
|
||||
err = userService.Register("testuser2", "password123", "test@example.com", "123456")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "邮箱已被注册")
|
||||
}
|
||||
|
||||
func TestUserService_Login(t *testing.T) {
|
||||
db := utils.TestDB(t)
|
||||
config := utils.TestConfig()
|
||||
userService := NewUserService(db, config)
|
||||
|
||||
// 创建测试用户
|
||||
err := userService.Register("testuser", "password123", "test@example.com", "123456")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 测试正确登录
|
||||
token, err := userService.Login("testuser", "password123")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
// 测试错误密码
|
||||
_, err = userService.Login("testuser", "wrongpassword")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "密码错误")
|
||||
|
||||
// 测试不存在的用户
|
||||
_, err = userService.Login("nonexistent", "password123")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "用户不存在")
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GenerateCaptcha 生成6位数字验证码
|
||||
func GenerateCaptcha() (string, error) {
|
||||
// 生成6位随机数字
|
||||
b := make([]byte, 3)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 将随机字节转换为6位数字
|
||||
num := int(b[0])<<16 | int(b[1])<<8 | int(b[2])
|
||||
return fmt.Sprintf("%06d", num%1000000), nil
|
||||
}
|
||||
|
||||
// GenerateEmailCaptchaContent 生成验证码邮件内容
|
||||
func GenerateEmailCaptchaContent(code, username, action string) string {
|
||||
return fmt.Sprintf(`
|
||||
<h3>验证码</h3>
|
||||
<p>您好,%s</p>
|
||||
<p>您正在进行%s操作,验证码为:</p>
|
||||
<h2 style="color: #1890ff;">%s</h2>
|
||||
<p>验证码有效期为5分钟,请勿泄露给他人。</p>
|
||||
<p>如果这不是您的操作,请忽略此邮件。</p>
|
||||
`, username, action, code)
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
Email EmailConfig
|
||||
Upload UploadConfig
|
||||
Site SiteConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Mode string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
Path string
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
Expire string
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type UploadConfig struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
type SiteConfig struct {
|
||||
Title string `mapstructure:"title"`
|
||||
Description string `mapstructure:"description"`
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
ICP string `mapstructure:"icp"`
|
||||
Copyright string `mapstructure:"copyright"`
|
||||
Logo string `mapstructure:"logo"`
|
||||
Favicon string `mapstructure:"favicon"`
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./config")
|
||||
|
||||
// 读取环境变量
|
||||
viper.AutomaticEnv()
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 环境变量优先
|
||||
if port := os.Getenv("SERVER_PORT"); port != "" {
|
||||
config.Server.Port = port
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"sync"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigVersion struct {
|
||||
Version int `json:"version"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type ConfigWithVersion struct {
|
||||
Config *Config `json:"config"`
|
||||
Version ConfigVersion `json:"version"`
|
||||
}
|
||||
|
||||
var (
|
||||
configMutex sync.RWMutex
|
||||
|
||||
configFile = "config/config.json" // JSON格式更适合动态更新
|
||||
|
||||
)
|
||||
|
||||
// SaveConfig 保存配置到文件
|
||||
|
||||
func SaveConfig(config *Config, updatedBy, comment string) error {
|
||||
|
||||
configMutex.Lock()
|
||||
|
||||
defer configMutex.Unlock()
|
||||
|
||||
// 读取当前版本
|
||||
|
||||
currentVersion := 0
|
||||
|
||||
if existing, err := LoadPersistedConfig(); err == nil {
|
||||
|
||||
currentVersion = existing.Version.Version
|
||||
|
||||
}
|
||||
|
||||
// 创建新的配置版本
|
||||
|
||||
configWithVersion := ConfigWithVersion{
|
||||
|
||||
Config: config,
|
||||
|
||||
Version: ConfigVersion{
|
||||
|
||||
Version: currentVersion + 1,
|
||||
|
||||
UpdatedAt: time.Now(),
|
||||
|
||||
UpdatedBy: updatedBy,
|
||||
|
||||
Comment: comment,
|
||||
},
|
||||
}
|
||||
|
||||
// 确保配置目录存在
|
||||
|
||||
configDir := filepath.Dir(configFile)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 备份旧配置
|
||||
|
||||
if err := backupConfig(); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 将配置转换为JSON
|
||||
|
||||
data, err := json.MarshalIndent(configWithVersion, "", " ")
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
|
||||
return os.WriteFile(configFile, data, 0644)
|
||||
|
||||
}
|
||||
|
||||
// LoadPersistedConfig 加载持久化的配置
|
||||
|
||||
func LoadPersistedConfig() (*ConfigWithVersion, error) {
|
||||
|
||||
configMutex.RLock()
|
||||
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
// 检查配置文件是否存在
|
||||
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
|
||||
// 如果不存在,创建默认配置
|
||||
|
||||
config, err := LoadConfig()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return &ConfigWithVersion{
|
||||
|
||||
Config: config,
|
||||
|
||||
Version: ConfigVersion{
|
||||
|
||||
Version: 1,
|
||||
|
||||
UpdatedAt: time.Now(),
|
||||
|
||||
UpdatedBy: "system",
|
||||
|
||||
Comment: "初始配置",
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
|
||||
data, err := os.ReadFile(configFile)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 解析JSON配置
|
||||
|
||||
var configWithVersion ConfigWithVersion
|
||||
|
||||
if err := json.Unmarshal(data, &configWithVersion); err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return &configWithVersion, nil
|
||||
|
||||
}
|
||||
|
||||
// MergeConfig 合并配置(环境变量优先)
|
||||
|
||||
func MergeConfig(persisted, env *Config) *Config {
|
||||
|
||||
if env.Server.Port != "" {
|
||||
|
||||
persisted.Server.Port = env.Server.Port
|
||||
|
||||
}
|
||||
|
||||
if env.Server.Mode != "" {
|
||||
|
||||
persisted.Server.Mode = env.Server.Mode
|
||||
|
||||
}
|
||||
|
||||
// ... 其他配置项的合并 ...
|
||||
|
||||
return persisted
|
||||
|
||||
}
|
||||
|
||||
// backupConfig 备份配置文件
|
||||
|
||||
func backupConfig() error {
|
||||
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 确保备份目录存在
|
||||
|
||||
backupDir := "config/backups"
|
||||
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 读取当前配置
|
||||
|
||||
data, err := os.ReadFile(configFile)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// 创建备份文件名
|
||||
|
||||
backupFile := filepath.Join(backupDir,
|
||||
|
||||
fmt.Sprintf("config_%s.json", time.Now().Format("20060102150405")))
|
||||
|
||||
// 写入备份文件
|
||||
|
||||
return os.WriteFile(backupFile, data, 0644)
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func InitDB(config *DatabaseConfig) (*gorm.DB, error) {
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(config.Path), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// 自动迁移数据库结构
|
||||
|
||||
err = db.AutoMigrate(
|
||||
|
||||
&model.User{},
|
||||
|
||||
&model.Device{},
|
||||
|
||||
&model.DeviceModel{},
|
||||
|
||||
&model.PasswordResetToken{},
|
||||
|
||||
&model.Captcha{},
|
||||
|
||||
&model.FileUpload{},
|
||||
|
||||
&model.UploadChunk{},
|
||||
|
||||
&model.LicenseCode{},
|
||||
|
||||
&model.LicenseLog{},
|
||||
|
||||
&model.AccessToken{},
|
||||
|
||||
&model.TokenLog{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
return db, nil
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
config *EmailConfig
|
||||
auth smtp.Auth
|
||||
}
|
||||
|
||||
func NewEmailService(config *EmailConfig) *EmailService {
|
||||
auth := smtp.PlainAuth("", config.Username, config.Password, config.Host)
|
||||
return &EmailService{
|
||||
config: config,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EmailService) SendEmail(to, subject, body string) error {
|
||||
addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
|
||||
msg := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||
"Subject: %s\r\n"+
|
||||
"Content-Type: text/html; charset=UTF-8\r\n"+
|
||||
"\r\n"+
|
||||
"%s\r\n", to, subject, body))
|
||||
|
||||
return smtp.SendMail(addr, s.auth, s.config.Username, []string{to}, msg)
|
||||
}
|
||||
|
||||
func GenerateResetToken() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package utils
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// 通用错误
|
||||
ErrInvalidInput = errors.New("无效的输入")
|
||||
ErrNotFound = errors.New("资源不存在")
|
||||
ErrUnauthorized = errors.New("未授权的访问")
|
||||
ErrForbidden = errors.New("禁止访问")
|
||||
|
||||
// 授权相关错误
|
||||
ErrInvalidToken = errors.New("无效的令牌")
|
||||
ErrTokenExpired = errors.New("令牌已过期")
|
||||
ErrInvalidCaptcha = errors.New("无效的验证码")
|
||||
ErrCaptchaExpired = errors.New("验证码已过期")
|
||||
ErrInvalidLicense = errors.New("无效的授权码")
|
||||
ErrLicenseExpired = errors.New("授权码已过期")
|
||||
ErrLicenseUsed = errors.New("授权码已被使用")
|
||||
ErrDeviceNotFound = errors.New("设备不存在")
|
||||
ErrDeviceRegistered = errors.New("设备已注册")
|
||||
)
|
||||
|
||||
// ErrorResponse 统一错误响应结构
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint
|
||||
Username string
|
||||
Role string
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, username, role string, config *JWTConfig) (string, error) {
|
||||
expDuration, err := time.ParseDuration(config.Expire)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(config.Secret))
|
||||
}
|
||||
|
||||
func ParseToken(tokenString string, config *JWTConfig) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(config.Secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
logFile *os.File
|
||||
}
|
||||
|
||||
func NewLogger(logPath string) (*Logger, error) {
|
||||
// 确保日志目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 打开日志文件
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Logger{logFile: file}, nil
|
||||
}
|
||||
|
||||
func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.log("INFO", format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Error(format string, args ...interface{}) {
|
||||
l.log("ERROR", format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||
l.log("DEBUG", format, args...)
|
||||
}
|
||||
|
||||
func (l *Logger) log(level, format string, args ...interface{}) {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
message := fmt.Sprintf(format, args...)
|
||||
logLine := fmt.Sprintf("[%s] [%s] %s\n", timestamp, level, message)
|
||||
l.logFile.WriteString(logLine)
|
||||
}
|
||||
|
||||
func (l *Logger) Close() error {
|
||||
return l.logFile.Close()
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestDB 创建测试数据库连接
|
||||
func TestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 迁移测试表
|
||||
err = db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.Device{},
|
||||
&model.DeviceModel{},
|
||||
&model.LicenseCode{},
|
||||
&model.LicenseLog{},
|
||||
&model.AccessToken{},
|
||||
&model.TokenLog{},
|
||||
&model.Captcha{},
|
||||
&model.PasswordResetToken{},
|
||||
&model.FileUpload{},
|
||||
&model.UploadChunk{},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// TestConfig 创建测试配置
|
||||
func TestConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: "8080",
|
||||
Mode: "test",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: "sqlite3",
|
||||
Path: ":memory:",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "test-secret",
|
||||
Expire: "24h",
|
||||
},
|
||||
Email: EmailConfig{
|
||||
Host: "smtp.example.com",
|
||||
Port: 587,
|
||||
Username: "test@example.com",
|
||||
Password: "test-password",
|
||||
},
|
||||
Upload: UploadConfig{
|
||||
Path: "./test-uploads",
|
||||
},
|
||||
Site: SiteConfig{
|
||||
Title: "Test Site",
|
||||
Description: "Test Description",
|
||||
BaseURL: "http://localhost:8080",
|
||||
ICP: "Test ICP",
|
||||
Copyright: "Test Copyright",
|
||||
},
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 运行所有测试
|
||||
go test -v ./...
|
||||
|
||||
# 运行指定包的测试
|
||||
# go test -v ./internal/service/...
|
||||
|
||||
# 运行指定测试
|
||||
# go test -v ./internal/service -run TestLicenseService
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
|
@ -0,0 +1,121 @@
|
|||
# Set console encoding to UTF-8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# Set error action preference
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Define color output function
|
||||
function Write-ColorOutput($ForegroundColor) {
|
||||
$fc = $host.UI.RawUI.ForegroundColor
|
||||
$host.UI.RawUI.ForegroundColor = $ForegroundColor
|
||||
if ($args) {
|
||||
Write-Output $args
|
||||
}
|
||||
$host.UI.RawUI.ForegroundColor = $fc
|
||||
}
|
||||
|
||||
# Create necessary directories
|
||||
Write-ColorOutput Green "Creating necessary directories..."
|
||||
$directories = @("config", "data", "uploads")
|
||||
foreach ($dir in $directories) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir | Out-Null
|
||||
Write-ColorOutput Yellow "Created directory: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Check if config file exists
|
||||
$configPath = "config/config.yaml"
|
||||
if (-not (Test-Path $configPath)) {
|
||||
Write-ColorOutput Green "Creating default configuration file..."
|
||||
$configContent = @"
|
||||
server:
|
||||
port: 8081
|
||||
mode: debug
|
||||
|
||||
database:
|
||||
type: sqlite3
|
||||
path: ./data/license.db
|
||||
|
||||
jwt:
|
||||
secret: your-secret-key-change-this
|
||||
expire: 24h
|
||||
|
||||
email:
|
||||
host: smtp.example.com
|
||||
port: 587
|
||||
username: your-email@example.com
|
||||
password: your-password
|
||||
|
||||
upload:
|
||||
path: ./uploads
|
||||
|
||||
site:
|
||||
title: "授权验证管理平台"
|
||||
description: "专业的软件授权和设备管理平台"
|
||||
base_url: "http://localhost:8080"
|
||||
icp: "京ICP备XXXXXXXX号-1"
|
||||
copyright: "© 2024 Your Company Name. All rights reserved."
|
||||
logo: "/static/images/logo.png"
|
||||
favicon: "/static/images/favicon.ico"
|
||||
"@
|
||||
$configContent | Out-File -FilePath $configPath -Encoding UTF8
|
||||
Write-ColorOutput Yellow "Created default config file: $configPath"
|
||||
}
|
||||
|
||||
# Compile program
|
||||
Write-ColorOutput Green "Compiling program..."
|
||||
try {
|
||||
go build -o licserver.exe cmd/main.go
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Compilation failed"
|
||||
}
|
||||
Write-ColorOutput Yellow "Compilation successful"
|
||||
}
|
||||
catch {
|
||||
Write-ColorOutput Red "Compilation failed: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Start server
|
||||
Write-ColorOutput Green "Starting server..."
|
||||
try {
|
||||
# Check if port is in use
|
||||
$port = 8081
|
||||
$portInUse = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue
|
||||
if ($portInUse) {
|
||||
throw "Port $port is already in use"
|
||||
}
|
||||
|
||||
# Start server
|
||||
.\licserver.exe
|
||||
|
||||
# Capture Ctrl+C
|
||||
$handler = {
|
||||
Write-ColorOutput Yellow "`nShutting down server..."
|
||||
Stop-Process -Name "licserver" -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
$null = Register-ObjectEvent -InputObject ([Console]) -EventName CancelKeyPress -Action $handler
|
||||
}
|
||||
catch {
|
||||
Write-ColorOutput Red "Startup failed: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 检查并下载 echarts
|
||||
$echartsPath = "web/static/lib/echarts.min.js"
|
||||
if (-not (Test-Path $echartsPath)) {
|
||||
Write-ColorOutput Green "Downloading echarts..."
|
||||
$echartsUrl = "https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"
|
||||
|
||||
try {
|
||||
Invoke-WebRequest -Uri $echartsUrl -OutFile $echartsPath
|
||||
Write-ColorOutput Yellow "Downloaded echarts successfully"
|
||||
}
|
||||
catch {
|
||||
Write-ColorOutput Red "Failed to download echarts: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 确保配置目录存在
|
||||
mkdir -p config
|
||||
mkdir -p data
|
||||
mkdir -p uploads
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if [ ! -f config/config.yaml ]; then
|
||||
echo "创建默认配置文件..."
|
||||
cat > config/config.yaml << EOF
|
||||
server:
|
||||
port: 8080
|
||||
mode: debug
|
||||
|
||||
database:
|
||||
type: sqlite3
|
||||
path: ./data/license.db
|
||||
|
||||
jwt:
|
||||
secret: your-secret-key-change-this
|
||||
expire: 24h
|
||||
|
||||
email:
|
||||
host: smtp.example.com
|
||||
port: 587
|
||||
username: your-email@example.com
|
||||
password: your-password
|
||||
|
||||
upload:
|
||||
path: ./uploads
|
||||
|
||||
site:
|
||||
title: "授权验证管理平台"
|
||||
description: "专业的软件授权和设备管理平台"
|
||||
base_url: "http://localhost:8080"
|
||||
icp: "京ICP备XXXXXXXX号-1"
|
||||
copyright: "© 2024 Your Company Name. All rights reserved."
|
||||
logo: "/static/images/logo.png"
|
||||
favicon: "/static/images/favicon.ico"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 编译并运行
|
||||
echo "编译程序..."
|
||||
go build -o licserver cmd/main.go
|
||||
|
||||
echo "启动服务器..."
|
||||
./licserver
|
|
@ -0,0 +1,298 @@
|
|||
/* 全局样式 */
|
||||
|
||||
.login-body {
|
||||
background-color: #f2f2f2;
|
||||
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background-color: #fff;
|
||||
|
||||
padding: 30px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-box h2 {
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.forget-pwd {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.register {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
width: 100%;
|
||||
|
||||
height: 38px;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 主界面样式 */
|
||||
|
||||
.layui-layout-admin .layui-body {
|
||||
padding: 15px;
|
||||
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.layui-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.layui-card-header {
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 文件上传相关样式 */
|
||||
|
||||
.upload-progress {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
color: #999;
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.upload-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
margin-left: 10px;
|
||||
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.layui-progress {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 上传按钮组样式 */
|
||||
|
||||
.layui-card-header .layui-btn-group {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* 文件列表表格样式 */
|
||||
|
||||
.layui-table {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.layui-table th {
|
||||
font-weight: bold;
|
||||
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
/* 上传进度条样式 */
|
||||
|
||||
.upload-progress-container {
|
||||
margin: 10px 0;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
background-color: #f8f8f8;
|
||||
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.upload-progress-info {
|
||||
margin-top: 5px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 主界面布局样式 */
|
||||
|
||||
.layui-layout-admin .layui-logo {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.layui-layout-admin .layui-logo img {
|
||||
height: 30px;
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.layui-layout-admin .layui-logo span {
|
||||
color: #fff;
|
||||
|
||||
font-size: 18px;
|
||||
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.layui-nav .layui-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 底部样式 */
|
||||
|
||||
.layui-footer {
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
color: #666;
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer-right a {
|
||||
color: #666;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-right a:hover {
|
||||
color: #009688;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
height: 38px;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layui-form-item .captcha-wrapper {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layui-form-item .captcha-input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 主页布局样式 */
|
||||
.layui-layout-admin .layui-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.layui-layout-admin .layui-logo img {
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 15px;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.layadmin-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.layui-breadcrumb {
|
||||
padding: 10px 15px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
|
||||
}
|
||||
|
||||
/* 导航菜单样式 */
|
||||
.layui-nav .layui-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.layui-nav-item a {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.layui-nav-child dd {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* 底部样式 */
|
||||
.layui-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.layui-side {
|
||||
display: none;
|
||||
}
|
||||
.layui-body {
|
||||
left: 0;
|
||||
}
|
||||
.layui-footer {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.big-font {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #009688;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,41 @@
|
|||
layui.use(['layer'], function(){
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 加载统计数据
|
||||
function loadStats() {
|
||||
fetch('/api/dashboard/stats', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
$('#total-devices').text(result.data.total_devices);
|
||||
$('#total-licenses').text(result.data.total_licenses);
|
||||
$('#today-new').text(result.data.today_new);
|
||||
$('#online-devices').text(result.data.online_devices);
|
||||
$('#active-devices').text(result.data.active_devices);
|
||||
$('#expired-devices').text(result.data.expired_devices);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载统计数据失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadStats();
|
||||
|
||||
// 定时刷新(每30秒)
|
||||
setInterval(loadStats, 30000);
|
||||
});
|
|
@ -0,0 +1,198 @@
|
|||
layui.use(['upload', 'table', 'layer', 'form'], function(){
|
||||
var upload = layui.upload;
|
||||
var table = layui.table;
|
||||
var layer = layui.layer;
|
||||
var form = layui.form;
|
||||
var $ = layui.$;
|
||||
|
||||
// 获取设备型号
|
||||
var deviceModel = decodeURIComponent(location.search.match(/model=([^&]+)/)[1]);
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#file-table',
|
||||
url: '/api/uploads/device/' + deviceModel,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'file_name', title: '文件名', width: 200},
|
||||
{field: 'version', title: '版本', width: 100},
|
||||
{field: 'file_type', title: '类型', width: 100, templet: function(d){
|
||||
if(d.is_update) return '<span class="layui-badge layui-bg-blue">更新</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">普通</span>';
|
||||
}},
|
||||
{field: 'file_size', title: '大小', width: 120, templet: function(d){
|
||||
return formatFileSize(d.file_size);
|
||||
}},
|
||||
{field: 'downloads', title: '下载次数', width: 100},
|
||||
{field: 'description', title: '描述'},
|
||||
{field: 'created_at', title: '上传时间', width: 160, templet: function(d){
|
||||
return new Date(d.created_at).toLocaleString();
|
||||
}},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180}
|
||||
]],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// 上传普通文件
|
||||
upload.render({
|
||||
elem: '#uploadFile',
|
||||
url: '/api/uploads',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
data: {
|
||||
device_model: deviceModel,
|
||||
is_update: false
|
||||
},
|
||||
accept: 'file',
|
||||
before: function(obj){
|
||||
layer.load();
|
||||
},
|
||||
done: function(res){
|
||||
layer.closeAll('loading');
|
||||
if(res.error){
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('上传成功');
|
||||
table.reload('file-table');
|
||||
},
|
||||
error: function(){
|
||||
layer.closeAll('loading');
|
||||
layer.msg('上传失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 上传更新文件
|
||||
upload.render({
|
||||
elem: '#uploadUpdate',
|
||||
url: '/api/uploads',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
data: {
|
||||
device_model: deviceModel,
|
||||
is_update: true
|
||||
},
|
||||
accept: 'file',
|
||||
before: function(obj){
|
||||
// 弹出版本信息输入框
|
||||
layer.prompt({
|
||||
formType: 0,
|
||||
title: '请输入版本号',
|
||||
area: ['300px', '150px']
|
||||
}, function(value, index, elem){
|
||||
this.field.version = value;
|
||||
layer.close(index);
|
||||
layer.load();
|
||||
});
|
||||
},
|
||||
done: function(res){
|
||||
layer.closeAll('loading');
|
||||
if(res.error){
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('上传成功');
|
||||
table.reload('file-table');
|
||||
},
|
||||
error: function(){
|
||||
layer.closeAll('loading');
|
||||
layer.msg('上传失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on('toolbar(file-table)', function(obj){
|
||||
var checkStatus = table.checkStatus(obj.config.id);
|
||||
|
||||
switch(obj.event){
|
||||
case 'refresh':
|
||||
table.reload('file-table');
|
||||
break;
|
||||
case 'batchDel':
|
||||
var data = checkStatus.data;
|
||||
if(data.length === 0){
|
||||
layer.msg('请选择要删除的文件');
|
||||
return;
|
||||
}
|
||||
layer.confirm('确定删除选中的文件吗?', function(index){
|
||||
var ids = data.map(item => item.id);
|
||||
Promise.all(ids.map(id =>
|
||||
fetch('/api/uploads/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
}).then(response => response.json())
|
||||
))
|
||||
.then(() => {
|
||||
layer.msg('批量删除成功');
|
||||
table.reload('file-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('批量删除失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 行工具栏事件
|
||||
table.on('tool(file-table)', function(obj){
|
||||
var data = obj.data;
|
||||
|
||||
switch(obj.event){
|
||||
case 'download':
|
||||
window.location.href = '/api/uploads/' + data.id;
|
||||
break;
|
||||
case 'del':
|
||||
layer.confirm('确定删除该文件吗?', function(index){
|
||||
fetch('/api/uploads/' + data.id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.error){
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('删除成功');
|
||||
obj.del();
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('删除失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(size) {
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var index = 0;
|
||||
while(size >= 1024 && index < units.length - 1) {
|
||||
size /= 1024;
|
||||
index++;
|
||||
}
|
||||
return size.toFixed(2) + ' ' + units[index];
|
||||
}
|
||||
});
|
|
@ -0,0 +1,202 @@
|
|||
layui.use(['table', 'form', 'layer'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 加载设备型号列表
|
||||
fetch('/api/devices/models', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.data) {
|
||||
var options = '<option value="">全部</option>';
|
||||
result.data.forEach(function(model) {
|
||||
options += '<option value="' + model.model_name + '">' + model.model_name + '</option>';
|
||||
});
|
||||
$('select[name=device_model]').html(options);
|
||||
form.render('select');
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#device-table',
|
||||
url: '/api/devices/registered', // 已注册设备列表接口
|
||||
headers: undefined,
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'uid', title: '设备UID', width: 180},
|
||||
{field: 'device_model', title: '设备型号', width: 120},
|
||||
{field: 'license_code', title: '授权码', width: 180},
|
||||
{field: 'license_type', title: '授权类型', width: 100, templet: function(d){
|
||||
var types = {
|
||||
'time': '时间授权',
|
||||
'count': '次数授权',
|
||||
'permanent': '永久授权'
|
||||
};
|
||||
return types[d.license_type] || '-';
|
||||
}},
|
||||
{field: 'expire_time', title: '过期时间', width: 160, templet: function(d){
|
||||
return d.expire_time ? new Date(d.expire_time).toLocaleString() : '-';
|
||||
}},
|
||||
{field: 'start_count', title: '启动次数', width: 100},
|
||||
{field: 'status', title: '状态', width: 100, templet: function(d){
|
||||
if(d.status === 'active') return '<span class="layui-badge layui-bg-green">正常</span>';
|
||||
if(d.status === 'expired') return '<span class="layui-badge layui-bg-orange">已过期</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">未激活</span>';
|
||||
}},
|
||||
{field: 'last_active_at', title: '最后活跃', width: 160, templet: function(d){
|
||||
return d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-';
|
||||
}},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 250}
|
||||
]],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on('toolbar(device-table)', function(obj){
|
||||
switch(obj.event){
|
||||
case 'refresh':
|
||||
table.reload('device-table');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 行工具栏事件
|
||||
table.on('tool(device-table)', function(obj){
|
||||
var data = obj.data;
|
||||
|
||||
switch(obj.event){
|
||||
case 'view':
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '设备详情',
|
||||
area: ['600px', '500px'],
|
||||
content: laytpl($('#deviceDetailTpl').html()).render(data)
|
||||
});
|
||||
break;
|
||||
case 'bind':
|
||||
layer.prompt({
|
||||
formType: 0,
|
||||
value: '',
|
||||
title: '请输入授权码',
|
||||
area: ['300px', '150px']
|
||||
}, function(value, index, elem){
|
||||
// 绑定授权码
|
||||
fetch('/api/devices/' + data.uid + '/license', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
license_code: value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('绑定成功');
|
||||
table.reload('device-table');
|
||||
layer.close(index);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('绑定失败:' + error.message);
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'logs':
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: '设备日志',
|
||||
area: ['800px', '600px'],
|
||||
content: '/admin/device-logs?uid=' + data.uid
|
||||
});
|
||||
break;
|
||||
case 'revoke':
|
||||
layer.confirm('确定要撤销该设备的授权吗?', function(index){
|
||||
fetch('/api/devices/' + data.uid + '/license', {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('撤销成功');
|
||||
table.reload('device-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('撤销失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
table.reload('device-table', {
|
||||
where: data.field,
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 导出设备列表
|
||||
$('#export-devices').on('click', function(){
|
||||
var checkStatus = table.checkStatus('device-table');
|
||||
var data = checkStatus.data;
|
||||
if(data.length === 0){
|
||||
layer.msg('请选择要导出的设备');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建CSV内容
|
||||
var csv = '设备UID,设备型号,授权码,授权类型,过期时间,启动次数,状态,最后活跃\n';
|
||||
data.forEach(function(item){
|
||||
csv += [
|
||||
item.uid,
|
||||
item.device_model,
|
||||
item.license_code || '',
|
||||
item.license_type || '',
|
||||
item.expire_time ? new Date(item.expire_time).toLocaleString() : '',
|
||||
item.start_count,
|
||||
item.status,
|
||||
item.last_active_at ? new Date(item.last_active_at).toLocaleString() : ''
|
||||
].join(',') + '\n';
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
var link = document.createElement("a");
|
||||
if (link.download !== undefined) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "devices.csv");
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,223 @@
|
|||
layui.use(['table', 'form', 'layer'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#device-table',
|
||||
url: '/api/devices/models',
|
||||
headers: undefined,
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'model_name', title: '设备型号', width: 180},
|
||||
{field: 'device_type', title: '设备类型', width: 120, templet: function(d){
|
||||
var types = {
|
||||
'software': '软件',
|
||||
'website': '网站',
|
||||
'embedded': '嵌入式设备',
|
||||
'mcu': '单片机设备'
|
||||
};
|
||||
return types[d.device_type] || d.device_type;
|
||||
}},
|
||||
{field: 'company', title: '所属公司', width: 150},
|
||||
{field: 'remark', title: '备注说明'},
|
||||
{field: 'device_count', title: '设备数量', width: 100},
|
||||
{field: 'status', title: '状态', width: 100, templet: function(d){
|
||||
if(d.status === 'active') return '<span class="layui-badge layui-bg-green">已激活</span>';
|
||||
if(d.status === 'expired') return '<span class="layui-badge layui-bg-orange">已过期</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">未激活</span>';
|
||||
}},
|
||||
{field: 'CreatedAt', title: '创建时间', width: 180, templet: function(d){
|
||||
return d.CreatedAt ? new Date(d.CreatedAt).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}) : '';
|
||||
}},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180}
|
||||
]],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on('toolbar(device-table)', function(obj){
|
||||
var checkStatus = table.checkStatus(obj.config.id);
|
||||
|
||||
switch(obj.event){
|
||||
case 'refresh':
|
||||
table.reload('device-table');
|
||||
break;
|
||||
case 'batchDel':
|
||||
var data = checkStatus.data;
|
||||
if(data.length === 0){
|
||||
layer.msg('请选择要删除的设备型号');
|
||||
return;
|
||||
}
|
||||
layer.confirm('确定删除选中的设备型号吗?', function(index){
|
||||
var ids = data.map(item => item.id);
|
||||
fetch('/api/devices/models/batch', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
body: JSON.stringify({ids: ids})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('批量删除成功');
|
||||
table.reload('device-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('批量删除失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 行工具栏事件
|
||||
table.on('tool(device-table)', function(obj){
|
||||
var data = obj.data;
|
||||
|
||||
switch(obj.event){
|
||||
case 'edit':
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '编辑设备型号',
|
||||
area: ['500px', '400px'],
|
||||
content: $('#deviceFormTpl').html(),
|
||||
success: function(){
|
||||
form.val('deviceForm', data);
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'files':
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: '设备文件管理',
|
||||
area: ['900px', '600px'],
|
||||
content: '/admin/device-files?model=' + encodeURIComponent(data.deviceModel)
|
||||
});
|
||||
break;
|
||||
case 'del':
|
||||
layer.confirm('确定删除该设备型号吗?', function(index){
|
||||
fetch('/api/devices/models/' + data.id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('删除成功');
|
||||
obj.del();
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('删除失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
table.reload('device-table', {
|
||||
where: data.field
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 添加设备型号按钮点击事件
|
||||
$('#add-device').on('click', function(){
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '添加设备型号',
|
||||
area: ['500px', '400px'],
|
||||
content: $('#deviceFormTpl').html(),
|
||||
success: function(){
|
||||
// 初始化设备类型选择
|
||||
form.val('deviceForm', {
|
||||
'device_type': 'software' // 设置默认值
|
||||
});
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 设备型号表单提交
|
||||
form.on('submit(deviceSubmit)', function(data){
|
||||
// 如果是编辑模式,确保 id 是数字类型
|
||||
if(data.field.id) {
|
||||
data.field.id = parseInt(data.field.id);
|
||||
}
|
||||
|
||||
// 构造提交数据,使用下划线命名
|
||||
const submitData = {
|
||||
model_name: data.field.model_name,
|
||||
device_type: data.field.device_type,
|
||||
company: data.field.company,
|
||||
remark: data.field.remark,
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
// 如果是编辑模式,添加 ID
|
||||
if(data.field.id) {
|
||||
submitData.id = data.field.id;
|
||||
}
|
||||
|
||||
var url = data.field.id ? '/api/devices/models/' + data.field.id : '/api/devices/models';
|
||||
var method = data.field.id ? 'PUT' : 'POST';
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.closeAll('page');
|
||||
layer.msg(data.field.id ? '更新成功' : '添加成功');
|
||||
table.reload('device-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg((data.field.id ? '更新' : '添加') + '失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,340 @@
|
|||
layui.use(['table', 'form', 'layer'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 自定义验证规则
|
||||
form.verify({
|
||||
min1: function(value) {
|
||||
if (value < 1) {
|
||||
return '必须大于0';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#license-table',
|
||||
url: '/api/licenses',
|
||||
headers: undefined,
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'code', title: '授权码', width: 320, templet: function(d){
|
||||
return '<div class="layui-table-cell laytable-cell-1-code">' +
|
||||
d.code +
|
||||
'<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="copy" style="margin-left:5px;">复制</a>' +
|
||||
'</div>';
|
||||
}},
|
||||
{field: 'license_type', title: '授权类型', width: 100, templet: function(d){
|
||||
var types = {
|
||||
'time': '时间授权',
|
||||
'count': '次数授权',
|
||||
'permanent': '永久授权'
|
||||
};
|
||||
return types[d.license_type.toLowerCase()] || d.license_type;
|
||||
}},
|
||||
{field: 'duration', title: '有效期', width: 150, templet: function(d){
|
||||
if(d.license_type.toLowerCase() === 'time') {
|
||||
let minutes = d.duration;
|
||||
if (minutes >= 525600) {
|
||||
return Math.floor(minutes / 525600) + '年';
|
||||
} else if (minutes >= 43200) {
|
||||
return Math.floor(minutes / 43200) + '月';
|
||||
} else if (minutes >= 1440) {
|
||||
return Math.floor(minutes / 1440) + '天';
|
||||
} else if (minutes >= 60) {
|
||||
return Math.floor(minutes / 60) + '小时';
|
||||
} else {
|
||||
return minutes + '分钟';
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}},
|
||||
{field: 'max_uses', title: '使用次数', width: 100, templet: function(d){
|
||||
if(d.license_type.toLowerCase() === 'count') {
|
||||
return d.max_uses || 0;
|
||||
}
|
||||
return '-';
|
||||
}},
|
||||
{field: 'status', title: '状态', width: 100, templet: function(d){
|
||||
var status = d.status.toLowerCase();
|
||||
if(status === 'unused') return '<span class="layui-badge layui-bg-green">未使用</span>';
|
||||
if(status === 'used') return '<span class="layui-badge layui-bg-gray">已使用</span>';
|
||||
if(status === 'expired') return '<span class="layui-badge layui-bg-orange">已过期</span>';
|
||||
if(status === 'revoked') return '<span class="layui-badge layui-bg-red">已撤销</span>';
|
||||
return '<span class="layui-badge layui-bg-black">未知</span>';
|
||||
}},
|
||||
{field: 'used_by', title: '使用设备', width: 180},
|
||||
{field: 'used_at', title: '使用时间', width: 160, templet: function(d){
|
||||
return d.used_at ? new Date(d.used_at).toLocaleString() : '-';
|
||||
}},
|
||||
{field: 'batch_no', title: '批次号', width: 160},
|
||||
{field: 'remark', title: '备注'},
|
||||
{field: 'bind_count', title: '可绑定次数', width: 100, templet: function(d){
|
||||
if(d.bind_count === -1) return '<span class="layui-badge layui-bg-blue">无限制</span>';
|
||||
if(d.bind_count === 0) return '<span class="layui-badge layui-bg-gray">已用完</span>';
|
||||
return d.bind_count;
|
||||
}},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180, templet: function(d){
|
||||
var btns = [
|
||||
'<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>',
|
||||
'<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>'
|
||||
];
|
||||
// 只有未过期且未撤销的授权码才能撤销
|
||||
if(d.status.toLowerCase() !== 'expired' && d.status.toLowerCase() !== 'revoked') {
|
||||
btns.push('<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>');
|
||||
}
|
||||
return btns.join('');
|
||||
}}
|
||||
]],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.msg,
|
||||
"count": res.count,
|
||||
"data": res.data
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on('toolbar(license-table)', function(obj){
|
||||
var checkStatus = table.checkStatus(obj.config.id);
|
||||
|
||||
switch(obj.event){
|
||||
case 'refresh':
|
||||
table.reload('license-table');
|
||||
break;
|
||||
case 'copySelected':
|
||||
var data = checkStatus.data;
|
||||
if(data.length === 0){
|
||||
layer.msg('请选择要复制的授权码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取授权码并按格式组织
|
||||
var codes = data.map(item => item.code);
|
||||
var copyText = '';
|
||||
|
||||
// 弹出选择框
|
||||
layer.confirm('请选择复制格式', {
|
||||
btn: ['换行分隔', '逗号分隔']
|
||||
}, function(index){
|
||||
// 换行分隔
|
||||
copyText = codes.join('\n');
|
||||
copyToClipboard(copyText);
|
||||
layer.close(index);
|
||||
}, function(index){
|
||||
// 逗号分隔
|
||||
copyText = codes.join(',');
|
||||
copyToClipboard(copyText);
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
case 'batchDel':
|
||||
var data = checkStatus.data;
|
||||
if(data.length === 0){
|
||||
layer.msg('请选择要删除的授权码');
|
||||
return;
|
||||
}
|
||||
layer.confirm('确定撤销选中的授权码吗?', function(index){
|
||||
var codes = data.map(item => item.code);
|
||||
fetch('/api/licenses/batch/revoke', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({codes: codes})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('批量撤销成功');
|
||||
table.reload('license-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('批量撤销失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加复制到剪贴板的函数
|
||||
function copyToClipboard(text) {
|
||||
// 创建临时文本区域
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
// 选择文本
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, 99999); // 兼容移动设备
|
||||
|
||||
try {
|
||||
// 执行复制
|
||||
var successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
layer.msg('复制成功');
|
||||
} else {
|
||||
layer.msg('复制失败,请手动复制');
|
||||
}
|
||||
} catch (err) {
|
||||
layer.msg('复制失败:' + err.message);
|
||||
}
|
||||
|
||||
// 移除临时文本区域
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// 行工具栏事件
|
||||
table.on('tool(license-table)', function(obj){
|
||||
var data = obj.data;
|
||||
|
||||
switch(obj.event){
|
||||
case 'view':
|
||||
layer.alert(JSON.stringify(data, null, 2), {
|
||||
title: '授权码详情'
|
||||
});
|
||||
break;
|
||||
case 'logs':
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: '使用日志',
|
||||
area: ['800px', '600px'],
|
||||
content: '/admin/license-logs?id=' + data.id
|
||||
});
|
||||
break;
|
||||
case 'revoke':
|
||||
layer.confirm('确定撤销该授权码吗?', function(index){
|
||||
fetch('/api/licenses/' + data.code + '/revoke', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('撤销成功');
|
||||
table.reload('license-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('撤销失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
case 'copy':
|
||||
copyToClipboard(data.code);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
// 转换所有字段为小写
|
||||
Object.keys(data.field).forEach(key => {
|
||||
if(data.field[key]) {
|
||||
data.field[key] = data.field[key].toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
table.reload('license-table', {
|
||||
where: data.field,
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 创建授权码按钮点击事件
|
||||
$('#create-license').on('click', function(){
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '生成授权码',
|
||||
area: ['500px', '400px'],
|
||||
content: $('#createLicenseTpl').html(),
|
||||
success: function(){
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听授权类型切换
|
||||
form.on('select(licenseType)', function(data){
|
||||
if(data.value === 'time'){
|
||||
$('#durationItem').show();
|
||||
$('#maxUsesItem').hide();
|
||||
} else if(data.value === 'count'){
|
||||
$('#durationItem').hide();
|
||||
$('#maxUsesItem').show();
|
||||
} else {
|
||||
$('#durationItem').hide();
|
||||
$('#maxUsesItem').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建授权码表单提交
|
||||
form.on('submit(licenseSubmit)', function(data){
|
||||
var field = data.field;
|
||||
|
||||
// 构造请求数据,确保数值类型正确
|
||||
const submitData = {
|
||||
license_type: field.license_type.toLowerCase(),
|
||||
count: parseInt(field.count),
|
||||
remark: field.remark
|
||||
};
|
||||
|
||||
// 根据授权类型处理参数
|
||||
if(field.license_type.toLowerCase() === 'time'){
|
||||
submitData.duration = parseInt(field.duration) * parseInt(field.duration_unit);
|
||||
delete field.max_uses;
|
||||
} else if(field.license_type.toLowerCase() === 'count'){
|
||||
submitData.max_uses = parseInt(field.max_uses);
|
||||
delete field.duration;
|
||||
} else {
|
||||
delete field.duration;
|
||||
delete field.max_uses;
|
||||
}
|
||||
|
||||
fetch('/api/licenses', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.closeAll('page');
|
||||
layer.msg('授权码生成成功');
|
||||
table.reload('license-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('生成失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
layui.use(['form', 'layer'], function(){
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 检查用户是否已登录
|
||||
function checkIfLoggedIn() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
checkIfLoggedIn();
|
||||
|
||||
// 加载验证码
|
||||
function loadCaptcha() {
|
||||
fetch('/api/captcha')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
layer.msg(data.error);
|
||||
return;
|
||||
}
|
||||
// 确保图片数据正确加载
|
||||
if (data.imageBase64) {
|
||||
$('#captchaImg').attr('src', 'data:image/png;base64,' + data.imageBase64);
|
||||
$('input[name=captchaId]').val(data.captchaId);
|
||||
} else {
|
||||
throw new Error('验证码图片数据无效');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('获取验证码失败:' + error.message);
|
||||
// 设置一个默认的错误图片
|
||||
$('#captchaImg').attr('src', '/static/images/captcha-error.png');
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时获取验证码
|
||||
loadCaptcha();
|
||||
|
||||
// 点击验证码图片刷新
|
||||
$('#captchaImg').on('click', function() {
|
||||
loadCaptcha();
|
||||
});
|
||||
|
||||
// 登录表单提交
|
||||
form.on('submit(login)', function(data){
|
||||
var field = data.field;
|
||||
|
||||
// 添加验证码验证
|
||||
if (!field.captcha) {
|
||||
layer.msg('请输入验证码');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!field.captchaId) {
|
||||
layer.msg('验证码已失效,请刷新');
|
||||
loadCaptcha();
|
||||
return false;
|
||||
}
|
||||
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: field.username,
|
||||
password: field.password,
|
||||
captcha: field.captcha,
|
||||
captchaId: field.captchaId
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
loadCaptcha(); // 刷新验证码
|
||||
return;
|
||||
}
|
||||
// 确保 token 被正确设置
|
||||
document.cookie = `token=${result.token}; path=/; secure; samesite=strict`;
|
||||
localStorage.setItem('token', result.token);
|
||||
window.location.href = '/';
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('登录失败:' + error.message);
|
||||
loadCaptcha(); // 刷新验证码
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// 注册账号点击事件
|
||||
$('.register').on('click', function(){
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: '注册账号',
|
||||
area: ['500px', '400px'],
|
||||
content: '/register.html'
|
||||
});
|
||||
});
|
||||
|
||||
// 忘记密码点击事件
|
||||
$('.forget-pwd').on('click', function(){
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: '重置密码',
|
||||
area: ['500px', '300px'],
|
||||
content: '/reset-password.html'
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,190 @@
|
|||
layui.use(["element", "layer"], function () {
|
||||
var element = layui.element;
|
||||
|
||||
var layer = layui.layer;
|
||||
|
||||
var $ = layui.$;
|
||||
|
||||
// 检查认证状态
|
||||
|
||||
function checkAuth() {
|
||||
try {
|
||||
// 从 cookie 中获取 token
|
||||
const token = localStorage.getItem('token');
|
||||
// console.log(token);
|
||||
if (!token) {
|
||||
window.location.href = "/login";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("认证检查失败:", error);
|
||||
window.location.href = "/login";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加通用的 fetch 封装,自动处理认证
|
||||
|
||||
window.authFetch = function (url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
credentials: 'include', // 自动携带 cookie
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
}
|
||||
throw new Error(`请求失败,状态码:${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("请求处理失败:", error);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// 在页面加载时检查认证
|
||||
|
||||
$(document).ready(function () {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
// 加载用户信息
|
||||
|
||||
authFetch("/api/users/profile")
|
||||
.then((user) => {
|
||||
$("#current-user").text(user.username);
|
||||
|
||||
// 根据用户角色显示/隐藏菜单
|
||||
|
||||
if (user.role !== "admin") {
|
||||
$(".admin-only").hide();
|
||||
}
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
layer.msg("加载用户信息失败:" + error.message);
|
||||
});
|
||||
|
||||
// 加载站点配置
|
||||
|
||||
loadSiteConfig();
|
||||
|
||||
// 默认加载 dashboard
|
||||
|
||||
loadPage("/admin/dashboard", "控制台");
|
||||
});
|
||||
|
||||
// 左侧菜单点击事件
|
||||
|
||||
$(".layui-nav-item a").on("click", function () {
|
||||
var url = $(this).data("url");
|
||||
|
||||
if (url) {
|
||||
var title = $(this).text().trim();
|
||||
|
||||
loadPage(url, title);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载页面内容
|
||||
|
||||
function loadPage(url, title) {
|
||||
// 更新面包屑
|
||||
|
||||
updateBreadcrumb(title);
|
||||
|
||||
// 加载页面内容
|
||||
|
||||
$("#content-frame").attr("src", url);
|
||||
|
||||
// 更新选中状态
|
||||
|
||||
$(".layui-nav-item").removeClass("layui-this");
|
||||
|
||||
$(`a[data-url="${url}"]`).parent().addClass("layui-this");
|
||||
}
|
||||
|
||||
// 更新面包屑导航
|
||||
|
||||
function updateBreadcrumb(title) {
|
||||
var html =
|
||||
'<a href="javascript:;">首页</a> <span lay-separator="">/</span> ' +
|
||||
title;
|
||||
|
||||
$(".layui-breadcrumb").html(html);
|
||||
|
||||
element.render("breadcrumb");
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
|
||||
$(".change-password").on("click", function () {
|
||||
layer.open({
|
||||
type: 2,
|
||||
|
||||
title: "修改密码",
|
||||
|
||||
area: ["500px", "300px"],
|
||||
|
||||
content: "/admin/change-password",
|
||||
});
|
||||
});
|
||||
|
||||
// 退出登录
|
||||
|
||||
$(".logout").on("click", function () {
|
||||
layer.confirm("确定要退出登录吗?", function (index) {
|
||||
// 清除 cookie
|
||||
document.cookie = "token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
window.location.href = '/login';
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 加载站点配置
|
||||
|
||||
function loadSiteConfig() {
|
||||
authFetch("/api/site/settings")
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
layer.msg(data.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新页面元素
|
||||
|
||||
document.title = data.title;
|
||||
|
||||
$("#site-title").text(data.title);
|
||||
|
||||
$("#site-description").attr("content", data.description);
|
||||
|
||||
$("#site-favicon").attr("href", data.favicon);
|
||||
|
||||
$("#site-logo").attr("src", data.logo);
|
||||
|
||||
$("#site-name").text(data.title);
|
||||
|
||||
$("#site-copyright").text(data.copyright);
|
||||
|
||||
$("#site-icp").text(data.icp);
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
layer.msg("加载配置失败:" + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 监听子页面消息
|
||||
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'updateBreadcrumb') {
|
||||
updateBreadcrumb(event.data.title);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,209 @@
|
|||
layui.use(['element', 'layer'], function(){
|
||||
var element = layui.element;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 初始化所有图表
|
||||
var cpuChart = echarts.init(document.getElementById('cpu-chart'));
|
||||
var memoryChart = echarts.init(document.getElementById('memory-chart'));
|
||||
var diskChart = echarts.init(document.getElementById('disk-chart'));
|
||||
var networkChart = echarts.init(document.getElementById('network-chart'));
|
||||
|
||||
// 更新系统状态
|
||||
function updateSystemStatus() {
|
||||
fetch('/api/monitor/status', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
layer.msg(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新基础信息
|
||||
$('#uptime').text(formatDuration(Math.floor(data.system.uptime / 1e9))); // 转换纳秒为秒
|
||||
$('#active-users').text(data.system.active_users);
|
||||
$('#total-devices').text(data.system.total_devices);
|
||||
$('#load-avg').text(data.cpu.load_avg.map(v => v.toFixed(2)).join(' '));
|
||||
|
||||
// 更新系统信息
|
||||
$('#hostname').text(data.host.hostname);
|
||||
$('#os').text(data.host.os);
|
||||
$('#platform').text(data.host.platform);
|
||||
$('#kernel').text(data.host.kernel_version);
|
||||
$('#cpu-model').text(data.cpu.model_name);
|
||||
$('#cpu-cores').text(data.cpu.core_count);
|
||||
$('#boot-time').text(new Date(data.host.boot_time).toLocaleString());
|
||||
|
||||
// 更新进程列表
|
||||
var processHtml = '';
|
||||
data.process.list.forEach(function(proc) {
|
||||
processHtml += `
|
||||
<tr>
|
||||
<td>${proc.pid}</td>
|
||||
<td>${proc.name}</td>
|
||||
<td>${proc.cpu.toFixed(1)}%</td>
|
||||
<td>${proc.memory.toFixed(1)}%</td>
|
||||
<td>${formatDuration(Math.floor((Date.now() - proc.created) / 1000))}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#process-list').html(processHtml);
|
||||
$('#total-processes').text(data.process.total);
|
||||
|
||||
// 更新图表
|
||||
updateCPUChart(data.cpu);
|
||||
updateMemoryChart(data.memory);
|
||||
updateDiskChart(data.disk);
|
||||
updateNetworkChart(data.network);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('获取系统状态失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新CPU图表
|
||||
function updateCPUChart(cpu) {
|
||||
var option = {
|
||||
title: { text: 'CPU使用率' },
|
||||
tooltip: { formatter: '{b}: {c}%' },
|
||||
series: [{
|
||||
type: 'gauge',
|
||||
min: 0,
|
||||
max: 100,
|
||||
detail: { formatter: '{value}%' },
|
||||
data: [{ value: cpu.usage.toFixed(1), name: 'CPU' }]
|
||||
}]
|
||||
};
|
||||
cpuChart.setOption(option);
|
||||
}
|
||||
|
||||
// 更新内存图表
|
||||
function updateMemoryChart(memory) {
|
||||
var used = (memory.used / 1024 / 1024 / 1024).toFixed(1);
|
||||
var free = (memory.free / 1024 / 1024 / 1024).toFixed(1);
|
||||
var option = {
|
||||
title: { text: '内存使用情况' },
|
||||
tooltip: { formatter: '{b}: {c}GB ({d}%)' },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
data: [
|
||||
{ value: used, name: '已用内存' },
|
||||
{ value: free, name: '空闲内存' }
|
||||
]
|
||||
}]
|
||||
};
|
||||
memoryChart.setOption(option);
|
||||
}
|
||||
|
||||
// 更新磁盘图表
|
||||
function updateDiskChart(disk) {
|
||||
var option = {
|
||||
title: { text: '磁盘使用情况' },
|
||||
tooltip: {
|
||||
formatter: function(params) {
|
||||
var data = params.data;
|
||||
return `${params.name}<br/>
|
||||
总空间: ${formatSize(data.total)}<br/>
|
||||
已用空间: ${formatSize(data.used)}<br/>
|
||||
剩余空间: ${formatSize(data.free)}<br/>
|
||||
使用率: ${data.usage_rate.toFixed(1)}%`;
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
data: disk.partitions.map(p => ({
|
||||
name: p.mountpoint,
|
||||
value: p.usage_rate,
|
||||
total: p.total,
|
||||
used: p.used,
|
||||
free: p.free,
|
||||
usage_rate: p.usage_rate
|
||||
}))
|
||||
}]
|
||||
};
|
||||
diskChart.setOption(option);
|
||||
}
|
||||
|
||||
// 更新网络图表
|
||||
function updateNetworkChart(network) {
|
||||
var option = {
|
||||
title: { text: '网络流量' },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
return params.map(p =>
|
||||
`${p.seriesName}: ${formatSize(p.value)}/s`
|
||||
).join('<br/>');
|
||||
}
|
||||
},
|
||||
legend: { data: ['发送', '接收'] },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: network.interfaces.map(i => i.name)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return formatSize(value) + '/s';
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '发送',
|
||||
type: 'bar',
|
||||
data: network.interfaces.map(i => i.bytes_sent)
|
||||
}, {
|
||||
name: '接收',
|
||||
type: 'bar',
|
||||
data: network.interfaces.map(i => i.bytes_recv)
|
||||
}]
|
||||
};
|
||||
networkChart.setOption(option);
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var k = 1024;
|
||||
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatDuration(seconds) {
|
||||
var days = Math.floor(seconds / 86400);
|
||||
var hours = Math.floor((seconds % 86400) / 3600);
|
||||
var minutes = Math.floor((seconds % 3600) / 60);
|
||||
var parts = [];
|
||||
if (days > 0) parts.push(days + '天');
|
||||
if (hours > 0) parts.push(hours + '小时');
|
||||
if (minutes > 0) parts.push(minutes + '分钟');
|
||||
return parts.join(' ') || '0分钟';
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
updateSystemStatus();
|
||||
|
||||
// 定时更新
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
window.onresize = function(){
|
||||
cpuChart.resize();
|
||||
memoryChart.resize();
|
||||
diskChart.resize();
|
||||
networkChart.resize();
|
||||
};
|
||||
});
|
|
@ -0,0 +1,142 @@
|
|||
layui.use(['form', 'upload', 'layer'], function(){
|
||||
var form = layui.form;
|
||||
var upload = layui.upload;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 加载当前配置
|
||||
fetch('/api/site/settings', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
layer.msg(data.error);
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
// 填充表单数据
|
||||
form.val('siteSettingsForm', {
|
||||
'title': data.title,
|
||||
'description': data.description,
|
||||
'baseUrl': data.base_url,
|
||||
'icp': data.icp,
|
||||
'copyright': data.copyright,
|
||||
'logo': data.logo,
|
||||
'favicon': data.favicon
|
||||
});
|
||||
|
||||
// 显示当前图片
|
||||
if (data.logo) {
|
||||
$('#currentLogo').attr('src', data.logo).show();
|
||||
}
|
||||
if (data.favicon) {
|
||||
$('#currentFavicon').attr('src', data.favicon).show();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载配置失败:' + error.message);
|
||||
});
|
||||
|
||||
// 上传Logo
|
||||
upload.render({
|
||||
elem: '#uploadLogo',
|
||||
url: '/api/uploads/site',
|
||||
accept: 'images',
|
||||
acceptMime: 'image/*',
|
||||
field: 'file',
|
||||
before: function() {
|
||||
layer.load();
|
||||
},
|
||||
done: function(res) {
|
||||
layer.closeAll('loading');
|
||||
if (res.error) {
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
$('input[name=logo]').val(res.url);
|
||||
$('#currentLogo').attr('src', res.url).show();
|
||||
layer.msg('Logo上传成功');
|
||||
},
|
||||
error: function() {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('上传失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 上传Favicon
|
||||
upload.render({
|
||||
elem: '#uploadFavicon',
|
||||
url: '/api/uploads/site',
|
||||
accept: 'images',
|
||||
acceptMime: 'image/*',
|
||||
field: 'file',
|
||||
before: function() {
|
||||
layer.load();
|
||||
},
|
||||
done: function(res) {
|
||||
layer.closeAll('loading');
|
||||
if (res.error) {
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
$('input[name=favicon]').val(res.url);
|
||||
$('#currentFavicon').attr('src', res.url).show();
|
||||
layer.msg('Favicon上传成功');
|
||||
},
|
||||
error: function() {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('上传失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
form.on('submit(siteSubmit)', function(data){
|
||||
// 转换字段名以匹配后端
|
||||
const submitData = {
|
||||
title: data.field.title,
|
||||
description: data.field.description,
|
||||
base_url: data.field.baseUrl,
|
||||
icp: data.field.icp,
|
||||
copyright: data.field.copyright,
|
||||
logo: data.field.logo,
|
||||
favicon: data.field.favicon
|
||||
};
|
||||
|
||||
fetch('/api/site/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('保存成功');
|
||||
// 刷新父页面
|
||||
parent.window.location.reload();
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('保存失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,214 @@
|
|||
layui.use(["table", "form", "layer"], function () {
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: "#token-table",
|
||||
url: "/api/tokens",
|
||||
headers: [
|
||||
{
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
},
|
||||
],
|
||||
toolbar: "#tableToolbar",
|
||||
defaultToolbar: ["filter", "exports", "print"],
|
||||
cols: [
|
||||
[
|
||||
{ type: "checkbox" },
|
||||
{ field: "token", title: "访问令牌", width: 320 },
|
||||
{ field: "deviceUID", title: "设备UID", width: 180 },
|
||||
{ field: "type", title: "令牌类型", width: 100 },
|
||||
{
|
||||
field: "status",
|
||||
title: "状态",
|
||||
width: 100,
|
||||
templet: function (d) {
|
||||
if (d.status === "active")
|
||||
return '<span class="layui-badge layui-bg-green">有效</span>';
|
||||
if (d.status === "revoked")
|
||||
return '<span class="layui-badge layui-bg-gray">已撤销</span>';
|
||||
return '<span class="layui-badge layui-bg-orange">已过期</span>';
|
||||
},
|
||||
},
|
||||
{ field: "expireTime", title: "过期时间", width: 160 },
|
||||
{ field: "lastUsed", title: "最后使用", width: 160 },
|
||||
{ field: "usageCount", title: "使用次数", width: 100 },
|
||||
{ field: "ipList", title: "IP限制", width: 200 },
|
||||
{ fixed: "right", title: "操作", toolbar: "#tableRowBar", width: 180 },
|
||||
],
|
||||
],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on("toolbar(token-table)", function (obj) {
|
||||
var checkStatus = table.checkStatus(obj.config.id);
|
||||
|
||||
switch (obj.event) {
|
||||
case "refresh":
|
||||
table.reload("token-table");
|
||||
break;
|
||||
case "batchRevoke":
|
||||
var data = checkStatus.data;
|
||||
if (data.length === 0) {
|
||||
layer.msg("请选择要撤销的令牌");
|
||||
return;
|
||||
}
|
||||
layer.confirm("确定撤销选中的令牌吗?", function (index) {
|
||||
var tokens = data.map((item) => item.token);
|
||||
// 执行批量撤销
|
||||
Promise.all(
|
||||
tokens.map((token) =>
|
||||
fetch("/api/tokens/" + token, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
},
|
||||
}).then((response) => response.json())
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
layer.msg("批量撤销成功");
|
||||
table.reload("token-table");
|
||||
})
|
||||
.catch((error) => {
|
||||
layer.msg("批量撤销失败:" + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 行工具栏事件
|
||||
table.on("tool(token-table)", function (obj) {
|
||||
var data = obj.data;
|
||||
|
||||
switch (obj.event) {
|
||||
case "view":
|
||||
layer.alert(JSON.stringify(data, null, 2), {
|
||||
title: "令牌详情",
|
||||
});
|
||||
break;
|
||||
case "logs":
|
||||
layer.open({
|
||||
type: 2,
|
||||
title: "令牌使用日志",
|
||||
area: ["800px", "600px"],
|
||||
content: "/admin/token-logs.html?id=" + data.id,
|
||||
});
|
||||
break;
|
||||
case "revoke":
|
||||
layer.confirm("确定撤销该令牌吗?", function (index) {
|
||||
fetch("/api/tokens/" + data.token, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg("撤销成功");
|
||||
obj.update({
|
||||
status: "revoked",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
layer.msg("撤销失败:" + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单提交
|
||||
form.on("submit(search)", function (data) {
|
||||
table.reload("token-table", {
|
||||
where: data.field,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 创建令牌按钮点击事件
|
||||
$("#create-token").on("click", function () {
|
||||
// 加载设备列表
|
||||
fetch("/api/devices", {
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
var devices = result.data;
|
||||
var options = devices
|
||||
.map(
|
||||
(device) =>
|
||||
`<option value="${device.uid}">${device.uid} (${device.deviceModel})</option>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: "创建访问令牌",
|
||||
area: ["500px", "400px"],
|
||||
content: $("#createTokenTpl").html(),
|
||||
success: function () {
|
||||
$("select[name=device_uid]").append(options);
|
||||
form.render("select");
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
layer.msg("加载设备列表失败:" + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
// 创建令牌表单提交
|
||||
form.on("submit(tokenSubmit)", function (data) {
|
||||
var ipList = data.field.ip_list.split(/[,,\s]+/).filter((ip) => ip);
|
||||
|
||||
fetch("/api/tokens", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
device_uid: data.field.device_uid,
|
||||
token_type: data.field.token_type,
|
||||
expire_days: parseInt(data.field.expire_days),
|
||||
ip_list: ipList,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.closeAll("page");
|
||||
layer.msg("创建成功");
|
||||
table.reload("token-table");
|
||||
})
|
||||
.catch((error) => {
|
||||
layer.msg("创建失败:" + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,200 @@
|
|||
layui.use(['upload', 'element', 'layer'], function(){
|
||||
var upload = layui.upload;
|
||||
var element = layui.element;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 普通文件上传
|
||||
upload.render({
|
||||
elem: '#fileUpload',
|
||||
url: '/api/uploads',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
data: {
|
||||
device_uid: function() {
|
||||
return $('#deviceUID').val();
|
||||
},
|
||||
description: function() {
|
||||
return $('#description').val();
|
||||
}
|
||||
},
|
||||
accept: 'file',
|
||||
before: function(obj) {
|
||||
layer.load();
|
||||
},
|
||||
done: function(res) {
|
||||
layer.closeAll('loading');
|
||||
if (res.error) {
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('上传成功');
|
||||
// 刷新文件列表
|
||||
loadFileList();
|
||||
},
|
||||
error: function() {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('上传失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 分片上传
|
||||
upload.render({
|
||||
elem: '#chunkUpload',
|
||||
url: '/api/uploads/chunk',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
data: {
|
||||
deviceUID: function() {
|
||||
return $('#deviceUID').val();
|
||||
}
|
||||
},
|
||||
accept: 'file',
|
||||
size: 1024 * 1024 * 2, // 每片2MB
|
||||
chunked: true,
|
||||
chunkSize: 1024 * 1024 * 2,
|
||||
before: function(obj) {
|
||||
layer.load();
|
||||
},
|
||||
progress: function(n) {
|
||||
var percent = n + '%';
|
||||
element.progress('uploadProgress', percent);
|
||||
},
|
||||
done: function(res) {
|
||||
layer.closeAll('loading');
|
||||
if (res.error) {
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
// 如果所有分片都上传完成,开始合并
|
||||
if (res.completed) {
|
||||
mergeChunks(res.fileHash);
|
||||
} else {
|
||||
layer.msg('分片上传成功');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
layer.closeAll('loading');
|
||||
layer.msg('上传失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 合并分片
|
||||
function mergeChunks(fileHash) {
|
||||
fetch('/api/uploads/merge', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: 'fileHash=' + fileHash
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('文件合并成功');
|
||||
// 刷新文件列表
|
||||
loadFileList();
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('文件合并失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载文件列表
|
||||
function loadFileList() {
|
||||
var deviceUID = $('#deviceUID').val();
|
||||
if (!deviceUID) return;
|
||||
|
||||
fetch('/api/uploads/device/' + deviceUID, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
renderFileList(result);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载文件列表失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染文件列表
|
||||
function renderFileList(files) {
|
||||
var html = '';
|
||||
files.forEach(file => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${file.fileName}</td>
|
||||
<td>${formatFileSize(file.fileSize)}</td>
|
||||
<td>${file.fileType}</td>
|
||||
<td>${formatTime(file.createdAt)}</td>
|
||||
<td>
|
||||
<a href="javascript:;" class="layui-btn layui-btn-xs" onclick="downloadFile(${file.id})">下载</a>
|
||||
<a href="javascript:;" class="layui-btn layui-btn-danger layui-btn-xs" onclick="deleteFile(${file.id})">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#fileList').html(html);
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(size) {
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var index = 0;
|
||||
while (size >= 1024 && index < units.length - 1) {
|
||||
size /= 1024;
|
||||
index++;
|
||||
}
|
||||
return size.toFixed(2) + ' ' + units[index];
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time) {
|
||||
return new Date(time).toLocaleString();
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
window.downloadFile = function(id) {
|
||||
window.location.href = '/api/uploads/' + id;
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
window.deleteFile = function(id) {
|
||||
layer.confirm('确定删除该文件吗?', function(index) {
|
||||
fetch('/api/uploads/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('删除成功');
|
||||
loadFileList();
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('删除失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始加载文件列表
|
||||
loadFileList();
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
layui.use(['table', 'form', 'layer'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#user-table',
|
||||
url: '/api/users',
|
||||
headers: undefined,
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'username', title: '用户名', width: 150},
|
||||
{field: 'email', title: '邮箱', width: 200},
|
||||
{field: 'role', title: '角色', width: 100, templet: '#roleTpl'},
|
||||
{field: 'lastLogin', title: '最后登录', width: 160},
|
||||
{field: 'createdAt', title: '创建时间', width: 160},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 150}
|
||||
]],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// 表格工具栏事件
|
||||
table.on('toolbar(user-table)', function(obj){
|
||||
switch(obj.event){
|
||||
case 'refresh':
|
||||
table.reload('user-table');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 行工具栏事件
|
||||
table.on('tool(user-table)', function(obj){
|
||||
var data = obj.data;
|
||||
|
||||
switch(obj.event){
|
||||
case 'edit':
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '编辑用户',
|
||||
area: ['500px', '400px'],
|
||||
content: $('#userFormTpl').html(),
|
||||
success: function(){
|
||||
// 填充表单数据
|
||||
form.val('userForm', {
|
||||
'id': data.id,
|
||||
'username': data.username,
|
||||
'email': data.email,
|
||||
'role': data.role
|
||||
});
|
||||
// 编辑时密码可选
|
||||
$('input[name=password]').removeAttr('required').removeAttr('lay-verify');
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'del':
|
||||
layer.confirm('确定删除该用户吗?', function(index){
|
||||
fetch('/api/users/' + data.id, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('删除成功');
|
||||
obj.del();
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('删除失败:' + error.message);
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
table.reload('user-table', {
|
||||
where: data.field
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 添加用户按钮点击事件
|
||||
$('#add-user').on('click', function(){
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '添加用户',
|
||||
area: ['500px', '400px'],
|
||||
content: $('#userFormTpl').html(),
|
||||
success: function(){
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 用户表单提交
|
||||
form.on('submit(userSubmit)', function(data){
|
||||
var url = data.field.id ? '/api/users/' + data.field.id : '/api/users';
|
||||
var method = data.field.id ? 'PUT' : 'POST';
|
||||
|
||||
// 如果是编辑且没有输入密码,则删除密码字段
|
||||
if (data.field.id && !data.field.password) {
|
||||
delete data.field.password;
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data.field)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.closeAll('page');
|
||||
layer.msg(data.field.id ? '更新成功' : '添加成功');
|
||||
table.reload('user-table');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg((data.field.id ? '更新' : '添加') + '失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 322 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,103 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>修改密码</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="changePasswordForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">原密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="old_password" required lay-verify="required"
|
||||
placeholder="请输入原密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">新密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="newPassword" required lay-verify="required|password"
|
||||
placeholder="请输入新密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">确认密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="confirmPassword" required lay-verify="required|confirmPassword"
|
||||
placeholder="请再次输入新密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="passwordSubmit">修改</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'layer'], function(){
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
|
||||
// 自定义验证规则
|
||||
form.verify({
|
||||
password: [
|
||||
/^[\S]{6,12}$/,
|
||||
'密码必须6到12位,且不能出现空格'
|
||||
],
|
||||
confirmPassword: function(value) {
|
||||
var password = $('input[name=newPassword]').val();
|
||||
if (value !== password) {
|
||||
return '两次输入的密码不一致';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
form.on('submit(passwordSubmit)', function(data){
|
||||
fetch('/api/users/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
old_password: data.field.oldPassword,
|
||||
new_password: data.field.newPassword
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('密码修改成功,请重新登录');
|
||||
setTimeout(function(){
|
||||
localStorage.removeItem('token');
|
||||
top.location.href = '/login.html';
|
||||
}, 1500);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('修改失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>控制台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">设备总数</div>
|
||||
<div class="layui-card-body big-font" id="total-devices">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">授权码总数</div>
|
||||
<div class="layui-card-body big-font" id="total-licenses">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">今日新增</div>
|
||||
<div class="layui-card-body big-font" id="today-new">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">在线设备</div>
|
||||
<div class="layui-card-body big-font" id="online-devices">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">激活设备</div>
|
||||
<div class="layui-card-body big-font" id="active-devices">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">过期设备</div>
|
||||
<div class="layui-card-body big-font" id="expired-devices">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,119 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>设备文件管理</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>设备文件管理</span>
|
||||
<div class="layui-btn-group">
|
||||
<button class="layui-btn layui-btn-sm" id="uploadFile">
|
||||
<i class="layui-icon"></i>上传文件
|
||||
</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-normal" id="uploadUpdate">
|
||||
<i class="layui-icon"></i>上传更新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备型号</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="deviceModel" lay-search>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">文件类型</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="fileType">
|
||||
<option value="">全部</option>
|
||||
<option value="update">更新文件</option>
|
||||
<option value="normal">普通文件</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文件名</th>
|
||||
<th>设备型号</th>
|
||||
<th>版本</th>
|
||||
<th>类型</th>
|
||||
<th>大小</th>
|
||||
<th>下载次数</th>
|
||||
<th>上传时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fileList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传表单模板 -->
|
||||
<script type="text/html" id="uploadFormTpl">
|
||||
<form class="layui-form" style="padding: 20px;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">设备型号</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="deviceModel" lay-verify="required" lay-search>
|
||||
<option value="">请选择设备型号</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">文件版本</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="version" required lay-verify="required"
|
||||
placeholder="请输入文件版本" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">文件描述</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="description" placeholder="请输入文件描述"
|
||||
class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item" id="updateOptions" style="display:none;">
|
||||
<label class="layui-form-label">更新选项</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="forceUpdate" title="强制更新">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">选择文件</label>
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn" id="selectFile">
|
||||
<i class="layui-icon"></i>选择文件
|
||||
</button>
|
||||
<div class="layui-inline layui-word-aux" id="selectedFile"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/upload.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>设备授权管理</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>设备授权管理</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备UID</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="uid" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备型号</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="device_model" lay-search>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">授权状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="active">正常</option>
|
||||
<option value="expired">已过期</option>
|
||||
<option value="inactive">未激活</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="export-devices">
|
||||
<i class="layui-icon"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table id="device-table" lay-filter="device-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格工具栏模板 -->
|
||||
<script type="text/html" id="tableToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="refresh">
|
||||
<i class="layui-icon"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 行工具栏模板 -->
|
||||
<script type="text/html" id="tableRowBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="bind">绑定授权</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>
|
||||
{{# if(d.status !== 'expired'){ }}
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<!-- 设备详情模板 -->
|
||||
<script type="text/html" id="deviceDetailTpl">
|
||||
<div style="padding: 20px;">
|
||||
<table class="layui-table" lay-skin="line">
|
||||
<colgroup>
|
||||
<col width="150">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>设备UID</td>
|
||||
<td>{{ d.uid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>设备型号</td>
|
||||
<td>{{ d.device_model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权码</td>
|
||||
<td>{{ d.license_code || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权类型</td>
|
||||
<td>{{ d.license_type || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>过期时间</td>
|
||||
<td>{{ d.expire_time ? new Date(d.expire_time).toLocaleString() : '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>启动次数</td>
|
||||
<td>{{ d.start_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最后活跃</td>
|
||||
<td>{{ d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>注册时间</td>
|
||||
<td>{{ new Date(d.register_time).toLocaleString() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/device-license.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,213 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>设备管理</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css" />
|
||||
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>设备型号管理</span>
|
||||
|
||||
<button
|
||||
class="layui-btn layui-btn-sm layui-btn-normal"
|
||||
id="add-device"
|
||||
>
|
||||
<i class="layui-icon"></i> 添加设备型号
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备型号</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<input
|
||||
type="text"
|
||||
name="deviceModel"
|
||||
autocomplete="off"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备类型</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<select name="deviceType">
|
||||
<option value="">全部</option>
|
||||
|
||||
<option value="软件">软件</option>
|
||||
|
||||
<option value="网站">网站</option>
|
||||
|
||||
<option value="嵌入式设备">嵌入式设备</option>
|
||||
|
||||
<option value="单片机设备">单片机设备</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">所属公司</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
autocomplete="off"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="layui-btn layui-btn-primary"
|
||||
id="export-devices"
|
||||
>
|
||||
<i class="layui-icon"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
|
||||
<table id="device-table" lay-filter="device-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格工具栏模板 -->
|
||||
|
||||
<script type="text/html" id="tableToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="refresh">
|
||||
<i class="layui-icon"></i> 刷新
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="layui-btn layui-btn-sm layui-btn-danger"
|
||||
lay-event="batchDel"
|
||||
>
|
||||
<i class="layui-icon"></i> 批量删除
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 行工具栏模板 -->
|
||||
|
||||
<script type="text/html" id="tableRowBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="files"
|
||||
>文件</a
|
||||
>
|
||||
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del"
|
||||
>删除</a
|
||||
>
|
||||
</script>
|
||||
|
||||
<!-- 设备表单模板 -->
|
||||
|
||||
<script type="text/html" id="deviceFormTpl">
|
||||
<form class="layui-form" style="padding: 20px;" lay-filter="deviceForm">
|
||||
<input type="hidden" name="id" />
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">设备型号</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<input
|
||||
type="text"
|
||||
name="model_name"
|
||||
required
|
||||
lay-verify="required"
|
||||
placeholder="请输入设备型号"
|
||||
autocomplete="off"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">设备类型</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<select name="device_type" required lay-verify="required">
|
||||
<option value="software">软件</option>
|
||||
|
||||
<option value="website">网站</option>
|
||||
|
||||
<option value="embedded">嵌入式设备</option>
|
||||
|
||||
<option value="mcu">单片机设备</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">所属公司</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
placeholder="请输入所属公司"
|
||||
autocomplete="off"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注说明</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<textarea
|
||||
name="remark"
|
||||
placeholder="请输入备注说明"
|
||||
class="layui-textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="deviceSubmit">
|
||||
提交
|
||||
</button>
|
||||
|
||||
<button type="reset" class="layui-btn layui-btn-primary">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
|
||||
<script src="/static/js/devices.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,172 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>授权码操作日志</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>授权码操作日志</span>
|
||||
<div class="layui-btn-group">
|
||||
<button class="layui-btn layui-btn-sm" id="refresh-logs">
|
||||
<i class="layui-icon"></i> 刷新
|
||||
</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-normal" id="export-logs">
|
||||
<i class="layui-icon"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">操作类型</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="action">
|
||||
<option value="">全部</option>
|
||||
<option value="create">创建</option>
|
||||
<option value="use">使用</option>
|
||||
<option value="verify">验证</option>
|
||||
<option value="revoke">撤销</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="success">成功</option>
|
||||
<option value="failed">失败</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备UID</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="device_uid" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table id="log-table" lay-filter="log-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作类型模板 -->
|
||||
<script type="text/html" id="actionTpl">
|
||||
{{# var types = {
|
||||
'create': '<span class="layui-badge layui-bg-blue">创建</span>',
|
||||
'use': '<span class="layui-badge layui-bg-green">使用</span>',
|
||||
'verify': '<span class="layui-badge layui-bg-orange">验证</span>',
|
||||
'revoke': '<span class="layui-badge layui-bg-red">撤销</span>'
|
||||
}; }}
|
||||
{{# return types[d.action] || d.action; }}
|
||||
</script>
|
||||
|
||||
<!-- 状态模板 -->
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status === 'success'){ }}
|
||||
<span class="layui-badge layui-bg-green">成功</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-red">失败</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['table', 'layer', 'form'], function(){
|
||||
var table = layui.table;
|
||||
var layer = layui.layer;
|
||||
var form = layui.form;
|
||||
var $ = layui.$;
|
||||
|
||||
// 获取授权码ID
|
||||
var licenseId = location.search.match(/id=(\d+)/)[1];
|
||||
|
||||
// 初始化表格
|
||||
var tableIns = table.render({
|
||||
elem: '#log-table',
|
||||
url: '/api/licenses/' + licenseId + '/logs',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
cols: [[
|
||||
{field: 'action', title: '操作类型', width: 100, templet: '#actionTpl', sort: true},
|
||||
{field: 'device_uid', title: '设备UID', width: 180},
|
||||
{field: 'ip', title: 'IP地址', width: 150},
|
||||
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
|
||||
{field: 'message', title: '详细信息'},
|
||||
{field: 'created_at', title: '操作时间', width: 180, sort: true, templet: function(d){
|
||||
return new Date(d.created_at).toLocaleString();
|
||||
}}
|
||||
]],
|
||||
page: true,
|
||||
parseData: function(res) {
|
||||
if (res.code === 401) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
tableIns.reload({
|
||||
where: data.field,
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 刷新按钮点击事件
|
||||
$('#refresh-logs').on('click', function(){
|
||||
tableIns.reload();
|
||||
});
|
||||
|
||||
// 导出按钮点击事件
|
||||
$('#export-logs').on('click', function(){
|
||||
var loadIndex = layer.load(2);
|
||||
fetch('/api/licenses/' + licenseId + '/logs?export=1', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
layer.close(loadIndex);
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'license_logs_' + licenseId + '_' +
|
||||
new Date().toISOString().slice(0,19).replace(/[-:]/g, '') + '.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.close(loadIndex);
|
||||
layer.msg('导出失败:' + error.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,263 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>授权码管理</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css" />
|
||||
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>授权码管理</span>
|
||||
|
||||
<button
|
||||
class="layui-btn layui-btn-sm layui-btn-normal"
|
||||
id="create-license"
|
||||
>
|
||||
<i class="layui-icon"></i> 生成授权码
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">授权码</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
autocomplete="off"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">授权类型</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<select name="license_type">
|
||||
<option value="">全部</option>
|
||||
|
||||
<option value="time">时间授权</option>
|
||||
|
||||
<option value="count">次数授权</option>
|
||||
|
||||
<option value="permanent">永久授权</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
|
||||
<option value="unused">未使用</option>
|
||||
|
||||
<option value="used">已使用</option>
|
||||
|
||||
<option value="expired">已过期</option>
|
||||
|
||||
<option value="revoked">已撤销</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">批次号</label>
|
||||
|
||||
<div class="layui-input-inline">
|
||||
<input
|
||||
type="text"
|
||||
name="batch_no"
|
||||
autocomplete="off"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="layui-btn layui-btn-primary"
|
||||
id="export-licenses"
|
||||
>
|
||||
<i class="layui-icon"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
|
||||
<table id="license-table" lay-filter="license-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layui 表格工具栏模板 -->
|
||||
|
||||
<script type="text/html" id="tableToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="refresh">
|
||||
<i class="layui-icon"></i> 刷新
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="layui-btn layui-btn-sm layui-btn-normal"
|
||||
lay-event="copySelected"
|
||||
>
|
||||
<i class="layui-icon"></i> 复制选中
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="layui-btn layui-btn-sm layui-btn-danger"
|
||||
lay-event="batchDel"
|
||||
>
|
||||
<i class="layui-icon"></i> 批量删除
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 行工具栏模板 -->
|
||||
|
||||
<script id="tableRowBar" type="text/html">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>
|
||||
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>
|
||||
|
||||
{{# if(d.status === 'unused'){ }}
|
||||
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke"
|
||||
>撤销</a
|
||||
>
|
||||
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
|
||||
<script src="/static/js/licenses.js"></script>
|
||||
|
||||
<!-- 在页面底部添加创建授权码表单模板 -->
|
||||
|
||||
<script type="text/html" id="createLicenseTpl">
|
||||
<form class="layui-form" style="padding: 20px;" lay-filter="licenseForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">授权类型</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<select
|
||||
name="license_type"
|
||||
lay-verify="required"
|
||||
lay-filter="licenseType"
|
||||
>
|
||||
<option value="time">时间授权</option>
|
||||
|
||||
<option value="count">次数授权</option>
|
||||
|
||||
<option value="permanent">永久授权</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item" id="durationItem">
|
||||
<label class="layui-form-label">有效期</label>
|
||||
|
||||
<div class="layui-input-inline" style="width: 150px;">
|
||||
<input
|
||||
type="number"
|
||||
name="duration"
|
||||
class="layui-input"
|
||||
placeholder="请输入数值"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="layui-input-inline" style="width: 100px;">
|
||||
<select name="duration_unit">
|
||||
<option value="1">分钟</option>
|
||||
|
||||
<option value="60">小时</option>
|
||||
|
||||
<option value="1440">天</option>
|
||||
|
||||
<option value="43200">月</option>
|
||||
|
||||
<option value="525600">年</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item" id="maxUsesItem" style="display:none;">
|
||||
<label class="layui-form-label">使用次数</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<input
|
||||
type="number"
|
||||
name="max_uses"
|
||||
class="layui-input"
|
||||
placeholder="请输入最大使用次数"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">生成数量</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<input
|
||||
type="number"
|
||||
name="count"
|
||||
required
|
||||
lay-verify="required|number|min1"
|
||||
value="1"
|
||||
class="layui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<textarea
|
||||
name="remark"
|
||||
placeholder="请输入备注信息"
|
||||
class="layui-textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="licenseSubmit">
|
||||
生成
|
||||
</button>
|
||||
|
||||
<button type="reset" class="layui-btn layui-btn-primary">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,325 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>系统监控</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="layui-fluid">
|
||||
|
||||
<!-- 系统概览 -->
|
||||
|
||||
<div class="layui-row layui-col-space15">
|
||||
|
||||
<div class="layui-col-md3">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">运行时间</div>
|
||||
|
||||
<div class="layui-card-body big-font" id="uptime">
|
||||
|
||||
0天0小时0分钟
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-col-md3">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">活跃用户</div>
|
||||
|
||||
<div class="layui-card-body big-font" id="active-users">
|
||||
|
||||
0
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-col-md3">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">设备总数</div>
|
||||
|
||||
<div class="layui-card-body big-font" id="total-devices">
|
||||
|
||||
0
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-col-md3">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">系统负载</div>
|
||||
|
||||
<div class="layui-card-body big-font" id="load-avg">
|
||||
|
||||
0.00
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 资源监控 -->
|
||||
|
||||
<div class="layui-row layui-col-space15">
|
||||
|
||||
<div class="layui-col-md6">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">CPU使用率</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
|
||||
<div id="cpu-chart" style="height: 300px;"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-col-md6">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">内存使用情况</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
|
||||
<div id="memory-chart" style="height: 300px;"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 磁盘和网络 -->
|
||||
|
||||
<div class="layui-row layui-col-space15">
|
||||
|
||||
<div class="layui-col-md6">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">磁盘使用情况</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
|
||||
<div id="disk-chart" style="height: 300px;"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-col-md6">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">网络流量</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
|
||||
<div id="network-chart" style="height: 300px;"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 进程和系统信息 -->
|
||||
|
||||
<div class="layui-row layui-col-space15">
|
||||
|
||||
<div class="layui-col-md6">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">
|
||||
|
||||
<span>进程列表(Top 10)</span>
|
||||
|
||||
<span class="layui-badge layui-bg-blue" id="total-processes">0</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
|
||||
<table class="layui-table" lay-skin="line">
|
||||
|
||||
<thead>
|
||||
|
||||
<tr>
|
||||
|
||||
<th>PID</th>
|
||||
|
||||
<th>名称</th>
|
||||
|
||||
<th>CPU使用率</th>
|
||||
|
||||
<th>内存使用率</th>
|
||||
|
||||
<th>运行时间</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody id="process-list"></tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="layui-col-md6">
|
||||
|
||||
<div class="layui-card">
|
||||
|
||||
<div class="layui-card-header">系统信息</div>
|
||||
|
||||
<div class="layui-card-body">
|
||||
|
||||
<table class="layui-table" lay-skin="nob">
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>主机名</td>
|
||||
|
||||
<td id="hostname"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>操作系统</td>
|
||||
|
||||
<td id="os"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>平台</td>
|
||||
|
||||
<td id="platform"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>内核版本</td>
|
||||
|
||||
<td id="kernel"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>CPU型号</td>
|
||||
|
||||
<td id="cpu-model"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>CPU核心数</td>
|
||||
|
||||
<td id="cpu-cores"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>启动时间</td>
|
||||
|
||||
<td id="boot-time"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
|
||||
<script src="/static/lib/echarts.min.js"></script>
|
||||
|
||||
<script src="/static/js/monitor.js"></script>
|
||||
|
||||
<script type="text/html" id="processListTpl">
|
||||
{{# layui.each(d, function(index, item){ }}
|
||||
<tr>
|
||||
<td>{{item.pid}}</td>
|
||||
<td>{{item.name}}</td>
|
||||
<td>{{item.cpu.toFixed(1)}}%</td>
|
||||
<td>{{item.memory.toFixed(1)}}%</td>
|
||||
<td>{{formatDuration(Math.floor((Date.now() - item.created) / 1000))}}</td>
|
||||
</tr>
|
||||
{{# }); }}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,185 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>站点设置</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">站点设置</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="siteSettingsForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">站点标题</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" required lay-verify="required"
|
||||
placeholder="请输入站点标题" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">站点描述</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="description" placeholder="请输入站点描述"
|
||||
class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">基础URL</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="baseUrl" required lay-verify="required"
|
||||
placeholder="请输入基础URL" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">ICP备案号</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="icp" placeholder="请输入ICP备案号"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">版权信息</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="copyright" required lay-verify="required"
|
||||
placeholder="请输入版权信息" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">Logo</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="logo" placeholder="Logo路径"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-input-inline" style="width: auto;">
|
||||
<button type="button" class="layui-btn" id="uploadLogo">
|
||||
<i class="layui-icon"></i>上传Logo
|
||||
</button>
|
||||
</div>
|
||||
<div class="layui-input-inline" style="width: auto;">
|
||||
<img id="currentLogo" src="" style="display:none;max-height:38px;margin-left:10px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">Favicon</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="favicon" placeholder="Favicon路径"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-input-inline" style="width: auto;">
|
||||
<button type="button" class="layui-btn" id="uploadFavicon">
|
||||
<i class="layui-icon"></i>上传Favicon
|
||||
</button>
|
||||
</div>
|
||||
<div class="layui-input-inline" style="width: auto;">
|
||||
<img id="currentFavicon" src="" style="display:none;max-height:38px;margin-left:10px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="siteSubmit">保存设置</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'upload', 'layer'], function(){
|
||||
var form = layui.form;
|
||||
var upload = layui.upload;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 加载当前配置
|
||||
fetch('/api/site/settings', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
layer.msg(data.error);
|
||||
return;
|
||||
}
|
||||
form.val('siteForm', data);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载配置失败:' + error.message);
|
||||
});
|
||||
|
||||
// 上传Logo
|
||||
upload.render({
|
||||
elem: '#uploadLogo',
|
||||
url: '/api/uploads/site',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
accept: 'images',
|
||||
done: function(res){
|
||||
if (res.error) {
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
$('input[name=logo]').val(res.url);
|
||||
}
|
||||
});
|
||||
|
||||
// 上传Favicon
|
||||
upload.render({
|
||||
elem: '#uploadFavicon',
|
||||
url: '/api/uploads/site',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
accept: 'images',
|
||||
done: function(res){
|
||||
if (res.error) {
|
||||
layer.msg(res.error);
|
||||
return;
|
||||
}
|
||||
$('input[name=favicon]').val(res.url);
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
form.on('submit(siteSubmit)', function(data){
|
||||
fetch('/api/site/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data.field)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('保存成功');
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('保存失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>令牌使用日志</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">令牌使用日志</div>
|
||||
<div class="layui-card-body">
|
||||
<table id="log-table" lay-filter="log-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态模板 -->
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status === 'success'){ }}
|
||||
<span class="layui-badge layui-bg-green">成功</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-red">失败</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<!-- 操作类型模板 -->
|
||||
<script type="text/html" id="actionTpl">
|
||||
{{# var types = {
|
||||
'create': '创建',
|
||||
'use': '使用',
|
||||
'revoke': '撤销'
|
||||
}; }}
|
||||
{{# var type = types[d.action] || d.action; }}
|
||||
<span>{{type}}</span>
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['table', 'layer'], function(){
|
||||
var table = layui.table;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 获取令牌ID
|
||||
var tokenId = location.search.match(/id=(\d+)/)[1];
|
||||
|
||||
// 初始化表格
|
||||
table.render({
|
||||
elem: '#log-table',
|
||||
url: '/api/tokens/' + tokenId + '/logs',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
cols: [[
|
||||
{field: 'action', title: '操作类型', width: 100, templet: '#actionTpl'},
|
||||
{field: 'ip', title: 'IP地址', width: 150},
|
||||
{field: 'userAgent', title: 'User-Agent', width: 300},
|
||||
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
|
||||
{field: 'message', title: '详细信息'},
|
||||
{field: 'createdAt', title: '时间', width: 160}
|
||||
]],
|
||||
page: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,130 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>访问令牌管理</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>访问令牌管理</span>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-normal" id="create-token">
|
||||
<i class="layui-icon"></i> 创建令牌
|
||||
</button>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备UID</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="device_uid" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">令牌类型</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="token_type">
|
||||
<option value="">全部</option>
|
||||
<option value="api">API</option>
|
||||
<option value="device">设备</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="active">有效</option>
|
||||
<option value="revoked">已撤销</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table id="token-table" lay-filter="token-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格工具栏模板 -->
|
||||
<script type="text/html" id="tableToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="refresh">
|
||||
<i class="layui-icon"></i> 刷新
|
||||
</button>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="batchRevoke">
|
||||
<i class="layui-icon"></i> 批量撤销
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 行工具栏模板 -->
|
||||
<script type="text/html" id="tableRowBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="view">查看</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="logs">日志</a>
|
||||
[[# if(d.status === 'active'){ ]]
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>
|
||||
[[# } ]]
|
||||
</script>
|
||||
|
||||
<!-- 创建令牌表单模板 -->
|
||||
<script type="text/html" id="createTokenTpl">
|
||||
<form class="layui-form" style="padding: 20px;" lay-filter="tokenForm">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">设备UID</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="device_uid" lay-verify="required" lay-search>
|
||||
<option value="">请选择设备</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">令牌类型</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="token_type" lay-verify="required">
|
||||
<option value="api">API</option>
|
||||
<option value="device">设备</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">有效期</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="expire_days" required lay-verify="required|number"
|
||||
placeholder="请输入有效期天数" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">IP限制</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="ip_list" placeholder="请输入允许访问的IP地址,多个IP用逗号分隔"
|
||||
class="layui-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="tokenSubmit">创建</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/tokens.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,126 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>编辑用户</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="userForm">
|
||||
<input type="hidden" name="id">
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" required lay-verify="required"
|
||||
placeholder="请输入用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" required lay-verify="required|email"
|
||||
placeholder="请输入邮箱" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">角色</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="role" required lay-verify="required">
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="userSubmit">保存</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['form', 'layer'], function(){
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 获取用户ID
|
||||
var userId = location.search.match(/id=(\d+)/);
|
||||
if (userId) {
|
||||
userId = userId[1];
|
||||
// 加载用户数据
|
||||
fetch('/api/users/' + userId, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
layer.msg(data.error);
|
||||
return;
|
||||
}
|
||||
// 填充表单
|
||||
form.val('userForm', {
|
||||
'id': data.id,
|
||||
'username': data.username,
|
||||
'email': data.email,
|
||||
'role': data.role
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载用户数据失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
form.on('submit(userSubmit)', function(data){
|
||||
var field = data.field;
|
||||
var url = userId ? '/api/users/' + userId : '/api/users';
|
||||
var method = userId ? 'PUT' : 'POST';
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(field)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
layer.msg(result.error);
|
||||
return;
|
||||
}
|
||||
layer.msg('保存成功');
|
||||
// 如果是在弹窗中,则关闭弹窗并刷新父页面的表格
|
||||
var index = parent.layer.getFrameIndex(window.name);
|
||||
if (index) {
|
||||
parent.layui.table.reload('user-table');
|
||||
parent.layer.close(index);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('保存失败:' + error.message);
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,124 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>用户管理</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>用户管理</span>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-normal" id="add-user">
|
||||
<i class="layui-icon"></i> 添加用户
|
||||
</button>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索表单 -->
|
||||
<form class="layui-form layui-form-pane" action="">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="username" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">角色</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="role">
|
||||
<option value="">全部</option>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="user">普通用户</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">
|
||||
<i class="layui-icon"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table id="user-table" lay-filter="user-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格工具栏模板 -->
|
||||
<script type="text/html" id="tableToolbar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="refresh">
|
||||
<i class="layui-icon"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 角色模板 -->
|
||||
<script type="text/html" id="roleTpl">
|
||||
{{# if(d.role === 'admin'){ }}
|
||||
<span class="layui-badge layui-bg-blue">管理员</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-gray">普通用户</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<!-- 行工具栏模板 -->
|
||||
<script type="text/html" id="tableRowBar">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
{{# if(d.role !== 'admin'){ }}
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<!-- 用户表单模板 -->
|
||||
<script type="text/html" id="userFormTpl">
|
||||
<form class="layui-form" style="padding: 20px;" lay-filter="userForm">
|
||||
<input type="hidden" name="id">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="username" required lay-verify="required"
|
||||
placeholder="请输入用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="password" name="password" required lay-verify="required"
|
||||
placeholder="请输入密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">邮箱</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="email" required lay-verify="required|email"
|
||||
placeholder="请输入邮箱" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">角色</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="role" required lay-verify="required">
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="userSubmit">提交</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/users.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,111 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>授权验证管理平台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body class="layui-layout-body">
|
||||
<div class="layui-layout layui-layout-admin">
|
||||
<!-- 头部 -->
|
||||
<div class="layui-header">
|
||||
<div class="layui-logo">
|
||||
<img src="/static/images/logo.png" alt="logo" id="site-logo">
|
||||
<span id="site-name">授权验证管理平台</span>
|
||||
</div>
|
||||
<ul class="layui-nav layui-layout-right">
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;">
|
||||
<span id="current-user"></span>
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a href="javascript:;" class="change-password">修改密码</a></dd>
|
||||
<dd><a href="javascript:;" class="logout">退出登录</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<div class="layui-side layui-bg-black">
|
||||
<div class="layui-side-scroll">
|
||||
<ul class="layui-nav layui-nav-tree">
|
||||
<li class="layui-nav-item layui-this">
|
||||
<a href="javascript:;" data-url="/admin/dashboard">
|
||||
<i class="layui-icon"></i> 控制台
|
||||
</a>
|
||||
</li>
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;">
|
||||
<i class="layui-icon"></i> 设备管理
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a href="javascript:;" data-url="/admin/devices">设备型号管理</a></dd>
|
||||
<dd><a href="javascript:;" data-url="/admin/device-files">设备文件管理</a></dd>
|
||||
<dd><a href="javascript:;" data-url="/admin/device-license">设备授权管理</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;">
|
||||
<i class="layui-icon"></i> 授权管理
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a href="javascript:;" data-url="/admin/licenses">授权码管理</a></dd>
|
||||
<dd><a href="javascript:;" data-url="/admin/license-logs">授权日志</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;">
|
||||
<i class="layui-icon"></i> 令牌管理
|
||||
</a>
|
||||
<dl class="layui-nav-child">
|
||||
<dd><a href="javascript:;" data-url="/admin/tokens">访问令牌</a></dd>
|
||||
<dd><a href="javascript:;" data-url="/admin/token-logs">令牌日志</a></dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li class="layui-nav-item admin-only">
|
||||
<a href="javascript:;" data-url="/admin/users">
|
||||
<i class="layui-icon"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="layui-nav-item">
|
||||
<a href="javascript:;" data-url="/admin/monitor">
|
||||
<i class="layui-icon"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
<li class="layui-nav-item admin-only">
|
||||
<a href="javascript:;" data-url="/admin/site-settings">
|
||||
<i class="layui-icon"></i> 站点设置
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容主体 -->
|
||||
<div class="layui-body">
|
||||
<div class="layui-breadcrumb" lay-separator="/">
|
||||
<a href="javascript:;">首页</a>
|
||||
<a><cite>控制台</cite></a>
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<iframe id="content-frame" frameborder="0" class="layadmin-iframe"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="layui-footer">
|
||||
<div class="footer-left">
|
||||
<span id="site-copyright"></span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span id="site-icp"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>登录 - 授权验证管理平台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body class="login-body">
|
||||
<div class="layui-container">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-md4 layui-col-md-offset4">
|
||||
<div class="login-box">
|
||||
<h2>授权验证管理平台</h2>
|
||||
<form class="layui-form" action="">
|
||||
<div class="layui-form-item">
|
||||
<input type="text" name="username" required lay-verify="required"
|
||||
placeholder="用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<input type="password" name="password" required lay-verify="required"
|
||||
placeholder="密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-inline" style="width: 200px;">
|
||||
<input type="text" name="captcha" required lay-verify="required"
|
||||
placeholder="请输入验证码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-input-inline" style="width: 120px;">
|
||||
<img src="" id="captchaImg" style="height:38px;cursor:pointer;" alt="点击刷新">
|
||||
<input type="hidden" name="captchaId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<a href="javascript:;" class="forget-pwd">忘记密码?</a>
|
||||
<a href="javascript:;" class="register">注册账号</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue