From 87859c7bb88ba9fa929ac0e7cbe87b4ce200fff2 Mon Sep 17 00:00:00 2001 From: JiXieShi Date: Sat, 16 Nov 2024 23:59:15 +0800 Subject: [PATCH] up --- api-docs.yaml | 126 +++++++++++++- cmd/main.go | 5 +- config.yaml | 31 ++++ data/license.db | Bin 217088 -> 225280 bytes go.mod | 12 +- go.sum | 26 ++- internal/api/dashboard.go | 46 ++--- internal/api/device.go | 111 ++++++++---- internal/api/router.go | 6 +- internal/api/user.go | 49 +++--- internal/middleware/auth.go | 18 +- internal/middleware/jwt.go | 1 + internal/model/device.go | 1 + internal/model/device_log.go | 12 +- internal/service/dashboard.go | 94 ++++++++++ internal/service/device.go | 102 +++++------ internal/service/user.go | 45 +++++ internal/utils/config.go | 40 +++-- internal/utils/crypto.go | 79 +++++++++ internal/utils/database.go | 2 + internal/utils/test_helper.go | 23 +-- project_structure | 1 + sdk/cpp/README.md | 146 ++++++++++++++++ sdk/cpp/device_sdk.cpp | 100 +++++++++++ sdk/cpp/device_sdk.h | 61 +++++++ sdk/csharp/DeviceSDK.cs | 220 ++++++++++++++++++++++++ sdk/csharp/README.md | 90 ++++++++++ sdk/csharp/docs/README.md | 1 + sdk/go/README.md | 104 +++++++++++ sdk/go/device_sdk.go | 191 ++++++++++++++++++++ web/static/js/dashboard.js | 157 ++++++++++++++++- web/static/js/device-license.js | 87 +++------- web/static/js/devices.js | 88 ++++------ web/static/js/licenses.js | 9 +- web/static/js/login.js | 67 ++------ web/static/js/main.js | 56 +++--- web/static/js/site-settings.js | 8 +- web/static/js/users.js | 4 +- web/templates/admin/dashboard.html | 124 +++++++++++-- web/templates/admin/device-license.html | 34 +++- web/templates/admin/devices.html | 15 +- web/templates/login.html | 111 ++++++++---- 42 files changed, 2018 insertions(+), 485 deletions(-) create mode 100644 config.yaml create mode 100644 internal/middleware/jwt.go create mode 100644 internal/model/device.go create mode 100644 internal/service/dashboard.go create mode 100644 internal/utils/crypto.go create mode 100644 project_structure create mode 100644 sdk/cpp/README.md create mode 100644 sdk/cpp/device_sdk.cpp create mode 100644 sdk/cpp/device_sdk.h create mode 100644 sdk/csharp/DeviceSDK.cs create mode 100644 sdk/csharp/README.md create mode 100644 sdk/csharp/docs/README.md create mode 100644 sdk/go/README.md create mode 100644 sdk/go/device_sdk.go diff --git a/api-docs.yaml b/api-docs.yaml index 52f9832..8839f7d 100644 --- a/api-docs.yaml +++ b/api-docs.yaml @@ -275,7 +275,7 @@ paths: description: 生成数量 remark: type: string - description: ���注说明 + 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 diff --git a/cmd/main.go b/cmd/main.go index a7a1200..7805b2d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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, ) // 创建必要的目录 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..ab0e67e --- /dev/null +++ b/config.yaml @@ -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" \ No newline at end of file diff --git a/data/license.db b/data/license.db index 2153889be2cbaccb077752a306970cf2596c9037..18546aa06ddbbe8c16c1334088ec2037b3ccd413 100644 GIT binary patch delta 1564 zcmb_cU1-}@6xPkP71@n^ls>+32>1JnULdGBdKKj0q&J zr0;&`-1D99EPTo@{Kzl%MGFkWr0KnPo%!$;4HZ_+l@kIBOAI`Ld$0lPunJ3!XzYC$ zim-SfRJJbKC2Op5VbTc)ao_H{6Aj=&wEM4mp=?zgwy`m!%Pazc0f7>*Fki2t<4i8c z6nI|j7IUY0uAk?)oGPojq$rZ2$5bs#NLI@v71_{rqP#2{Sy?_5P$U_7LRZRCu~@Ps zU7gS*!$|8A$&?8(^a(>A*S6M&zWC|u>-qDq7eddHSZm)713QhVxeb`oGyeKa(PSeHZ z1$2F(BVbZxZZYr}?!uq21-IaKBPxFhLJ0RD$E|8A8^nNWLGSK%#~{VtXDI4E?7$!J zD?FlU{Qz75bBLu6@0V>PQp^n+xb+`yz;~4I8oU(13}R2=9NIs0sws`8WK}sS8)_z{ z8oM=mc%q}1idn%)*}C@4 z=1TVM2aV4+FTeNToyE;}KXGPDCChR6PKklwIR;*j{2rO(pLG4!RqqOMABR5+i=nGr zckmp$%92J@cs}@IeVTOk3=E(*2i@^v#cFH#wxxO{)^*&QT0%44=v(Nh_rVH^ct1;+ z_ijrVPRC4p+KjnY%}vauJA2L!pnAVORkmtξ2DL)R6{waR1TZrj(iHxkDprXIWU z#*yA(-)v^w{2OZ~=2A~*^-Vf!m)lrVK5L=r+`SYiz>MWaVw8H{)qbG8?Pel$lG*aB z5&f^#h@iw;cN||rM_9u8!=~P$dA)J`g7+wa!)xCr@cHKimd`Wr5yT^3MTYrxK2NnE z{QOKH(rhKMUlb=EY9*&- z^g0)8M#@2-IDb$mRnHPRNo1ms6geo=p@0K?RJ&vq9ow~9xj0TM7i4+EBRG4sb%JzO bGm?gp(REE-yE=j^DEvRooK7kW#!Toh!e^*P delta 390 zcmZp8z}v8ZcY?H_Hv32d3-%69f3BzkSarjw&R}!py+P z0wS0}1QRne11HdRP_Ug#fH|3evVy>5C0r2qf` diff --git a/go.mod b/go.mod index 7c7c030..78c3d07 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index bb179c9..b4c0944 100644 --- a/go.sum +++ b/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= diff --git a/internal/api/dashboard.go b/internal/api/dashboard.go index 29c1eff..19d039b 100644 --- a/internal/api/dashboard.go +++ b/internal/api/dashboard.go @@ -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, + }) } diff --git a/internal/api/device.go b/internal/api/device.go index 03c7bed..051f0dc 100644 --- a/internal/api/device.go +++ b/internal/api/device.go @@ -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, - }) -} diff --git a/internal/api/router.go b/internal/api/router.go index 4edfb4d..08fb617 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -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) } } diff --git a/internal/api/user.go b/internal/api/user.go index 61e5d30..9bce9b9 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -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": "重置密邮件已发送"}) } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index f2c81bc..002941a 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -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() } } diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/internal/middleware/jwt.go @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/model/device.go b/internal/model/device.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/internal/model/device.go @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/model/device_log.go b/internal/model/device_log.go index 8d3fe73..259e797 100644 --- a/internal/model/device_log.go +++ b/internal/model/device_log.go @@ -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" +) diff --git a/internal/service/dashboard.go b/internal/service/dashboard.go new file mode 100644 index 0000000..1bb2dfc --- /dev/null +++ b/internal/service/dashboard.go @@ -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 +} diff --git a/internal/service/device.go b/internal/service/device.go index 4ab640c..510aa3a 100644 --- a/internal/service/device.go +++ b/internal/service/device.go @@ -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���否已存在 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"` // 过期设备 -} - -// 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 +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("设备不存在") + } + return &device, nil } diff --git a/internal/service/user.go b/internal/service/user.go index 3978f30..8db1f24 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -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) +} diff --git a/internal/utils/config.go b/internal/utils/config.go index 122663c..a5b6698 100644 --- a/internal/utils/config.go +++ b/internal/utils/config.go @@ -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) { diff --git a/internal/utils/crypto.go b/internal/utils/crypto.go new file mode 100644 index 0000000..d2ff8e2 --- /dev/null +++ b/internal/utils/crypto.go @@ -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 +} diff --git a/internal/utils/database.go b/internal/utils/database.go index 1719aa4..2840c8d 100644 --- a/internal/utils/database.go +++ b/internal/utils/database.go @@ -28,6 +28,8 @@ func InitDB(config *DatabaseConfig) (*gorm.DB, error) { &model.DeviceModel{}, + &model.DeviceLog{}, + &model.PasswordResetToken{}, &model.Captcha{}, diff --git a/internal/utils/test_helper.go b/internal/utils/test_helper.go index 6b9f46f..8761f0e 100644 --- a/internal/utils/test_helper.go +++ b/internal/utils/test_helper.go @@ -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", }, } } diff --git a/project_structure b/project_structure new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/project_structure @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/cpp/README.md b/sdk/cpp/README.md new file mode 100644 index 0000000..b6fed68 --- /dev/null +++ b/sdk/cpp/README.md @@ -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 +#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; +} +``` diff --git a/sdk/cpp/device_sdk.cpp b/sdk/cpp/device_sdk.cpp new file mode 100644 index 0000000..ff15eb2 --- /dev/null +++ b/sdk/cpp/device_sdk.cpp @@ -0,0 +1,100 @@ +#include "device_sdk.h" +#include +#include +#include + +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 请求和加密相关的辅助函数实现 ... \ No newline at end of file diff --git a/sdk/cpp/device_sdk.h b/sdk/cpp/device_sdk.h new file mode 100644 index 0000000..3a70f35 --- /dev/null +++ b/sdk/cpp/device_sdk.h @@ -0,0 +1,61 @@ +#ifndef DEVICE_SDK_H +#define DEVICE_SDK_H + +#ifdef _WIN32 + #include + #include +#else + #include +#endif + +#include +#include +#include +#include + +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 \ No newline at end of file diff --git a/sdk/csharp/DeviceSDK.cs b/sdk/csharp/DeviceSDK.cs new file mode 100644 index 0000000..2296614 --- /dev/null +++ b/sdk/csharp/DeviceSDK.cs @@ -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 RegisterDeviceAsync(DeviceRegisterRequest request) + { + try + { + var response = await PostAsync("/api/devices/register", request); + return JsonSerializer.Deserialize(response); + } + catch (Exception ex) + { + throw new DeviceSDKException("设备注册失败", ex); + } + } + + // 设备验证 + public async Task ValidateDeviceAsync(string uid) + { + try + { + var response = await GetAsync($"/api/devices/{uid}/validate"); + var result = JsonSerializer.Deserialize(response); + + if (result.Code != 0) + { + throw new DeviceSDKException(result.Error); + } + + var decrypted = DecryptResponse(result.Data); + return JsonSerializer.Deserialize(decrypted); + } + catch (Exception ex) + { + throw new DeviceSDKException("设备验证失败", ex); + } + } + + // 更新启动次数 + public async Task UpdateStartCountAsync(string uid) + { + try + { + var response = await PostAsync($"/api/devices/{uid}/start", null); + return JsonSerializer.Deserialize(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(response); + + if (result.Code != 0) + { + throw new DeviceSDKException(result.Error); + } + } + catch (Exception ex) + { + throw new DeviceSDKException("绑定授权码失败", ex); + } + } + + private async Task GetAsync(string path) + { + var response = await _httpClient.GetAsync(_baseUrl + path); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + private async Task 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) { } + } +} \ No newline at end of file diff --git a/sdk/csharp/README.md b/sdk/csharp/README.md new file mode 100644 index 0000000..bccc3c8 --- /dev/null +++ b/sdk/csharp/README.md @@ -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; } +} +``` diff --git a/sdk/csharp/docs/README.md b/sdk/csharp/docs/README.md new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/sdk/csharp/docs/README.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sdk/go/README.md b/sdk/go/README.md new file mode 100644 index 0000000..c7a671c --- /dev/null +++ b/sdk/go/README.md @@ -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) +} +``` \ No newline at end of file diff --git a/sdk/go/device_sdk.go b/sdk/go/device_sdk.go new file mode 100644 index 0000000..1055740 --- /dev/null +++ b/sdk/go/device_sdk.go @@ -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 +} diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 85642a2..a6081d6 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -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(); + }; }); \ No newline at end of file diff --git a/web/static/js/device-license.js b/web/static/js/device-license.js index c4cfcad..478dffd 100644 --- a/web/static/js/device-license.js +++ b/web/static/js/device-license.js @@ -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 = ''; - result.data.forEach(function(model) { - options += ''; - }); - $('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 '正常'; if(d.status === 'expired') return '已过期'; return '未激活'; }}, + {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': - layer.open({ - type: 1, - title: '设备详情', - area: ['600px', '500px'], - content: laytpl($('#deviceDetailTpl').html()).render(data) + // 使用laytpl渲染设备详情 + laytpl(document.getElementById('deviceDetailTpl').innerHTML).render(data, function(html){ + layer.open({ + type: 1, + title: '设备详情', + 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); - } - }); }); \ No newline at end of file diff --git a/web/static/js/devices.js b/web/static/js/devices.js index f32dbd5..421f112 100644 --- a/web/static/js/devices.js +++ b/web/static/js/devices.js @@ -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 '已激活'; - if(d.status === 'expired') return '已过期'; - return '未激活'; - }}, - {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 '启用'; + return '禁用'; }}, {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()) diff --git a/web/static/js/licenses.js b/web/static/js/licenses.js index b3595d2..2799fd3 100644 --- a/web/static/js/licenses.js +++ b/web/static/js/licenses.js @@ -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) diff --git a/web/static/js/login.js b/web/static/js/login.js index 9f15380..40260b8 100644 --- a/web/static/js/login.js +++ b/web/static/js/login.js @@ -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,65 +33,41 @@ 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) { + 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; }); diff --git a/web/static/js/main.js b/web/static/js/main.js index 281fe5f..878cd02 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -24,28 +24,42 @@ layui.use(["element", "layer"], function () { } } - // 添加通用的 fetch 封装,自动处理认证 + // 添加请求拦截器 + function addAuthHeader(url) { + const token = localStorage.getItem('token'); + if (!token) { + window.location.href = '/login'; + return false; + } - window.authFetch = function (url, options = {}) { - return fetch(url, { + 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, - credentials: 'include', // 自动携带 cookie - }) - .then((response) => { - if (!response.ok) { - if (response.status === 401) { - window.location.href = '/login'; - throw new Error('认证失败'); - } - throw new Error(`请求失败,状态码:${response.status}`); - } - return response.json(); - }) - .catch((error) => { - console.error("请求处理失败:", error); - throw error; - }); - }; + 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')); // 更新选中状态 diff --git a/web/static/js/site-settings.js b/web/static/js/site-settings.js index a4fb4f5..df026ab 100644 --- a/web/static/js/site-settings.js +++ b/web/static/js/site-settings.js @@ -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) diff --git a/web/static/js/users.js b/web/static/js/users.js index d5dde27..6f21953 100644 --- a/web/static/js/users.js +++ b/web/static/js/users.js @@ -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: [[ diff --git a/web/templates/admin/dashboard.html b/web/templates/admin/dashboard.html index 1e043b3..ba63ad6 100644 --- a/web/templates/admin/dashboard.html +++ b/web/templates/admin/dashboard.html @@ -4,51 +4,137 @@ 控制台 - +
+
-
-
设备总数
-
0
+
+ +
设备总数
+
0
+
+ 今日新增: 0 +
-
-
授权码总数
-
0
+
+ +
授权码总数
+
0
+
+ 未使用: 0 +
-
-
今日新增
-
0
+
+ +
在线设备
+
0
+
+ 活跃率: 0% +
-
-
在线设备
-
0
+
+ +
过期设备
+
0
+
+ 占比: 0% +
-
+ + +
-
激活设备
-
0
+
设备注册趋势
+
+
+
-
+
-
过期设备
-
0
+
设备类型分布
+
+
+
+
+
+ + +
+
+
系统状态
+
+
+
+
CPU使用率:0%
+
内存使用率:0%
+
+
+
系统运行时间:0天
+
磁盘使用率:0%
+
+
+
系统负载:0
+
网络流量:0 KB/s
+
+
+
当前在线用户:0
+
最后更新时间:-
+
+
+
+ \ No newline at end of file diff --git a/web/templates/admin/device-license.html b/web/templates/admin/device-license.html index c2e1c3b..2bb2e7a 100644 --- a/web/templates/admin/device-license.html +++ b/web/templates/admin/device-license.html @@ -72,7 +72,7 @@ 查看 绑定授权 日志 - {{# if(d.status !== 'expired'){ }} + {{# if(d.status !== 'expired' && d.license_code){ }} 撤销 {{# } }} @@ -94,13 +94,33 @@ 设备型号 {{ d.device_model }} + + 设备类型 + {{ d.device_type }} + + + 所属公司 + {{ d.company || '-' }} + 授权码 {{ d.license_code || '-' }} 授权类型 - {{ d.license_type || '-' }} + {{ { + 'time': '时间授权', + 'count': '次数授权', + 'permanent': '永久授权' + }[d.license_type] || '-' }} + + + 授权状态 + {{ { + 'active': '正常', + 'expired': '已过期', + 'inactive': '未激活' + }[d.status] || '-' }} 过期时间 @@ -108,16 +128,16 @@ 启动次数 - {{ d.start_count }} - - - 最后活跃 - {{ d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-' }} + {{ d.start_count || 0 }}{{ d.max_uses ? ' / ' + d.max_uses : '' }} 注册时间 {{ new Date(d.register_time).toLocaleString() }} + + 最后活跃 + {{ d.last_active_at ? new Date(d.last_active_at).toLocaleString() : '-' }} +
diff --git a/web/templates/admin/devices.html b/web/templates/admin/devices.html index 9b420cf..3396361 100644 --- a/web/templates/admin/devices.html +++ b/web/templates/admin/devices.html @@ -119,16 +119,12 @@ - +