🎨 优化登录逻辑,添加设备ID和名称生成,更新二维码状态处理
All checks were successful
BuildImage / build-image (push) Successful in 2m3s

This commit is contained in:
李寻欢 2025-04-07 13:51:21 +08:00
parent 37b766368f
commit 2f6b3fac01
12 changed files with 179 additions and 72 deletions

View File

@ -19,9 +19,9 @@ docker:
imageName: "lxh01/xybotv2:latest"
network: "bridge"
redis:
host: "localhost"
password: "password"
db: 0
host: "10.0.0.31"
password: "pGhQKwj7DE7FbFL1"
db: 2
auth:
secretKey: "dev-secret-key"

1
go.mod
View File

@ -14,6 +14,7 @@ require (
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
gorm.io/plugin/soft_delete v1.2.1
)
require (

8
go.sum
View File

@ -64,6 +64,8 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -81,6 +83,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
@ -243,10 +246,15 @@ gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
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/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

View File

@ -44,7 +44,13 @@ type CheckUuidResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data struct {
Status int `json:"Status"`
Uuid string `json:"uuid"`
Status int `json:"status"` // 状态
PushLoginUrlExpiredTime int `json:"pushLoginUrlexpiredTime"` // 推送登录url过期时间
ExpiredTime int `json:"expiredTime"` // 过期时间(秒)
HeadImgUrl string `json:"headImgUrl"` // 头像
NickName string `json:"nickName"` // 昵称
AcctSectResp map[string]any `json:"acctSectResp"` // 账号信息-登录成功之后才有
} `json:"data"`
}
@ -149,7 +155,7 @@ func GetQRCode(ctx context.Context, containerID string, containerHost string) (*
}
// CheckUuid 检查二维码状态
func CheckUuid(ctx context.Context, containerID string, uuid string, containerHost string) (*CheckUuidResponse, error) {
func CheckUuid(ctx context.Context, uuid string, containerHost string) (*CheckUuidResponse, error) {
client := newHTTPClient()
url := fmt.Sprintf("http://%s/CheckUuid", containerHost)

View File

@ -2,6 +2,10 @@ package handler
import (
"context"
"encoding/json"
"errors"
"gitee.ltd/lxh/wechat-robot/internal/config"
"github.com/gofiber/fiber/v2/log"
"strconv"
"time"
@ -33,8 +37,8 @@ func CheckQRCodeStatus(c *fiber.Ctx) error {
// 获取机器人实例
db := model.GetDB()
var robot model.Robot
if err := db.First(&robot, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if err = db.First(&robot, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.JSON(fiber.Map{
"success": false,
"message": "机器人不存在",
@ -51,7 +55,7 @@ func CheckQRCodeStatus(c *fiber.Ctx) error {
defer cancel()
// 调用CheckUuid API检查二维码状态传递容器访问地址
response, err := docker.CheckUuid(ctx, robot.ContainerID, uuid, robot.ContainerHost)
response, err := docker.CheckUuid(ctx, uuid, robot.ContainerHost)
if err != nil {
return c.JSON(fiber.Map{
"success": false,
@ -59,8 +63,23 @@ func CheckQRCodeStatus(c *fiber.Ctx) error {
})
}
if cfg, _ := config.Load(); cfg.Server.Env == "development" {
bs, _ := json.Marshal(response)
log.Debugf("扫码返回结果: %s", bs)
}
// 如果返回status=1表示已扫码暂存一下昵称和头像
if response.Data.Status == 1 {
robot.Nickname = response.Data.NickName
robot.Avatar = response.Data.HeadImgUrl
db.Save(&robot)
}
// 如果检测到已登录,更新机器人状态
if response.Success && response.Data.Status == 2 {
if response.Success && response.Data.AcctSectResp != nil {
response.Data.Status = 99
robot.WechatID = response.Data.AcctSectResp["userName"].(string)
// 开启自动心跳,传递容器访问地址
if robot.WechatID != "" {
_, _ = docker.AutoHeartbeatStart(ctx, robot.ContainerID, robot.WechatID, robot.ContainerHost)
@ -74,8 +93,9 @@ func CheckQRCodeStatus(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{
"success": response.Success,
"status": response.Data.Status,
"message": response.Message,
"success": response.Success,
"status": response.Data.Status,
"message": response.Message,
"userInfo": response.Data.AcctSectResp,
})
}

View File

@ -2,6 +2,7 @@ package handler
import (
"context"
"errors"
"log"
"strconv"
"strings"
@ -79,9 +80,9 @@ func CreateRobot(c *fiber.Ctx) error {
}
db := model.GetDB()
if err := db.Create(&robot).Error; err != nil {
if err = db.Create(&robot).Error; err != nil {
// 如果数据库创建失败,尝试删除容器
docker.RemoveContainer(ctx, containerID, true)
_ = docker.RemoveContainer(ctx, containerID, true)
return fiber.NewError(fiber.StatusInternalServerError, "保存机器人信息失败: "+err.Error())
}
@ -133,7 +134,7 @@ func DeleteRobot(c *fiber.Ctx) error {
var robot model.Robot
db := model.GetDB()
if err := db.First(&robot, id).Error; err != nil {
if err = db.First(&robot, id).Error; err != nil {
// 针对API请求返回JSON
if strings.HasPrefix(c.Get("Accept"), "application/json") || c.Method() == "DELETE" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
@ -141,7 +142,7 @@ func DeleteRobot(c *fiber.Ctx) error {
"message": "机器人不存在",
})
}
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "机器人不存在")
}
return fiber.NewError(fiber.StatusInternalServerError, "查询数据库失败")
@ -152,14 +153,14 @@ func DeleteRobot(c *fiber.Ctx) error {
defer cancel()
if robot.Status == model.RobotStatusOnline {
if err := docker.LogoutWechatBot(ctx, robot.ContainerID); err != nil {
if err = docker.LogoutWechatBot(ctx, robot.ContainerID); err != nil {
log.Printf("登出机器人失败: %v", err)
// 继续删除流程,不因登出失败而中断
}
}
// 删除容器
if err := docker.RemoveContainer(ctx, robot.ContainerID, true); err != nil {
if err = docker.RemoveContainer(ctx, robot.ContainerID, true); err != nil {
log.Printf("删除容器失败: %v", err)
// 继续删除流程,不因容器删除失败而中断
}
@ -169,7 +170,7 @@ func DeleteRobot(c *fiber.Ctx) error {
monitor.RemoveRobot(robot.ContainerID)
// 删除数据库记录
if err := db.Delete(&robot).Error; err != nil {
if err = db.Delete(&robot).Error; err != nil {
// 针对API请求返回JSON
if strings.HasPrefix(c.Get("Accept"), "application/json") || c.Method() == "DELETE" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
@ -201,13 +202,16 @@ func RobotLogin(c *fiber.Ctx) error {
var robot model.Robot
db := model.GetDB()
if err := db.First(&robot, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if err = db.First(&robot, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "机器人不存在")
}
return fiber.NewError(fiber.StatusInternalServerError, "查询数据库失败")
}
// 处理一下设备信息
if robot.CheckDevice() {
db.Save(&robot)
}
// 获取登录二维码
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -219,12 +223,6 @@ func RobotLogin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "获取登录二维码失败: "+err.Error())
}
// 保存二维码相关信息到数据库
robot.QRCodePath = qrcodeResp.Data.QRCodeBase64
if err := db.Save(&robot).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "保存二维码信息失败")
}
// 渲染登录页面包含二维码和UUID用于后续状态检查
return c.Render("robot/login", fiber.Map{
"Title": "微信登录",
@ -245,8 +243,8 @@ func RobotLogout(c *fiber.Ctx) error {
var robot model.Robot
db := model.GetDB()
if err := db.First(&robot, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if err = db.First(&robot, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "机器人不存在")
}
return fiber.NewError(fiber.StatusInternalServerError, "查询数据库失败")
@ -258,20 +256,19 @@ func RobotLogout(c *fiber.Ctx) error {
// 使用新的LogOut API接口传递容器访问地址
if robot.WechatID != "" {
if _, err := docker.LogOut(ctx, robot.ContainerID, robot.WechatID, robot.ContainerHost); err != nil {
if _, err = docker.LogOut(ctx, robot.ContainerID, robot.WechatID, robot.ContainerHost); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "登出微信失败: "+err.Error())
}
} else {
// 如果没有WechatID使用原来的方法
if err := docker.LogoutWechatBot(ctx, robot.ContainerID); err != nil {
if err = docker.LogoutWechatBot(ctx, robot.ContainerID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "登出微信失败: "+err.Error())
}
}
// 更新机器人状态
robot.Status = model.RobotStatusOffline
robot.QRCodePath = ""
if err := db.Save(&robot).Error; err != nil {
if err = db.Save(&robot).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "更新状态失败")
}

View File

@ -3,13 +3,14 @@ package model
import (
"time"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
)
// BaseModel 是所有模型的基础结构
type BaseModel struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt int64 `json:"-" gorm:"index:deleted; default:0"`
IsDel soft_delete.DeletedAt `json:"-" gorm:"softDelete:flag,DeletedAtField:DeletedAt; index:deleted; default:0; type:tinyint(1)"`
}

View File

@ -1,6 +1,7 @@
package model
import (
"gitee.ltd/lxh/wechat-robot/internal/utils"
"time"
"gorm.io/gorm"
@ -18,15 +19,16 @@ const (
// Robot 表示微信机器人实例
type Robot struct {
BaseModel
ContainerID string `gorm:"column:container_id;uniqueIndex:idx_container_id,length:64" json:"container_id"`
ContainerID string `gorm:"column:container_id;index:deleted,unique,length:64" json:"container_id"`
ShortContainerID string `gorm:"column:short_container_id;index" json:"short_container_id"` // 容器ID的前12位
ContainerHost string `gorm:"column:container_host" json:"container_host"` // 容器访问地址格式为ip:port
WechatID string `gorm:"column:wechat_id;index:idx_wechat_id,length:64" json:"wechat_id"`
DeviceId string `gorm:"column:device_id;index:deleted,unique;" json:"deviceId"` // 设备Id
DeviceName string `gorm:"column:device_name" json:"deviceName"` // 设备名称
WechatID string `gorm:"column:wechat_id;index:deleted,unique,length:64" json:"wechat_id"`
Nickname string `gorm:"column:nickname" json:"nickname"`
Avatar string `gorm:"column:avatar" json:"avatar"`
Status RobotStatus `gorm:"column:status;default:'offline'" json:"status"`
LastLoginAt *time.Time `gorm:"column:last_login_at" json:"last_login_at"`
QRCodePath string `gorm:"column:qrcode_path" json:"qrcode_path"` // 二维码路径,用于登录
ErrorMessage string `gorm:"column:error_message" json:"error_message"`
}
@ -68,3 +70,22 @@ func (r *Robot) BeforeDelete(tx *gorm.DB) error {
// 在删除机器人之前,可以处理相关依赖
return nil
}
// CheckDevice
// @description: 检查设备信息
// @receiver r
// @return bool 是否重新处理过如果是就返回true
func (r *Robot) CheckDevice() (flag bool) {
// 如果是空的,就获取环境变量配置的值,如果还是空的,就生成一个新的
if r.DeviceId == "" {
r.DeviceId = utils.GetDeviceId()
flag = true
}
// 如果是空的,就获取环境变量配置的值,如果还是空的,就生成一个新的
if r.DeviceName == "" {
r.DeviceName = utils.GetDeviceName()
flag = true
}
return
}

66
internal/utils/device.go Normal file
View File

@ -0,0 +1,66 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"math/rand"
"strings"
"time"
)
// GetDeviceId
// @description: 生成设备ID
// @return deviceId 设备ID
func GetDeviceId() (deviceId string) {
// 创建一个有种子的随机数生成器
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// 定义可能的字母
letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// 初始化字符串构建器
var sb strings.Builder
// 生成15个随机字母
for i := 0; i < 15; i++ {
sb.WriteByte(letters[r.Intn(len(letters))])
}
// 计算MD5哈希
hash := md5.Sum([]byte(sb.String()))
hashString := hex.EncodeToString(hash[:])
// 返回49加上MD5哈希的第3个字符开始的部分
return "49" + hashString[2:]
}
// GetDeviceName
// @description: 生成设备名称
// @return deviceName 设备名称
func GetDeviceName() (deviceName string) {
firstNames := []string{
"Oliver", "Emma", "Liam", "Ava", "Noah", "Sophia", "Elijah", "Isabella",
"James", "Mia", "William", "Amelia", "Benjamin", "Harper", "Lucas", "Evelyn",
"Henry", "Abigail", "Alexander", "Ella", "Jackson", "Scarlett", "Sebastian",
"Grace", "Aiden", "Chloe", "Matthew", "Zoey", "Samuel", "Lily", "David",
"Aria", "Joseph", "Riley", "Carter", "Nora", "Owen", "Luna", "Daniel",
"Sofia", "Gabriel", "Ellie", "Matthew", "Avery", "Isaac", "Mila", "Leo",
"Julian", "Layla",
}
lastNames := []string{
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson",
"Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson",
"White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker",
"Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill",
"Flores", "Green", "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell",
"Mitchell", "Carter", "Roberts", "Gomez", "Phillips", "Evans",
}
// 使用随机数生成器
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// 随机选择名字和姓氏
firstName := firstNames[r.Intn(len(firstNames))]
lastName := lastNames[r.Intn(len(lastNames))]
// 返回组合后的字符串
return firstName + " " + lastName + "'s Pad"
}

View File

@ -13,12 +13,12 @@
<h2 class="text-lg font-medium text-gray-800">{{.Robot.Nickname}}</h2>
<p class="text-sm text-gray-600 mt-1">请使用微信扫描二维码登录</p>
</div>
<div class="p-6 flex flex-col items-center">
<div class="border border-gray-200 rounded-lg p-3 mb-6 bg-white">
<img src="{{.QRCode}}" alt="Login QR Code" class="w-64 h-64">
</div>
<div id="qrcode-status" class="text-center mb-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
<span class="relative flex h-3 w-3 mr-2">
@ -28,15 +28,15 @@
等待扫描
</span>
</div>
<p class="text-sm text-gray-500 mb-4 text-center">
请打开微信,使用"扫一扫"功能扫描上方二维码登录
</p>
<div id="timer" class="text-sm text-gray-500 mb-5">
二维码有效期: <span id="countdown">120</span>
</div>
<div class="w-full flex justify-between">
<a href="/admin/robots/{{.Robot.ID}}" class="py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
取消
@ -47,7 +47,7 @@
</div>
</div>
</div>
<p class="text-center text-sm text-gray-500 mt-6">
<i class="fas fa-info-circle mr-1"></i> 请确保手机与电脑在同一网络环境
</p>
@ -59,30 +59,30 @@
const timer = document.getElementById('timer');
const countdown = document.getElementById('countdown');
const refreshQrcode = document.getElementById('refresh-qrcode');
let secondsLeft = 120;
let statusCheckInterval;
// 开始倒计时
const countdownTimer = setInterval(() => {
secondsLeft--;
countdown.textContent = secondsLeft;
if (secondsLeft <= 0) {
clearInterval(countdownTimer);
clearInterval(statusCheckInterval);
qrcodeStatus.innerHTML = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
<i class="fas fa-times-circle mr-1"></i>
二维码已过期
</span>
`;
refreshQrcode.classList.remove('hidden');
}
}, 1000);
// 检查扫码状态
function checkQRCodeStatus() {
fetch(`/admin/robots/{{.Robot.ID}}/check-qrcode?uuid={{.UUID}}`)
@ -110,17 +110,17 @@
已扫描,等待确认
</span>
`;
} else if (data.status === 2) {
} else if (data.status === 99 && data.userInfo && Object.keys(data.userInfo).length > 0) {
qrcodeStatus.innerHTML = `
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
<i class="fas fa-check-circle mr-1"></i>
登录成功
</span>
`;
clearInterval(countdownTimer);
clearInterval(statusCheckInterval);
// 重定向到机器人详情页
setTimeout(() => {
window.location.href = `/admin/robots/{{.Robot.ID}}`;
@ -134,10 +134,10 @@
console.error('Error:', error);
});
}
// 每3秒检查一次扫码状态
statusCheckInterval = setInterval(checkQRCodeStatus, 3000);
// 立即执行一次检查
checkQRCodeStatus();
});

View File

@ -1,6 +0,0 @@
# 数据库迁移
此目录包含数据库迁移文件:
- 表结构创建
- 数据初始化
- 架构升级脚本

View File

@ -1,7 +0,0 @@
# 静态资源
此目录包含Web应用的静态资源
- CSS样式
- JavaScript文件
- 图片资源
- 字体文件