main
parent
f722153536
commit
87859c7bb8
126
api-docs.yaml
126
api-docs.yaml
|
@ -275,7 +275,7 @@ paths:
|
|||
description: 生成数量
|
||||
remark:
|
||||
type: string
|
||||
description: <EFBFBD><EFBFBD><EFBFBD>注说明
|
||||
description: 注说明
|
||||
responses:
|
||||
200:
|
||||
description: 生成成功
|
||||
|
@ -419,6 +419,130 @@ paths:
|
|||
error:
|
||||
type: string
|
||||
|
||||
/devices/{uid}/start:
|
||||
post:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 更新设备启动次数
|
||||
description: 记录设备启动,更新启动次数和最后活跃时间
|
||||
parameters:
|
||||
- name: uid
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
description: 设备UID
|
||||
responses:
|
||||
200:
|
||||
description: 更新成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 0
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
start_count:
|
||||
type: integer
|
||||
description: 当前启动次数
|
||||
status:
|
||||
type: string
|
||||
description: 设备状态(active/expired/inactive)
|
||||
last_active_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 最后活跃时间
|
||||
400:
|
||||
description: 请求错误
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: -1
|
||||
error:
|
||||
type: string
|
||||
description: 错误信息
|
||||
404:
|
||||
description: 设备不存在
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: -1
|
||||
error:
|
||||
type: string
|
||||
example: "设备未注册"
|
||||
|
||||
/devices/{uid}/validate:
|
||||
get:
|
||||
tags:
|
||||
- 设备管理
|
||||
summary: 验证设备状态
|
||||
description: 验证设备是否可用,检查授权状态
|
||||
parameters:
|
||||
- name: uid
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
description: 设备UID
|
||||
responses:
|
||||
200:
|
||||
description: 验证成功
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 0
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: 设备状态(active/expired/inactive)
|
||||
license_type:
|
||||
type: string
|
||||
description: 授权类型(time/count/permanent)
|
||||
expire_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 过期时间(仅time类型)
|
||||
start_count:
|
||||
type: integer
|
||||
description: 当前启动次数(仅count类型)
|
||||
max_uses:
|
||||
type: integer
|
||||
description: 最大使用次数(仅count类型)
|
||||
400:
|
||||
description: 验证失败
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: -1
|
||||
error:
|
||||
type: string
|
||||
description: 错误信息
|
||||
404:
|
||||
description: 设备不存在
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: -1
|
||||
error:
|
||||
type: string
|
||||
example: "设备未注册"
|
||||
|
||||
definitions:
|
||||
DeviceModel:
|
||||
type: object
|
||||
|
|
|
@ -66,15 +66,17 @@ func main() {
|
|||
uploadService := service.NewUploadService(db, config)
|
||||
siteService := service.NewSiteService(config)
|
||||
tokenService := service.NewTokenService(db)
|
||||
dashboardService := service.NewDashboardService(db)
|
||||
|
||||
// 初始化处理器
|
||||
userHandler := api.NewUserHandler(userService)
|
||||
deviceHandler := api.NewDeviceHandler(deviceService)
|
||||
deviceHandler := api.NewDeviceHandler(deviceService, config)
|
||||
monitorHandler := api.NewMonitorHandler(monitorService)
|
||||
uploadHandler := api.NewUploadHandler(uploadService)
|
||||
siteHandler := api.NewSiteHandler(siteService)
|
||||
tokenHandler := api.NewTokenHandler(tokenService)
|
||||
licenseHandler := api.NewLicenseHandler(licenseService)
|
||||
dashboardHandler := api.NewDashboardHandler(dashboardService)
|
||||
|
||||
// 设置路由
|
||||
router := api.SetupRouter(
|
||||
|
@ -86,6 +88,7 @@ func main() {
|
|||
siteHandler,
|
||||
tokenHandler,
|
||||
licenseHandler,
|
||||
dashboardHandler,
|
||||
)
|
||||
|
||||
// 创建必要的目录
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
server:
|
||||
port: "8080"
|
||||
mode: "dev"
|
||||
|
||||
database:
|
||||
path: "./data/database.db"
|
||||
|
||||
jwt:
|
||||
secret: "your-jwt-secret-key"
|
||||
expire: "24h"
|
||||
|
||||
email:
|
||||
host: "smtp.example.com"
|
||||
port: 587
|
||||
username: "your-email@example.com"
|
||||
password: "your-email-password"
|
||||
|
||||
upload:
|
||||
path: "./uploads"
|
||||
|
||||
site:
|
||||
title: "授权验证管理平台"
|
||||
description: "设备授权和验证管理系统"
|
||||
base_url: "http://localhost:8080"
|
||||
logo: "/static/images/logo.png"
|
||||
favicon: "/static/images/favicon.ico"
|
||||
copyright: "© 2024 授权验证管理平台"
|
||||
icp: ""
|
||||
|
||||
security:
|
||||
encrypt_key: "your-32-byte-encrypt-key-here123456"
|
BIN
data/license.db
BIN
data/license.db
Binary file not shown.
12
go.mod
12
go.mod
|
@ -6,15 +6,15 @@ toolchain go1.23.0
|
|||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/shirou/gopsutil/v3 v3.24.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
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
|
||||
gorm.io/driver/sqlite v1.5.2
|
||||
gorm.io/gorm v1.25.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -60,11 +60,11 @@ require (
|
|||
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
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // 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/image v0.13.0 // 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
|
||||
|
|
26
go.sum
26
go.sum
|
@ -33,12 +33,11 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27
|
|||
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-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/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=
|
||||
|
@ -92,8 +91,8 @@ github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3
|
|||
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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
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=
|
||||
|
@ -116,7 +115,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
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=
|
||||
|
@ -129,8 +127,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||
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=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/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=
|
||||
|
@ -139,8 +137,9 @@ 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/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
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=
|
||||
|
@ -148,7 +147,6 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
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=
|
||||
|
@ -165,8 +163,8 @@ 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=
|
||||
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
|
||||
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
||||
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
|
|
@ -1,45 +1,33 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/service"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardHandler struct {
|
||||
db *gorm.DB
|
||||
dashboardService *service.DashboardService
|
||||
}
|
||||
|
||||
func NewDashboardHandler(db *gorm.DB) *DashboardHandler {
|
||||
return &DashboardHandler{db: db}
|
||||
func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardHandler {
|
||||
return &DashboardHandler{dashboardService: dashboardService}
|
||||
}
|
||||
|
||||
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"`
|
||||
// GetDashboardStats 获取仪表盘统计数据
|
||||
func (h *DashboardHandler) GetDashboardStats(c *gin.Context) {
|
||||
stats, err := h.dashboardService.GetDashboardStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备总数
|
||||
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)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": stats,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,22 +3,25 @@ package api
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"strconv"
|
||||
|
||||
"licserver/internal/model"
|
||||
"licserver/internal/service"
|
||||
"licserver/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DeviceHandler struct {
|
||||
deviceService *service.DeviceService
|
||||
config *utils.Config
|
||||
}
|
||||
|
||||
func NewDeviceHandler(deviceService *service.DeviceService) *DeviceHandler {
|
||||
func NewDeviceHandler(deviceService *service.DeviceService, config *utils.Config) *DeviceHandler {
|
||||
|
||||
return &DeviceHandler{deviceService: deviceService}
|
||||
return &DeviceHandler{deviceService: deviceService, config: config}
|
||||
|
||||
}
|
||||
|
||||
|
@ -50,7 +53,7 @@ func (h *DeviceHandler) GetDevices(c *gin.Context) {
|
|||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
|
||||
params := &service.DeviceQueryParams{
|
||||
|
||||
|
@ -89,19 +92,37 @@ func (h *DeviceHandler) GetDevices(c *gin.Context) {
|
|||
}
|
||||
|
||||
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()})
|
||||
|
||||
// 更新启动次数
|
||||
err := h.deviceService.UpdateStartCount(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "启动次数更新成功"})
|
||||
// 获取更新后的设备信息
|
||||
device, err := h.deviceService.GetDevice(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "启动次数更新成功",
|
||||
"data": gin.H{
|
||||
"start_count": device.StartCount,
|
||||
"status": device.Status,
|
||||
"last_active_at": device.LastActiveAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) UpdateDevice(c *gin.Context) {
|
||||
|
@ -207,19 +228,50 @@ func (h *DeviceHandler) RegisterDevice(c *gin.Context) {
|
|||
}
|
||||
|
||||
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()})
|
||||
|
||||
device, err := h.deviceService.GetDevice(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "设备验证通过"})
|
||||
// 验证设备状态
|
||||
if err := h.deviceService.ValidateDevice(uid); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 准备加密响应数据
|
||||
response := utils.DeviceValidateResponse{
|
||||
Status: device.Status,
|
||||
LicenseType: device.LicenseType,
|
||||
ExpireTime: device.ExpireTime.Format(time.RFC3339),
|
||||
StartCount: device.StartCount,
|
||||
MaxUses: device.MaxUses,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// 加密响应
|
||||
encrypted, err := utils.EncryptResponse(response, []byte(h.config.Security.EncryptKey))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": -1,
|
||||
"error": "加密响应失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": encrypted,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) BindLicense(c *gin.Context) {
|
||||
|
@ -360,14 +412,17 @@ func (h *DeviceHandler) UpdateDeviceModel(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var model model.DeviceModel
|
||||
if err := c.ShouldBindJSON(&model); err != nil {
|
||||
var input model.DeviceModel
|
||||
if err := c.ShouldBindJSON(&input); 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()})
|
||||
if err := h.deviceService.UpdateDeviceModel(uint(id), &input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -455,17 +510,3 @@ func (h *DeviceHandler) GetDeviceLogs(c *gin.Context) {
|
|||
"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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ func SetupRouter(
|
|||
siteHandler *SiteHandler,
|
||||
tokenHandler *TokenHandler,
|
||||
licenseHandler *LicenseHandler,
|
||||
dashboardHandler *DashboardHandler,
|
||||
) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
|
@ -63,6 +64,8 @@ func SetupRouter(
|
|||
api.POST("/captcha/reset-password", userHandler.SendResetPasswordCaptcha)
|
||||
api.POST("/validate-token", tokenHandler.ValidateToken)
|
||||
api.POST("/devices/register", deviceHandler.RegisterDevice)
|
||||
api.POST("/devices/:uid/start", deviceHandler.UpdateStartCount)
|
||||
api.GET("/devices/:uid/validate", deviceHandler.ValidateDevice)
|
||||
|
||||
// 需要认证的API
|
||||
authorized := api.Group("")
|
||||
|
@ -76,7 +79,6 @@ func SetupRouter(
|
|||
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)
|
||||
|
@ -113,7 +115,7 @@ func SetupRouter(
|
|||
authorized.POST("/licenses/use", licenseHandler.UseLicense)
|
||||
|
||||
// 仪表盘统计
|
||||
authorized.GET("/dashboard/stats", deviceHandler.GetDashboardStats)
|
||||
authorized.GET("/dashboard/stats", dashboardHandler.GetDashboardStats)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,44 +21,45 @@ func NewUserHandler(userService *service.UserService) *UserHandler {
|
|||
|
||||
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"`
|
||||
}
|
||||
|
||||
var input service.LoginInput
|
||||
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) {
|
||||
if !h.userService.VerifyCaptcha(input.CaptchaId, input.Captcha) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.userService.Login(input.Username, input.Password)
|
||||
|
||||
// 验证用户名密码
|
||||
user, err := h.userService.ValidateUser(input.Username, input.Password)
|
||||
if err != nil {
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// 设置 cookie
|
||||
// c.SetCookie("token", token, 86400, "/", "", false, true) // 24小时过期,httpOnly=true
|
||||
// 生成 JWT token
|
||||
token, err := h.userService.GenerateToken(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
// 返回token和用户信息
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"data": gin.H{
|
||||
"token": token,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
@ -146,7 +147,7 @@ func (h *UserHandler) ResetPassword(c *gin.Context) {
|
|||
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "重置密码邮件已发送"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "重置密邮件已发送"})
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,11 @@ 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
|
||||
}
|
||||
// // 1. 首先从 cookie 中获取 token
|
||||
// tokenCookie, err := c.Cookie("token")
|
||||
// if err == nil {
|
||||
// token = tokenCookie
|
||||
// }
|
||||
|
||||
// 2. 如果 cookie 中没有,则从 header 中获取
|
||||
if token == "" {
|
||||
|
@ -44,8 +44,6 @@ func JWTAuth(config *utils.JWTConfig) gin.HandlerFunc {
|
|||
// 验证 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
|
||||
|
@ -56,12 +54,6 @@ func JWTAuth(config *utils.JWTConfig) gin.HandlerFunc {
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -7,7 +7,17 @@ import (
|
|||
type DeviceLog struct {
|
||||
gorm.Model
|
||||
DeviceUID string `gorm:"index" json:"device_uid"` // 设备UID
|
||||
Action string `json:"action"` // 操作类型
|
||||
Action string `json:"action"` // 操作类型:register/start/verify/bind/unbind
|
||||
Message string `json:"message"` // 详细信息
|
||||
Status string `json:"status"` // 状态:success/failed
|
||||
IP string `json:"ip"` // 操作IP
|
||||
}
|
||||
|
||||
// 定义日志操作类型常量
|
||||
const (
|
||||
LogActionRegister = "register"
|
||||
LogActionStart = "start"
|
||||
LogActionVerify = "verify"
|
||||
LogActionBind = "bind"
|
||||
LogActionUnbind = "unbind"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"licserver/internal/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DashboardService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDashboardService(db *gorm.DB) *DashboardService {
|
||||
return &DashboardService{db: db}
|
||||
}
|
||||
|
||||
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"` // 过期设备
|
||||
DeviceTypes []DeviceTypeStats `json:"device_types"` // 设备类型分布
|
||||
TrendData []DailyRegistration `json:"trend_data"` // 注册趋势
|
||||
LicenseStats LicenseStatistics `json:"license_stats"` // 授权码统计
|
||||
}
|
||||
|
||||
type DeviceTypeStats struct {
|
||||
Type string `json:"type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type DailyRegistration struct {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type LicenseStatistics struct {
|
||||
Unused int64 `json:"unused"` // 未使用
|
||||
Used int64 `json:"used"` // 已使用
|
||||
Expired int64 `json:"expired"` // 已过期
|
||||
Revoked int64 `json:"revoked"` // 已撤销
|
||||
}
|
||||
|
||||
func (s *DashboardService) GetDashboardStats() (*DashboardStats, error) {
|
||||
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)
|
||||
|
||||
// 获取设备类型分布
|
||||
var deviceTypes []DeviceTypeStats
|
||||
s.db.Model(&model.Device{}).
|
||||
Select("device_type as type, count(*) as count").
|
||||
Group("device_type").
|
||||
Scan(&deviceTypes)
|
||||
stats.DeviceTypes = deviceTypes
|
||||
|
||||
// 获取最近7天的注册趋势
|
||||
var trendData []DailyRegistration
|
||||
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
|
||||
s.db.Model(&model.Device{}).
|
||||
Select("DATE(register_time) as date, count(*) as count").
|
||||
Where("register_time >= ?", sevenDaysAgo).
|
||||
Group("DATE(register_time)").
|
||||
Order("date ASC").
|
||||
Scan(&trendData)
|
||||
stats.TrendData = trendData
|
||||
|
||||
// 获取授权码统计
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "unused").Count(&stats.LicenseStats.Unused)
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "used").Count(&stats.LicenseStats.Used)
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "expired").Count(&stats.LicenseStats.Expired)
|
||||
s.db.Model(&model.LicenseCode{}).Where("status = ?", "revoked").Count(&stats.LicenseStats.Revoked)
|
||||
|
||||
return stats, nil
|
||||
}
|
|
@ -31,7 +31,7 @@ func NewDeviceService(db *gorm.DB, licenseService *LicenseService) *DeviceServic
|
|||
type DeviceRegisterInput struct {
|
||||
UID string `json:"uid" binding:"required"`
|
||||
DeviceModel string `json:"device_model" binding:"required"`
|
||||
LicenseCode string `json:"license_code"`
|
||||
LicenseCode string `json:"license_code,omitempty"`
|
||||
}
|
||||
|
||||
func (s *DeviceService) RegisterDevice(input *DeviceRegisterInput, ip string) error {
|
||||
|
@ -104,15 +104,12 @@ func (s *DeviceService) RegisterDevice(input *DeviceRegisterInput, ip string) er
|
|||
}
|
||||
|
||||
// 记录设备日志
|
||||
logMsg := "设备注册成功"
|
||||
if device.LicenseCode != "" {
|
||||
logMsg += fmt.Sprintf(",使用授权码: %s", device.LicenseCode)
|
||||
}
|
||||
log := model.DeviceLog{
|
||||
DeviceUID: input.UID,
|
||||
Action: "register",
|
||||
Message: logMsg,
|
||||
Message: fmt.Sprintf("设备注册成功,状态:%s", device.Status),
|
||||
Status: "success",
|
||||
IP: ip,
|
||||
}
|
||||
if err := tx.Create(&log).Error; err != nil {
|
||||
return err
|
||||
|
@ -337,7 +334,7 @@ type DeviceCreateInput struct {
|
|||
}
|
||||
|
||||
func (s *DeviceService) CreateDevice(input *DeviceCreateInput) error {
|
||||
// 检查设备UID是否已存在
|
||||
// 检查设备UID<EFBFBD><EFBFBD><EFBFBD>否已存在
|
||||
var count int64
|
||||
if err := s.db.Model(&model.Device{}).Where("uid = ?", input.UID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
|
@ -408,15 +405,49 @@ func (s *DeviceService) CreateDeviceModel(model_ *model.DeviceModel) 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("设备型号已存在")
|
||||
func (s *DeviceService) UpdateDeviceModel(id uint, input *model.DeviceModel) error {
|
||||
// 检查设备型号是否存在
|
||||
var existingModel model.DeviceModel
|
||||
if err := s.db.First(&existingModel, id).Error; err != nil {
|
||||
return errors.New("设备型号不存在")
|
||||
}
|
||||
|
||||
return s.db.Model(&model.DeviceModel{}).Where("id = ?", id).Updates(model_).Error
|
||||
// 如果型号名称发生变更,需要检查是否存在冲突
|
||||
if input.ModelName != existingModel.ModelName {
|
||||
// 检查新的型号名称是否与其他型号冲突(排除自身)
|
||||
var count int64
|
||||
s.db.Model(&model.DeviceModel{}).
|
||||
Where("model_name = ? AND id != ?", input.ModelName, id).
|
||||
Count(&count)
|
||||
if count > 0 {
|
||||
|
||||
return fmt.Errorf("设备型号[%d]:%s已存在", id, input.ModelName)
|
||||
}
|
||||
|
||||
// 检查是否有设备正在使用此型号
|
||||
var deviceCount int64
|
||||
s.db.Model(&model.Device{}).
|
||||
Where("device_model = ?", existingModel.ModelName).
|
||||
Count(&deviceCount)
|
||||
if deviceCount > 0 {
|
||||
return errors.New("该型号下存在设备,无法修改型号名称")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备型号信息
|
||||
updates := map[string]interface{}{
|
||||
"device_type": input.DeviceType,
|
||||
"company": input.Company,
|
||||
"status": input.Status,
|
||||
"remark": input.Remark,
|
||||
}
|
||||
|
||||
// 只有当型号名称变更时才更新
|
||||
if input.ModelName != existingModel.ModelName {
|
||||
updates["model_name"] = input.ModelName
|
||||
}
|
||||
|
||||
return s.db.Model(&existingModel).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteDeviceModel 删除设备型号
|
||||
|
@ -448,7 +479,7 @@ func (s *DeviceService) GetDeviceModels(modelName, deviceType, company string, p
|
|||
query = query.Where("company LIKE ?", "%"+company+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
// 取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
|
@ -683,39 +714,10 @@ func (s *DeviceService) GetDeviceLogs(uid string, page, pageSize int) ([]model.D
|
|||
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"` // 过期设备
|
||||
func (s *DeviceService) GetDevice(uid string) (*model.Device, error) {
|
||||
var device model.Device
|
||||
if err := s.db.Where("uid = ?", uid).First(&device).Error; err != nil {
|
||||
return nil, errors.New("设备不存在")
|
||||
}
|
||||
|
||||
// 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
|
||||
return &device, nil
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ import (
|
|||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
|
@ -29,6 +33,14 @@ type UserProfile struct {
|
|||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// LoginInput 登录输入
|
||||
type LoginInput struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
CaptchaId string `json:"captchaId" binding:"required"`
|
||||
Captcha string `json:"captcha" binding:"required"`
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, config *utils.Config) *UserService {
|
||||
return &UserService{
|
||||
db: db,
|
||||
|
@ -359,3 +371,36 @@ func (s *UserService) DeleteUser(id uint) error {
|
|||
|
||||
return s.db.Delete(&model.User{}, id).Error
|
||||
}
|
||||
|
||||
// ValidateUser 验证用户名密码
|
||||
func (s *UserService) ValidateUser(username, password string) (*model.User, error) {
|
||||
var user model.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
|
||||
// 使用 bcrypt 比较密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, errors.New("密码错误")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func (s *UserService) GenerateToken(user *model.User) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.config.JWT.Secret))
|
||||
}
|
||||
|
||||
// VerifyCaptcha 验证验证码
|
||||
func (s *UserService) VerifyCaptcha(captchaId, captcha string) bool {
|
||||
return base64Captcha.DefaultMemStore.Verify(captchaId, captcha, true)
|
||||
}
|
||||
|
|
|
@ -7,21 +7,19 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
JWT JWTConfig
|
||||
Email EmailConfig
|
||||
Upload UploadConfig
|
||||
Site SiteConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Mode string
|
||||
Server struct {
|
||||
Port string `yaml:"port"`
|
||||
Mode string `yaml:"mode"`
|
||||
} `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
JWT JWTConfig `yaml:"jwt"`
|
||||
Email EmailConfig `yaml:"email"`
|
||||
Upload UploadConfig `yaml:"upload"`
|
||||
Site SiteConfig `yaml:"site"`
|
||||
Security SecurityConfig `yaml:"security"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
Path string
|
||||
}
|
||||
|
||||
|
@ -42,13 +40,17 @@ type UploadConfig struct {
|
|||
}
|
||||
|
||||
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"`
|
||||
Title string
|
||||
Description string
|
||||
BaseURL string
|
||||
Logo string
|
||||
Favicon string
|
||||
Copyright string
|
||||
ICP string
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
EncryptKey string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type DeviceValidateResponse struct {
|
||||
Status string `json:"status"` // 设备状态
|
||||
LicenseType string `json:"license_type"` // 授权类型
|
||||
ExpireTime string `json:"expire_time"` // 过期时间
|
||||
StartCount int `json:"start_count"` // 启动次数
|
||||
MaxUses int `json:"max_uses"` // 最大使用次数
|
||||
Timestamp int64 `json:"timestamp"` // 时间戳
|
||||
Signature string `json:"signature"` // 签名
|
||||
}
|
||||
|
||||
// EncryptResponse 加密设备验证响应
|
||||
func EncryptResponse(data DeviceValidateResponse, key []byte) (string, error) {
|
||||
// 序列化数据
|
||||
plaintext, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 创建随机IV
|
||||
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 加密
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
|
||||
|
||||
// 返回base64编码的密文
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// DecryptResponse 解密设备验证响应
|
||||
func DecryptResponse(encrypted string, key []byte) (*DeviceValidateResponse, error) {
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, errors.New("密文太短")
|
||||
}
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
|
||||
var response DeviceValidateResponse
|
||||
if err := json.Unmarshal(ciphertext, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
|
@ -28,6 +28,8 @@ func InitDB(config *DatabaseConfig) (*gorm.DB, error) {
|
|||
|
||||
&model.DeviceModel{},
|
||||
|
||||
&model.DeviceLog{},
|
||||
|
||||
&model.PasswordResetToken{},
|
||||
|
||||
&model.Captcha{},
|
||||
|
|
|
@ -36,33 +36,22 @@ func TestDB(t *testing.T) *gorm.DB {
|
|||
// TestConfig 创建测试配置
|
||||
func TestConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Server: struct {
|
||||
Port string `yaml:"port"`
|
||||
Mode string `yaml:"mode"`
|
||||
}{
|
||||
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",
|
||||
Security: SecurityConfig{
|
||||
EncryptKey: "test-32-byte-encrypt-key-here1234567",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
# Device SDK for C++
|
||||
|
||||
## 依赖
|
||||
- libcurl
|
||||
- nlohmann/json
|
||||
- OpenSSL
|
||||
|
||||
### Windows
|
||||
1. vcpkg 安装依赖:
|
||||
```bash
|
||||
vcpkg install curl:x64-windows
|
||||
vcpkg install openssl:x64-windows
|
||||
vcpkg install nlohmann-json:x64-windows
|
||||
```
|
||||
### Linux
|
||||
1. Ubuntu/Debian:
|
||||
```bash
|
||||
sudo apt-get install libcurl4-openssl-dev libssl-dev nlohmann-json-dev
|
||||
```
|
||||
2. CentOS/RHEL:
|
||||
```bash
|
||||
sudo yum install libcurl-devel openssl-devel nlohmann-json-devel
|
||||
```
|
||||
|
||||
## 编译
|
||||
### Windows (Visual Studio)
|
||||
1. 添加包含目录:
|
||||
- $(vcpkg_root)/installed/x64-windows/include
|
||||
2. 添加库目录:
|
||||
- $(vcpkg_root)/installed/x64-windows/lib
|
||||
3. 添加依赖库:
|
||||
- libcurl
|
||||
- libssl
|
||||
- libcrypto
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
g++ -o example example.cpp device_sdk.cpp -lcurl -lssl -lcrypto -I/usr/include/nlohmann
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
### 初始化
|
||||
|
||||
```cpp
|
||||
#include "device_sdk.h"
|
||||
DeviceClient client("http://localhost:8080", "your-32-byte-encrypt-key-here123456");
|
||||
```
|
||||
### 设备注册
|
||||
```cpp
|
||||
// 不带授权码注册
|
||||
DeviceInfo device;
|
||||
device.uid = "device-001";
|
||||
device.device_model = "test-model";
|
||||
std::string error;
|
||||
if (!client.registerDevice(device, error)) {
|
||||
std::cerr << "注册失败: " << error << std::endl;
|
||||
return;
|
||||
}
|
||||
// 带授权码注册
|
||||
DeviceInfo deviceWithLicense;
|
||||
deviceWithLicense.uid = "device-002";
|
||||
deviceWithLicense.device_model = "test-model";
|
||||
deviceWithLicense.license_code = "your-license-code";
|
||||
if (!client.registerDevice(deviceWithLicense, error)) {
|
||||
std::cerr << "注册失败: " << error << std::endl;
|
||||
return;
|
||||
}
|
||||
```
|
||||
### 设备验证
|
||||
```cpp
|
||||
ValidateResponse response;
|
||||
std::string error;
|
||||
if (!client.validateDevice("device-001", response, error)) {
|
||||
std::cerr << "验证失败: " << error << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cout << "设备状态: " << response.status << std::endl;
|
||||
std::cout << "授权类型: " << response.license_type << std::endl;
|
||||
std::cout << "过期时间: " << response.expire_time << std::endl;
|
||||
```
|
||||
|
||||
### 更新启动次数
|
||||
|
||||
```cpp
|
||||
int count;
|
||||
std::string error;
|
||||
if (!client.updateStartCount("device-001", count, error)) {
|
||||
std::cerr << "更新失败: " << error << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cout << "当前启动次数: " << count << std::endl;
|
||||
```
|
||||
### 绑定授权码
|
||||
```cpp
|
||||
std::string error;
|
||||
if (!client.bindLicense("device-001", "license-code-123", error)) {
|
||||
std::cerr << "绑定失败: " << error << std::endl;
|
||||
return;
|
||||
}
|
||||
```
|
||||
## 完整示例
|
||||
```cpp
|
||||
#include <iostream>
|
||||
#include "device_sdk.h"
|
||||
int main() {
|
||||
try {
|
||||
DeviceClient client("http://localhost:8080", "your-32-byte-encrypt-key-here123456");
|
||||
std::string error;
|
||||
// 注册设备
|
||||
DeviceInfo device;
|
||||
device.uid = "device-001";
|
||||
device.device_model = "test-model";
|
||||
if (!client.registerDevice(device, error)) {
|
||||
std::cerr << "注册失败: " << error << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "设备注册成功" << std::endl;
|
||||
// 绑定授权码
|
||||
if (!client.bindLicense("device-001", "license-code-123", error)) {
|
||||
std::cerr << "绑定失败: " << error << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "授权码绑定成功" << std::endl;
|
||||
// 验证设备
|
||||
ValidateResponse response;
|
||||
if (!client.validateDevice("device-001", response, error)) {
|
||||
std::cerr << "验证失败: " << error << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "设备状态: " << response.status << std::endl;
|
||||
std::cout << "授权类型: " << response.license_type << std::endl;
|
||||
// 更新启动次数
|
||||
int count;
|
||||
if (!client.updateStartCount("device-001", count, error)) {
|
||||
std::cerr << "更新失败: " << error << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::cout << "当前启动次数: " << count << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "发生错误: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
|
@ -0,0 +1,100 @@
|
|||
#include "device_sdk.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <vector>
|
||||
|
||||
DeviceClient::DeviceClient(const std::string& base_url, const std::string& encrypt_key)
|
||||
: base_url_(base_url), encrypt_key_(encrypt_key) {
|
||||
curl_global_init(CURL_GLOBAL_ALL);
|
||||
curl_ = curl_easy_init();
|
||||
}
|
||||
|
||||
DeviceClient::~DeviceClient() {
|
||||
if (curl_) {
|
||||
curl_easy_cleanup(curl_);
|
||||
}
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
bool DeviceClient::registerDevice(const DeviceInfo& device_info, std::string& error) {
|
||||
json request;
|
||||
request["uid"] = device_info.uid;
|
||||
request["device_model"] = device_info.device_model;
|
||||
if (!device_info.license_code.empty()) {
|
||||
request["license_code"] = device_info.license_code;
|
||||
}
|
||||
|
||||
std::string response;
|
||||
if (!post(base_url_ + "/api/devices/register", request.dump(), response, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto resp_json = json::parse(response);
|
||||
if (resp_json["code"] != 0) {
|
||||
error = resp_json["error"];
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeviceClient::validateDevice(const std::string& uid, ValidateResponse& response, std::string& error) {
|
||||
std::string encrypted_response;
|
||||
if (!get(base_url_ + "/api/devices/" + uid + "/validate", encrypted_response, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto resp_json = json::parse(encrypted_response);
|
||||
if (resp_json["code"] != 0) {
|
||||
error = resp_json["error"];
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string decrypted = decryptResponse(resp_json["data"]);
|
||||
auto data = json::parse(decrypted);
|
||||
|
||||
response.status = data["status"];
|
||||
response.license_type = data["license_type"];
|
||||
response.expire_time = data["expire_time"];
|
||||
response.start_count = data["start_count"];
|
||||
response.max_uses = data["max_uses"];
|
||||
response.timestamp = data["timestamp"];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeviceClient::updateStartCount(const std::string& uid, int& count, std::string& error) {
|
||||
std::string response;
|
||||
if (!post(base_url_ + "/api/devices/" + uid + "/start", "", response, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto resp_json = json::parse(response);
|
||||
if (resp_json["code"] != 0) {
|
||||
error = resp_json["error"];
|
||||
return false;
|
||||
}
|
||||
|
||||
count = resp_json["data"]["start_count"];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeviceClient::bindLicense(const std::string& uid, const std::string& license_code, std::string& error) {
|
||||
json request;
|
||||
request["license_code"] = license_code;
|
||||
|
||||
std::string response;
|
||||
if (!post(base_url_ + "/api/devices/" + uid + "/license", request.dump(), response, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto resp_json = json::parse(response);
|
||||
if (resp_json["code"] != 0) {
|
||||
error = resp_json["error"];
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ... HTTP 请求和加密相关的辅助函数实现 ...
|
|
@ -0,0 +1,61 @@
|
|||
#ifndef DEVICE_SDK_H
|
||||
#define DEVICE_SDK_H
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#else
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <ctime>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct DeviceInfo {
|
||||
std::string uid;
|
||||
std::string device_model;
|
||||
std::string license_code; // 可选
|
||||
};
|
||||
|
||||
struct ValidateResponse {
|
||||
std::string status;
|
||||
std::string license_type;
|
||||
std::string expire_time;
|
||||
int start_count;
|
||||
int max_uses;
|
||||
int64_t timestamp;
|
||||
};
|
||||
|
||||
class DeviceClient {
|
||||
public:
|
||||
DeviceClient(const std::string& base_url, const std::string& encrypt_key);
|
||||
~DeviceClient();
|
||||
|
||||
// 设备注册
|
||||
bool registerDevice(const DeviceInfo& device_info, std::string& error);
|
||||
|
||||
// 设备验证
|
||||
bool validateDevice(const std::string& uid, ValidateResponse& response, std::string& error);
|
||||
|
||||
// 更新启动次数
|
||||
bool updateStartCount(const std::string& uid, int& count, std::string& error);
|
||||
|
||||
// 绑定授权码
|
||||
bool bindLicense(const std::string& uid, const std::string& license_code, std::string& error);
|
||||
|
||||
private:
|
||||
std::string base_url_;
|
||||
std::string encrypt_key_;
|
||||
CURL* curl_;
|
||||
|
||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp);
|
||||
bool post(const std::string& url, const std::string& data, std::string& response, std::string& error);
|
||||
bool get(const std::string& url, std::string& response, std::string& error);
|
||||
std::string decryptResponse(const std::string& encrypted);
|
||||
};
|
||||
|
||||
#endif // DEVICE_SDK_H
|
|
@ -0,0 +1,220 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceSDK
|
||||
{
|
||||
public class DeviceClient : IDisposable
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _encryptKey;
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool _disposed;
|
||||
|
||||
public DeviceClient(string baseUrl, string encryptKey)
|
||||
{
|
||||
_baseUrl = baseUrl.TrimEnd('/');
|
||||
_encryptKey = encryptKey;
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
}
|
||||
|
||||
// 设备注册
|
||||
public async Task<DeviceRegisterResponse> RegisterDeviceAsync(DeviceRegisterRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await PostAsync("/api/devices/register", request);
|
||||
return JsonSerializer.Deserialize<DeviceRegisterResponse>(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DeviceSDKException("设备注册失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 设备验证
|
||||
public async Task<DeviceValidateResponse> ValidateDeviceAsync(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync($"/api/devices/{uid}/validate");
|
||||
var result = JsonSerializer.Deserialize<ApiResponse>(response);
|
||||
|
||||
if (result.Code != 0)
|
||||
{
|
||||
throw new DeviceSDKException(result.Error);
|
||||
}
|
||||
|
||||
var decrypted = DecryptResponse(result.Data);
|
||||
return JsonSerializer.Deserialize<DeviceValidateResponse>(decrypted);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DeviceSDKException("设备验证失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新启动次数
|
||||
public async Task<StartCountResponse> UpdateStartCountAsync(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await PostAsync($"/api/devices/{uid}/start", null);
|
||||
return JsonSerializer.Deserialize<StartCountResponse>(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DeviceSDKException("更新启动次数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定授权码
|
||||
public async Task BindLicenseAsync(string uid, string licenseCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new { license_code = licenseCode };
|
||||
var response = await PostAsync($"/api/devices/{uid}/license", request);
|
||||
var result = JsonSerializer.Deserialize<ApiResponse>(response);
|
||||
|
||||
if (result.Code != 0)
|
||||
{
|
||||
throw new DeviceSDKException(result.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DeviceSDKException("绑定授权码失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetAsync(string path)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(_baseUrl + path);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
private async Task<string> PostAsync(string path, object data)
|
||||
{
|
||||
var content = data == null ? null :
|
||||
new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync(_baseUrl + path, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
private string DecryptResponse(string encrypted)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cipherBytes = Convert.FromBase64String(encrypted);
|
||||
var keyBytes = Encoding.UTF8.GetBytes(_encryptKey);
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = keyBytes;
|
||||
|
||||
var iv = new byte[16];
|
||||
Array.Copy(cipherBytes, 0, iv, 0, 16);
|
||||
aes.IV = iv;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
var cipher = new byte[cipherBytes.Length - 16];
|
||||
Array.Copy(cipherBytes, 16, cipher, 0, cipher.Length);
|
||||
|
||||
var plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
return Encoding.UTF8.GetString(plainBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DeviceSDKException("解密响应失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 请求和响应模型
|
||||
public class DeviceRegisterRequest
|
||||
{
|
||||
public string Uid { get; set; }
|
||||
public string DeviceModel { get; set; }
|
||||
public string LicenseCode { get; set; }
|
||||
}
|
||||
|
||||
public class DeviceRegisterResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DeviceRegisterData Data { get; set; }
|
||||
}
|
||||
|
||||
public class DeviceRegisterData
|
||||
{
|
||||
public string Uid { get; set; }
|
||||
public string DeviceModel { get; set; }
|
||||
public string Status { get; set; }
|
||||
}
|
||||
|
||||
public class DeviceValidateResponse
|
||||
{
|
||||
public string Status { get; set; }
|
||||
public string LicenseType { get; set; }
|
||||
public DateTime ExpireTime { get; set; }
|
||||
public int StartCount { get; set; }
|
||||
public int MaxUses { get; set; }
|
||||
public long Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public class StartCountResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
public StartCountData Data { get; set; }
|
||||
}
|
||||
|
||||
public class StartCountData
|
||||
{
|
||||
public int StartCount { get; set; }
|
||||
public string Status { get; set; }
|
||||
public DateTime LastActiveAt { get; set; }
|
||||
}
|
||||
|
||||
internal class ApiResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
// 自定义异常
|
||||
public class DeviceSDKException : Exception
|
||||
{
|
||||
public DeviceSDKException(string message) : base(message) { }
|
||||
public DeviceSDKException(string message, Exception innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
# Device SDK for C#
|
||||
|
||||
## 安装
|
||||
将 `DeviceSDK.cs` 添加到你的项目中。
|
||||
|
||||
## 初始化
|
||||
|
||||
```csharp
|
||||
var client = new DeviceClient(
|
||||
baseUrl: "http://your-server:8080", // 服务器地址
|
||||
encryptKey: "your-32-byte-encrypt-key-here123456" // 32字节加密密钥
|
||||
);
|
||||
```
|
||||
|
||||
## API 使用说明
|
||||
|
||||
### 1. 设备注册
|
||||
|
||||
```csharp
|
||||
// 基本注册(无授权码)
|
||||
var response = await client.RegisterDeviceAsync(new DeviceRegisterRequest {
|
||||
Uid = "device-001",
|
||||
DeviceModel = "test-model"
|
||||
});```
|
||||
|
||||
|
||||
|
||||
```csharp
|
||||
// 带授权码的注册
|
||||
var response = await client.RegisterDeviceAsync(new DeviceRegisterRequest {
|
||||
Uid = "device-001",
|
||||
DeviceModel = "test-model",
|
||||
License = "your-license-code-here"
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 设备验证
|
||||
|
||||
```csharp
|
||||
var validateResponse = await client.ValidateDeviceAsync("device-001");
|
||||
Console.WriteLine($"设备状态: {validateResponse.Status}");
|
||||
Console.WriteLine($"授权类型: {validateResponse.LicenseType}");
|
||||
Console.WriteLine($"过期时间: {validateResponse.ExpireTime}");
|
||||
```
|
||||
|
||||
### 3. 更新启动次数
|
||||
|
||||
```csharp
|
||||
var startCountResponse = await client.UpdateStartCountAsync("device-001");
|
||||
Console.WriteLine($"当前启动次数: {startCountResponse.Data.StartCount}");
|
||||
```
|
||||
|
||||
### 4. 绑定授权码
|
||||
|
||||
```csharp
|
||||
var bindLicenseResponse = await client.BindLicenseAsync("device-001", "your-license-code-here");
|
||||
Console.WriteLine($"绑定结果: {bindLicenseResponse.Data.Success}");
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
SDK 使用 `DeviceSDKException` 统一处理错误:
|
||||
|
||||
```csharp
|
||||
try {
|
||||
// 调用 API 的代码
|
||||
} catch (DeviceSDKException e) {
|
||||
Console.WriteLine($"错误码: {e.ErrorCode}, 错误信息: {e.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
## 响应数据结构
|
||||
|
||||
```csharp
|
||||
// 设备验证响应
|
||||
public class DeviceValidateResponse
|
||||
{
|
||||
public string Status { get; set; } // 设备状态
|
||||
public string LicenseType { get; set; } // 授权类型
|
||||
public DateTime ExpireTime { get; set; } // 过期时间
|
||||
public int StartCount { get; set; } // 启动次数
|
||||
public int MaxUses { get; set; } // 最大使用次数
|
||||
}
|
||||
// 启动次数响应
|
||||
public class StartCountResponse
|
||||
{
|
||||
public int Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
public StartCountData Data { get; set; }
|
||||
}
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
# Device SDK for Go
|
||||
|
||||
## 安装
|
||||
`go import "license-server/sdk/go/devicesdk"`
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 初始化客户端
|
||||
```go
|
||||
client := devicesdk.NewDeviceClient(
|
||||
"http://localhost:8080",
|
||||
"your-32-byte-encrypt-key-here123456",
|
||||
)
|
||||
```
|
||||
### 设备注册
|
||||
```go
|
||||
// 不带授权码注册
|
||||
err := client.RegisterDevice(&devicesdk.DeviceInfo{
|
||||
UID: "device-001",
|
||||
DeviceModel: "test-model",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("设备注册失败: %v", err)
|
||||
}
|
||||
// 带授权码注册
|
||||
err = client.RegisterDevice(&devicesdk.DeviceInfo{
|
||||
UID: "device-002",
|
||||
DeviceModel: "test-model",
|
||||
LicenseCode: "your-license-code", // 可选
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("设备注册失败: %v", err)
|
||||
}
|
||||
```
|
||||
### 设备验证
|
||||
```go
|
||||
validateResp, err := client.ValidateDevice("device-001")
|
||||
if err != nil {
|
||||
log.Fatalf("设备验证失败: %v", err)
|
||||
}
|
||||
fmt.Printf("设备状态: %s\n", validateResp.Status)
|
||||
fmt.Printf("授权类型: %s\n", validateResp.LicenseType)
|
||||
fmt.Printf("过期时间: %s\n", validateResp.ExpireTime)
|
||||
```
|
||||
### 更新启动次数
|
||||
```go
|
||||
count, err := client.UpdateStartCount("device-001")
|
||||
if err != nil {
|
||||
log.Fatalf("更新启动次数失败: %v", err)
|
||||
}
|
||||
fmt.Printf("当前启动次数: %d\n", count)
|
||||
```
|
||||
|
||||
### 绑定授权码
|
||||
```go
|
||||
err := client.BindLicense("device-001", "your-license-code")
|
||||
if err != nil {
|
||||
log.Fatalf("绑定授权码失败: %v", err)
|
||||
}
|
||||
```
|
||||
## 完整示例
|
||||
```go
|
||||
package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"license-server/sdk/go/devicesdk"
|
||||
)
|
||||
func main() {
|
||||
client := devicesdk.NewDeviceClient(
|
||||
"http://localhost:8080",
|
||||
"your-32-byte-encrypt-key-here123456",
|
||||
)
|
||||
// 注册设备
|
||||
err := client.RegisterDevice(&devicesdk.DeviceInfo{
|
||||
UID: "device-001",
|
||||
DeviceModel: "test-model",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("设备注册失败: %v", err)
|
||||
}
|
||||
fmt.Println("设备注册成功")
|
||||
// 绑定授权码
|
||||
err = client.BindLicense("device-001", "license-code-123")
|
||||
if err != nil {
|
||||
log.Fatalf("绑定授权码失败: %v", err)
|
||||
}
|
||||
fmt.Println("授权码绑定成功")
|
||||
// 验证设备
|
||||
validateResp, err := client.ValidateDevice("device-001")
|
||||
if err != nil {
|
||||
log.Fatalf("设备验证失败: %v", err)
|
||||
}
|
||||
fmt.Printf("设备状态: %s\n", validateResp.Status)
|
||||
fmt.Printf("授权类型: %s\n", validateResp.LicenseType)
|
||||
fmt.Printf("过期时间: %s\n", validateResp.ExpireTime)
|
||||
// 更新启动次数
|
||||
count, err := client.UpdateStartCount("device-001")
|
||||
if err != nil {
|
||||
log.Fatalf("更新启动次数失败: %v", err)
|
||||
}
|
||||
fmt.Printf("当前启动次数: %d\n", count)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,191 @@
|
|||
package devicesdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceClient struct {
|
||||
baseURL string
|
||||
encryptKey string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
UID string `json:"uid"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
LicenseCode string `json:"license_code,omitempty"`
|
||||
}
|
||||
|
||||
type ValidateResponse struct {
|
||||
Status string `json:"status"`
|
||||
LicenseType string `json:"license_type"`
|
||||
ExpireTime time.Time `json:"expire_time"`
|
||||
StartCount int `json:"start_count"`
|
||||
MaxUses int `json:"max_uses"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type ApiResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func NewDeviceClient(baseURL, encryptKey string) *DeviceClient {
|
||||
return &DeviceClient{
|
||||
baseURL: baseURL,
|
||||
encryptKey: encryptKey,
|
||||
client: &http.Client{Timeout: time.Second * 30},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DeviceClient) RegisterDevice(info *DeviceInfo) error {
|
||||
data, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化请求数据失败: %v", err)
|
||||
}
|
||||
|
||||
resp, err := c.post("/api/devices/register", data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Code != 0 {
|
||||
return fmt.Errorf(resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DeviceClient) ValidateDevice(uid string) (*ValidateResponse, error) {
|
||||
resp, err := c.get(fmt.Sprintf("/api/devices/%s/validate", uid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Code != 0 {
|
||||
return nil, fmt.Errorf(resp.Error)
|
||||
}
|
||||
|
||||
decrypted, err := c.decryptResponse(resp.Data.(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var validateResp ValidateResponse
|
||||
if err := json.Unmarshal([]byte(decrypted), &validateResp); err != nil {
|
||||
return nil, fmt.Errorf("解析响应数据失败: %v", err)
|
||||
}
|
||||
|
||||
return &validateResp, nil
|
||||
}
|
||||
|
||||
func (c *DeviceClient) UpdateStartCount(uid string) (int, error) {
|
||||
resp, err := c.post(fmt.Sprintf("/api/devices/%s/start", uid), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.Code != 0 {
|
||||
return 0, fmt.Errorf(resp.Error)
|
||||
}
|
||||
|
||||
data := resp.Data.(map[string]interface{})
|
||||
return int(data["start_count"].(float64)), nil
|
||||
}
|
||||
|
||||
func (c *DeviceClient) BindLicense(uid, licenseCode string) error {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"license_code": licenseCode,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化请求数据失败: %v", err)
|
||||
}
|
||||
|
||||
resp, err := c.post(fmt.Sprintf("/api/devices/%s/license", uid), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Code != 0 {
|
||||
return fmt.Errorf(resp.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTTP 请求辅助方法
|
||||
func (c *DeviceClient) get(path string) (*ApiResponse, error) {
|
||||
resp, err := c.client.Get(c.baseURL + path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return c.parseResponse(resp)
|
||||
}
|
||||
|
||||
func (c *DeviceClient) post(path string, data []byte) (*ApiResponse, error) {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
if data == nil {
|
||||
resp, err = c.client.Post(c.baseURL+path, "application/json", nil)
|
||||
} else {
|
||||
resp, err = c.client.Post(c.baseURL+path, "application/json", bytes.NewBuffer(data))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return c.parseResponse(resp)
|
||||
}
|
||||
|
||||
func (c *DeviceClient) parseResponse(resp *http.Response) (*ApiResponse, error) {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应数据失败: %v", err)
|
||||
}
|
||||
|
||||
var apiResp ApiResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("解析响应数据失败: %v", err)
|
||||
}
|
||||
|
||||
return &apiResp, nil
|
||||
}
|
||||
|
||||
func (c *DeviceClient) decryptResponse(encrypted string) (string, error) {
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64解码失败: %v", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher([]byte(c.encryptKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES cipher失败: %v", err)
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", fmt.Errorf("密文长度不足")
|
||||
}
|
||||
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
|
||||
return string(ciphertext), nil
|
||||
}
|
|
@ -2,10 +2,17 @@ layui.use(['layer'], function(){
|
|||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 初始化图表
|
||||
var registerTrendChart = echarts.init(document.getElementById('register-trend'));
|
||||
var deviceTypesChart = echarts.init(document.getElementById('device-types'));
|
||||
|
||||
// 加载统计数据
|
||||
function loadStats() {
|
||||
fetch('/api/dashboard/stats', {
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
|
@ -21,21 +28,157 @@ layui.use(['layer'], function(){
|
|||
}
|
||||
|
||||
// 更新统计数据
|
||||
$('#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);
|
||||
updateStats(result.data);
|
||||
// 更新图表
|
||||
updateCharts(result.data);
|
||||
// 更新系统状态
|
||||
updateSystemStatus(result.data);
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('加载统计数据失败:' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
function updateStats(data) {
|
||||
$('#total-devices').text(data.total_devices);
|
||||
$('#total-licenses').text(data.total_licenses);
|
||||
$('#online-devices').text(data.online_devices);
|
||||
$('#expired-devices').text(data.expired_devices);
|
||||
$('#today-new').text(data.today_new);
|
||||
$('#unused-licenses').text(data.unused_licenses);
|
||||
|
||||
// 计算比率
|
||||
var activeRate = data.total_devices > 0 ?
|
||||
((data.online_devices / data.total_devices) * 100).toFixed(1) : 0;
|
||||
var expiredRate = data.total_devices > 0 ?
|
||||
((data.expired_devices / data.total_devices) * 100).toFixed(1) : 0;
|
||||
|
||||
$('#active-rate').text(activeRate + '%');
|
||||
$('#expired-rate').text(expiredRate + '%');
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateCharts(data) {
|
||||
// 设备注册趋势图
|
||||
var trendOption = {
|
||||
title: {
|
||||
text: '最近7天设备注册趋势'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.trend_dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: '新增设备',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: data.trend_counts,
|
||||
itemStyle: {
|
||||
color: '#009688'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(0, 150, 136, 0.3)'
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgba(0, 150, 136, 0.1)'
|
||||
}])
|
||||
}
|
||||
}]
|
||||
};
|
||||
registerTrendChart.setOption(trendOption);
|
||||
|
||||
// 设备类型分布图
|
||||
var typesOption = {
|
||||
title: {
|
||||
text: '设备类型分布'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: data.device_types.map(item => ({
|
||||
name: item.type,
|
||||
value: item.count
|
||||
}))
|
||||
}]
|
||||
};
|
||||
deviceTypesChart.setOption(typesOption);
|
||||
}
|
||||
|
||||
// 更新系统状态
|
||||
function updateSystemStatus(data) {
|
||||
$('#cpu-usage').text(data.cpu_usage + '%');
|
||||
$('#memory-usage').text(data.memory_usage + '%');
|
||||
$('#disk-usage').text(data.disk_usage + '%');
|
||||
$('#uptime').text(formatDuration(data.uptime));
|
||||
$('#load-avg').text(data.load_avg.join(' '));
|
||||
$('#network-traffic').text(formatBytes(data.network_traffic) + '/s');
|
||||
$('#online-users').text(data.online_users);
|
||||
$('#last-update').text(new Date().toLocaleString());
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
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分钟';
|
||||
}
|
||||
|
||||
// 格式化字节大小
|
||||
function formatBytes(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];
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadStats();
|
||||
|
||||
// 定时刷新(每30秒)
|
||||
setInterval(loadStats, 30000);
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
window.onresize = function() {
|
||||
registerTrendChart.resize();
|
||||
deviceTypesChart.resize();
|
||||
};
|
||||
});
|
|
@ -1,36 +1,24 @@
|
|||
layui.use(['table', 'form', 'layer'], function(){
|
||||
layui.use(['table', 'form', 'layer', 'laytpl'], function(){
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var laytpl = layui.laytpl;
|
||||
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,
|
||||
url: '/api/devices/registered',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'uid', title: '设备UID', width: 180},
|
||||
{field: 'device_model', title: '设备型号', width: 120},
|
||||
{field: 'device_type', title: '设备类型', width: 120},
|
||||
{field: 'license_code', title: '授权码', width: 180},
|
||||
{field: 'license_type', title: '授权类型', width: 100, templet: function(d){
|
||||
var types = {
|
||||
|
@ -40,15 +28,17 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
};
|
||||
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: 'start_count', title: '启动次数', width: 100, templet: function(d){
|
||||
if(d.license_type === 'count') {
|
||||
return d.start_count + ' / ' + d.max_uses;
|
||||
}
|
||||
return d.start_count || 0;
|
||||
}},
|
||||
{field: 'last_active_at', title: '最后活跃', width: 160, templet: function(d){
|
||||
return d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-';
|
||||
}},
|
||||
|
@ -79,11 +69,14 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
|
||||
switch(obj.event){
|
||||
case 'view':
|
||||
// 使用laytpl渲染设备详情
|
||||
laytpl(document.getElementById('deviceDetailTpl').innerHTML).render(data, function(html){
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '设备详情',
|
||||
area: ['600px', '500px'],
|
||||
content: laytpl($('#deviceDetailTpl').html()).render(data)
|
||||
area: ['600px', '600px'],
|
||||
content: html
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'bind':
|
||||
|
@ -161,42 +154,4 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -8,39 +8,22 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
table.render({
|
||||
elem: '#device-table',
|
||||
url: '/api/devices/models',
|
||||
headers: undefined,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
{type: 'checkbox'},
|
||||
{field: 'ID', hide: true},
|
||||
{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: 'device_type', title: '设备类型', width: 120},
|
||||
{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
|
||||
}) : '';
|
||||
if(d.status === 'active') return '<span class="layui-badge layui-bg-green">启用</span>';
|
||||
return '<span class="layui-badge layui-bg-gray">禁用</span>';
|
||||
}},
|
||||
{fixed: 'right', title: '操作', toolbar: '#tableRowBar', width: 180}
|
||||
]],
|
||||
|
@ -69,7 +52,7 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
return;
|
||||
}
|
||||
layer.confirm('确定删除选中的设备型号吗?', function(index){
|
||||
var ids = data.map(item => item.id);
|
||||
var ids = data.map(item => item.ID);
|
||||
fetch('/api/devices/models/batch', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
|
@ -108,22 +91,21 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
area: ['500px', '400px'],
|
||||
content: $('#deviceFormTpl').html(),
|
||||
success: function(){
|
||||
form.val('deviceForm', data);
|
||||
form.val('deviceForm', {
|
||||
id: data.ID,
|
||||
model_name: data.model_name,
|
||||
device_type: data.device_type,
|
||||
company: data.company,
|
||||
status: data.status || 'active',
|
||||
remark: data.remark
|
||||
});
|
||||
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, {
|
||||
fetch('/api/devices/models/' + data.ID, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
|
@ -150,7 +132,10 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
// 搜索表单提交
|
||||
form.on('submit(search)', function(data){
|
||||
table.reload('device-table', {
|
||||
where: data.field
|
||||
where: data.field,
|
||||
page: {
|
||||
curr: 1
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
@ -163,10 +148,6 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
area: ['500px', '400px'],
|
||||
content: $('#deviceFormTpl').html(),
|
||||
success: function(){
|
||||
// 初始化设备类型选择
|
||||
form.val('deviceForm', {
|
||||
'device_type': 'software' // 设置默认值
|
||||
});
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
|
@ -174,34 +155,27 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
|
||||
// 设备型号表单提交
|
||||
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'
|
||||
status: data.field.status,
|
||||
remark: data.field.remark
|
||||
};
|
||||
|
||||
// 如果是编辑模式,添加 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';
|
||||
// 确定请求URL和方法
|
||||
const url = data.field.id ?
|
||||
`/api/devices/models/${data.field.id}` :
|
||||
'/api/devices/models';
|
||||
const method = data.field.id ? 'PUT' : 'POST';
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
|
|
@ -17,7 +17,9 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
table.render({
|
||||
elem: '#license-table',
|
||||
url: '/api/licenses',
|
||||
headers: undefined,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
|
@ -215,7 +217,7 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
type: 2,
|
||||
title: '使用日志',
|
||||
area: ['800px', '600px'],
|
||||
content: '/admin/license-logs?id=' + data.id
|
||||
content: '/admin/license-logs?id=' + data.id + '&token=' + localStorage.getItem('token')
|
||||
});
|
||||
break;
|
||||
case 'revoke':
|
||||
|
@ -316,7 +318,8 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
fetch('/api/licenses', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
|
|
|
@ -3,18 +3,8 @@ layui.use(['form', 'layer'], function(){
|
|||
var layer = layui.layer;
|
||||
var $ = layui.$;
|
||||
|
||||
// 检查用户是否已登录
|
||||
function checkIfLoggedIn() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
checkIfLoggedIn();
|
||||
|
||||
// 加载验证码
|
||||
var captchaId = '';
|
||||
function loadCaptcha() {
|
||||
fetch('/api/captcha')
|
||||
.then(response => {
|
||||
|
@ -32,6 +22,7 @@ layui.use(['form', 'layer'], function(){
|
|||
if (data.imageBase64) {
|
||||
$('#captchaImg').attr('src', 'data:image/png;base64,' + data.imageBase64);
|
||||
$('input[name=captchaId]').val(data.captchaId);
|
||||
captchaId = data.captchaId;
|
||||
} else {
|
||||
throw new Error('验证码图片数据无效');
|
||||
}
|
||||
|
@ -42,63 +33,39 @@ layui.use(['form', 'layer'], function(){
|
|||
$('#captchaImg').attr('src', '/static/images/captcha-error.png');
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时获取验证码
|
||||
loadCaptcha();
|
||||
|
||||
// 点击验证码图片刷新
|
||||
$('#captchaImg').on('click', function() {
|
||||
loadCaptcha();
|
||||
});
|
||||
// 点击验证码刷新
|
||||
$('#captchaImg').on('click', 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;
|
||||
}
|
||||
data.field.captchaId = captchaId;
|
||||
|
||||
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();
|
||||
body: JSON.stringify(data.field)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if(result.error) {
|
||||
layer.msg(result.error);
|
||||
loadCaptcha(); // 刷新验证码
|
||||
loadCaptcha();
|
||||
return;
|
||||
}
|
||||
// 确保 token 被正确设置
|
||||
document.cookie = `token=${result.token}; path=/; secure; samesite=strict`;
|
||||
localStorage.setItem('token', result.token);
|
||||
// 保存token到localStorage
|
||||
localStorage.setItem('token', result.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(result.data.user));
|
||||
|
||||
// 跳转到首页
|
||||
window.location.href = '/';
|
||||
})
|
||||
.catch(error => {
|
||||
layer.msg('登录失败:' + error.message);
|
||||
loadCaptcha(); // 刷新验证码
|
||||
loadCaptcha();
|
||||
});
|
||||
|
||||
return false;
|
||||
|
|
|
@ -24,28 +24,42 @@ layui.use(["element", "layer"], function () {
|
|||
}
|
||||
}
|
||||
|
||||
// 添加通用的 fetch 封装,自动处理认证
|
||||
|
||||
window.authFetch = function (url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
credentials: 'include', // 自动携带 cookie
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// 添加请求拦截器
|
||||
function addAuthHeader(url) {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
throw new Error('认证失败');
|
||||
return false;
|
||||
}
|
||||
throw new Error(`请求失败,状态码:${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("请求处理失败:", error);
|
||||
throw error;
|
||||
});
|
||||
|
||||
return {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
// 封装fetch请求
|
||||
async function request(url, options = {}) {
|
||||
const headers = addAuthHeader(url);
|
||||
if (!headers) return;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 在页面加载时检查认证
|
||||
|
||||
|
@ -99,7 +113,7 @@ layui.use(["element", "layer"], function () {
|
|||
|
||||
// 加载页面内容
|
||||
|
||||
$("#content-frame").attr("src", url);
|
||||
$("#content-frame").attr("src", url+"?token="+localStorage.getItem('token'));
|
||||
|
||||
// 更新选中状态
|
||||
|
||||
|
|
|
@ -6,7 +6,10 @@ layui.use(['form', 'upload', 'layer'], function(){
|
|||
|
||||
// 加载当前配置
|
||||
fetch('/api/site/settings', {
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
|
@ -112,7 +115,8 @@ layui.use(['form', 'upload', 'layer'], function(){
|
|||
fetch('/api/site/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(submitData)
|
||||
|
|
|
@ -8,7 +8,9 @@ layui.use(['table', 'form', 'layer'], function(){
|
|||
table.render({
|
||||
elem: '#user-table',
|
||||
url: '/api/users',
|
||||
headers: undefined,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
toolbar: '#tableToolbar',
|
||||
defaultToolbar: ['filter', 'exports', 'print'],
|
||||
cols: [[
|
||||
|
|
|
@ -4,51 +4,137 @@
|
|||
<meta charset="utf-8">
|
||||
<title>控制台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.stat-title {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.stat-number {
|
||||
color: #009688;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-icon {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
color: #009688;
|
||||
}
|
||||
.stat-trend {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.trend-up {
|
||||
color: #5FB878;
|
||||
}
|
||||
.trend-down {
|
||||
color: #FF5722;
|
||||
}
|
||||
</style>
|
||||
</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 class="stat-card">
|
||||
<i class="layui-icon layui-icon-component stat-icon"></i>
|
||||
<div class="stat-title">设备总数</div>
|
||||
<div class="stat-number" id="total-devices">0</div>
|
||||
<div class="stat-trend">
|
||||
今日新增: <span id="today-new">0</span>
|
||||
</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 class="stat-card">
|
||||
<i class="layui-icon layui-icon-vercode stat-icon"></i>
|
||||
<div class="stat-title">授权码总数</div>
|
||||
<div class="stat-number" id="total-licenses">0</div>
|
||||
<div class="stat-trend">
|
||||
未使用: <span id="unused-licenses">0</span>
|
||||
</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 class="stat-card">
|
||||
<i class="layui-icon layui-icon-circle-dot stat-icon"></i>
|
||||
<div class="stat-title">在线设备</div>
|
||||
<div class="stat-number" id="online-devices">0</div>
|
||||
<div class="stat-trend">
|
||||
活跃率: <span id="active-rate">0%</span>
|
||||
</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 class="stat-card">
|
||||
<i class="layui-icon layui-icon-chart stat-icon"></i>
|
||||
<div class="stat-title">过期设备</div>
|
||||
<div class="stat-number" id="expired-devices">0</div>
|
||||
<div class="stat-trend">
|
||||
占比: <span id="expired-rate">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="layui-col-md8">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">设备注册趋势</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="register-trend" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-md4">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">设备类型分布</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="device-types" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态 -->
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">系统状态</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-row layui-col-space10">
|
||||
<div class="layui-col-md3">
|
||||
<div>CPU使用率:<span id="cpu-usage">0%</span></div>
|
||||
<div>内存使用率:<span id="memory-usage">0%</span></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>系统运行时间:<span id="uptime">0天</span></div>
|
||||
<div>磁盘使用率:<span id="disk-usage">0%</span></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>系统负载:<span id="load-avg">0</span></div>
|
||||
<div>网络流量:<span id="network-traffic">0 KB/s</span></div>
|
||||
</div>
|
||||
<div class="layui-col-md3">
|
||||
<div>当前在线用户:<span id="online-users">0</span></div>
|
||||
<div>最后更新时间:<span id="last-update">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/layui/layui.js"></script>
|
||||
<script src="/static/lib/echarts.min.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -72,7 +72,7 @@
|
|||
<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'){ }}
|
||||
{{# if(d.status !== 'expired' && d.license_code){ }}
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="revoke">撤销</a>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
@ -94,13 +94,33 @@
|
|||
<td>设备型号</td>
|
||||
<td>{{ d.device_model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>设备类型</td>
|
||||
<td>{{ d.device_type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>所属公司</td>
|
||||
<td>{{ d.company || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权码</td>
|
||||
<td>{{ d.license_code || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权类型</td>
|
||||
<td>{{ d.license_type || '-' }}</td>
|
||||
<td>{{ {
|
||||
'time': '时间授权',
|
||||
'count': '次数授权',
|
||||
'permanent': '永久授权'
|
||||
}[d.license_type] || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>授权状态</td>
|
||||
<td>{{ {
|
||||
'active': '<span class="layui-badge layui-bg-green">正常</span>',
|
||||
'expired': '<span class="layui-badge layui-bg-orange">已过期</span>',
|
||||
'inactive': '<span class="layui-badge layui-bg-gray">未激活</span>'
|
||||
}[d.status] || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>过期时间</td>
|
||||
|
@ -108,16 +128,16 @@
|
|||
</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>
|
||||
<td>{{ d.start_count || 0 }}{{ d.max_uses ? ' / ' + d.max_uses : '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>注册时间</td>
|
||||
<td>{{ new Date(d.register_time).toLocaleString() }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最后活跃</td>
|
||||
<td>{{ d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -119,16 +119,12 @@
|
|||
<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">
|
||||
|
@ -180,6 +176,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" name="status" value="active" title="启用" checked>
|
||||
<input type="radio" name="status" value="disabled" title="禁用">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">备注说明</label>
|
||||
|
||||
|
|
|
@ -4,45 +4,86 @@
|
|||
<meta charset="utf-8">
|
||||
<title>登录 - 授权验证管理平台</title>
|
||||
<link rel="stylesheet" href="/static/layui/css/layui.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.login-container {
|
||||
width: 360px;
|
||||
padding: 30px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.1);
|
||||
}
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.captcha-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.captcha-img {
|
||||
height: 38px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.layui-input {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.layui-btn {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
</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="">
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2 class="login-title">授权验证管理平台</h2>
|
||||
<form class="layui-form login-form">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block" style="margin-left: 0;">
|
||||
<input type="text" name="username" required lay-verify="required"
|
||||
placeholder="用户名" autocomplete="off" class="layui-input">
|
||||
placeholder="请输入用户名" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block" style="margin-left: 0;">
|
||||
<input type="password" name="password" required lay-verify="required"
|
||||
placeholder="密码" autocomplete="off" class="layui-input">
|
||||
placeholder="请输入密码" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-inline" style="width: 200px;">
|
||||
<div class="layui-input-block captcha-container" style="margin-left: 0;">
|
||||
<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">
|
||||
placeholder="请输入验证码" autocomplete="off" class="layui-input captcha-input">
|
||||
<img id="captchaImg" class="captcha-img" alt="验证码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
|
||||
<div class="layui-input-block" style="margin-left: 0;">
|
||||
<button class="layui-btn layui-btn-normal" 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>
|
||||
|
|
Loading…
Reference in New Issue