feat: 新增设备登录功能,包括API接口、RPC服务、逻辑处理和相关数据类型及错误码。
Build docker and publish / prepare (20.15.1) (push) Successful in 11s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.admin image_name:ppanel-admin name:admin]) (push) Successful in 4m32s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.api image_name:ppanel-api name:api]) (push) Successful in 8m6s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.node image_name:ppanel-node name:node]) (push) Successful in 4m26s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.rpc-core image_name:ppanel-rpc-core name:rpc-core]) (push) Successful in 8m23s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.scheduler image_name:ppanel-scheduler name:scheduler]) (push) Successful in 4m1s
Build docker and publish / deploy (push) Successful in 45s
Build docker and publish / notify (push) Successful in 3s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.queue image_name:ppanel-queue name:queue]) (push) Successful in 3m55s

This commit is contained in:
2026-03-01 18:51:12 -08:00
parent be4cc669d2
commit 3b4429bdd9
20 changed files with 824 additions and 25 deletions
@@ -0,0 +1,104 @@
package logic
import (
"context"
"database/sql"
"errors"
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/core"
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/repo"
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/svc"
"github.com/zero-ppanel/zero-ppanel/pkg/xerr"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type DeviceLoginLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic {
return &DeviceLoginLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// DeviceLogin 设备登录:查设备 → 不存在则注册 → 返回用户信息
func (l *DeviceLoginLogic) DeviceLogin(in *core.DeviceLoginReq) (*core.DeviceLoginResp, error) {
// 1. 查询设备
device, err := l.svcCtx.DeviceRepo.FindDeviceByIdentifier(l.ctx, in.Identifier)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
l.Errorf("FindDeviceByIdentifier error: %v", err)
return nil, status.Error(codes.Code(xerr.DatabaseQueryError), "查询设备失败")
}
var userID int64
var isNewUser bool
var isAdmin bool
if errors.Is(err, sql.ErrNoRows) || device == nil {
// 2. 设备不存在,创建用户+设备
userID, err = l.svcCtx.DeviceRepo.CreateUserWithDevice(l.ctx, repo.CreateUserWithDeviceParams{
Identifier: in.Identifier,
UserAgent: in.UserAgent,
IP: in.Ip,
ShortCode: in.ShortCode,
})
if err != nil {
l.Errorf("CreateUserWithDevice error: %v", err)
return nil, status.Error(codes.Code(xerr.DatabaseInsertError), "创建用户设备失败")
}
isNewUser = true
// 记录注册日志
_ = l.svcCtx.DeviceRepo.InsertLoginLog(l.ctx, repo.LoginLogParams{
UserID: userID,
Method: "device",
IP: in.Ip,
UserAgent: in.UserAgent,
Success: true,
})
} else {
// 3. 设备存在,检查是否启用
if !device.Enabled {
return nil, status.Error(codes.Code(xerr.DeviceNotEnabled), "设备已禁用")
}
userID = device.UserID
// 查用户信息
user, err := l.svcCtx.DeviceRepo.FindUserByID(l.ctx, userID)
if err != nil {
l.Errorf("FindUserByID error: %v", err)
return nil, status.Error(codes.Code(xerr.DatabaseQueryError), "查询用户失败")
}
if !user.Enable {
return nil, status.Error(codes.Code(xerr.UserDisabled), "用户已禁用")
}
isAdmin = user.IsAdmin
}
// 4. 记录登录日志
_ = l.svcCtx.DeviceRepo.InsertLoginLog(l.ctx, repo.LoginLogParams{
UserID: userID,
Method: "device",
IP: in.Ip,
UserAgent: in.UserAgent,
Success: true,
})
return &core.DeviceLoginResp{
UserId: userID,
IsAdmin: isAdmin,
IsDisabled: false,
IsNewUser: isNewUser,
}, nil
}
+155
View File
@@ -0,0 +1,155 @@
package repo
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/zero-ppanel/zero-ppanel/pkg/cryptox"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// DeviceInfo represents a row from user_device table.
type DeviceInfo struct {
ID int64 `db:"id"`
UserID int64 `db:"user_id"`
Identifier string `db:"Identifier"`
Enabled bool `db:"enabled"`
}
// UserInfo represents minimal user fields needed for login.
type UserInfo struct {
ID int64 `db:"id"`
Enable bool `db:"enable"`
IsAdmin bool `db:"is_admin"`
IsDeleted bool `db:"is_del"`
}
// CreateUserWithDeviceParams holds parameters for creating a user and device in one transaction.
type CreateUserWithDeviceParams struct {
Identifier string
UserAgent string
IP string
ShortCode string
}
// LoginLogParams holds parameters for inserting a login log.
type LoginLogParams struct {
UserID int64
Method string
IP string
UserAgent string
Success bool
}
// DeviceRepo defines device-related data access methods.
type DeviceRepo interface {
FindDeviceByIdentifier(ctx context.Context, identifier string) (*DeviceInfo, error)
FindUserByID(ctx context.Context, userID int64) (*UserInfo, error)
CreateUserWithDevice(ctx context.Context, params CreateUserWithDeviceParams) (int64, error)
InsertLoginLog(ctx context.Context, params LoginLogParams) error
}
type deviceRepo struct {
conn sqlx.SqlConn
}
func NewDeviceRepo(conn sqlx.SqlConn) DeviceRepo {
return &deviceRepo{conn: conn}
}
func (r *deviceRepo) FindDeviceByIdentifier(ctx context.Context, identifier string) (*DeviceInfo, error) {
var d DeviceInfo
err := r.conn.QueryRowCtx(ctx, &d,
"SELECT `id`, `user_id`, `Identifier`, `enabled` FROM `user_device` WHERE `Identifier` = ? LIMIT 1",
identifier,
)
if err != nil {
return nil, err
}
return &d, nil
}
func (r *deviceRepo) FindUserByID(ctx context.Context, userID int64) (*UserInfo, error) {
var u UserInfo
err := r.conn.QueryRowCtx(ctx, &u,
"SELECT `id`, `enable`, `is_admin`, COALESCE(`is_del`, 0) AS `is_del` FROM `user` WHERE `id` = ? LIMIT 1",
userID,
)
if err != nil {
return nil, err
}
return &u, nil
}
func (r *deviceRepo) CreateUserWithDevice(ctx context.Context, params CreateUserWithDeviceParams) (int64, error) {
// Generate a random password hash for device-only users
randomPwd, err := cryptox.GeneratePasswordHash("device-placeholder-" + params.Identifier)
if err != nil {
return 0, fmt.Errorf("hash password: %w", err)
}
var userID int64
err = r.conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
// 1. Create user
result, err := session.ExecCtx(ctx,
"INSERT INTO `user` (`password`, `algo`, `salt`, `enable`, `is_admin`, `created_at`, `updated_at`) VALUES (?, 'bcrypt', 'default', 1, 0, NOW(3), NOW(3))",
randomPwd,
)
if err != nil {
return fmt.Errorf("insert user: %w", err)
}
userID, err = result.LastInsertId()
if err != nil {
return fmt.Errorf("last insert id: %w", err)
}
// 2. Create auth method
_, err = session.ExecCtx(ctx,
"INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`) VALUES (?, 'device', ?, 1, NOW(3), NOW(3))",
userID, params.Identifier,
)
if err != nil {
return fmt.Errorf("insert auth method: %w", err)
}
// 3. Create device
_, err = session.ExecCtx(ctx,
"INSERT INTO `user_device` (`user_id`, `ip`, `Identifier`, `short_code`, `user_agent`, `online`, `enabled`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, 0, 1, NOW(3), NOW(3))",
userID, params.IP, params.Identifier, params.ShortCode, params.UserAgent,
)
if err != nil {
return fmt.Errorf("insert device: %w", err)
}
return nil
})
return userID, err
}
func (r *deviceRepo) InsertLoginLog(ctx context.Context, params LoginLogParams) error {
content, _ := json.Marshal(map[string]interface{}{
"method": params.Method,
"login_ip": params.IP,
"user_agent": params.UserAgent,
"success": params.Success,
"timestamp": time.Now().UnixMilli(),
})
_, err := r.conn.ExecCtx(ctx,
"INSERT INTO `system_logs` (`type`, `date`, `object_id`, `content`, `created_at`) VALUES (?, ?, ?, ?, NOW(3))",
6, // type=6 is Login log
time.Now().Format("2006-01-02"),
params.UserID,
string(content),
)
if err != nil {
return fmt.Errorf("insert login log: %w", err)
}
return nil
}
// ErrNotFound is re-exported for convenience.
var ErrNotFound = sql.ErrNoRows
@@ -40,3 +40,9 @@ func (s *CoreServer) GetNodeInfo(ctx context.Context, in *core.GetNodeInfoReq) (
l := logic.NewGetNodeInfoLogic(ctx, s.svcCtx)
return l.GetNodeInfo(in)
}
// 设备登录
func (s *CoreServer) DeviceLogin(ctx context.Context, in *core.DeviceLoginReq) (*core.DeviceLoginResp, error) {
l := logic.NewDeviceLoginLogic(ctx, s.svcCtx)
return l.DeviceLogin(in)
}
+17 -3
View File
@@ -1,13 +1,27 @@
package svc
import "github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/config"
import (
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/config"
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/repo"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
Config config.Config
DB sqlx.SqlConn
Redis *redis.Redis
DeviceRepo repo.DeviceRepo
}
func NewServiceContext(c config.Config) *ServiceContext {
db := sqlx.NewMysql(c.MySQL.DataSource)
rds := redis.MustNewRedis(c.CacheRedis)
return &ServiceContext{
Config: c,
Config: c,
DB: db,
Redis: rds,
DeviceRepo: repo.NewDeviceRepo(db),
}
}