- Gin + config.yaml + SQLite 后端骨架,JWT 登录 + 管理员初始化
- web/ 原生单页:拖拽多图上传 → Agnes 免费识别 → 结果复制
- HTTP API:/api/v1/vision/analyze 免鉴权 + 令牌桶限流,支持程序直调
- cmd/vision-cli 独立 CLI 工具(复用 service 层,不依赖 Web 服务)
- build.bat / build.sh 构建脚本,移除 mcp-server
- 统一响应格式 {code, message, data},CORS/JWT/限流中间件
Co-Authored-By: Claude <noreply@anthropic.com>
90 lines
2.3 KiB
Go
90 lines
2.3 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
|
||
"vision-tool/internal/middleware"
|
||
"vision-tool/internal/model"
|
||
"vision-tool/internal/repository"
|
||
)
|
||
|
||
var (
|
||
ErrAdminExists = errors.New("管理员已初始化,禁止重复初始化")
|
||
ErrInvalidCredentials = errors.New("用户名或密码错误")
|
||
)
|
||
|
||
// AuthService 认证业务:管理员初始化 / 登录 / 用户信息。
|
||
type AuthService struct {
|
||
repo *repository.UserRepository
|
||
secret string
|
||
expire time.Duration
|
||
}
|
||
|
||
func NewAuthService(repo *repository.UserRepository, secret string, expire time.Duration) *AuthService {
|
||
return &AuthService{repo: repo, secret: secret, expire: expire}
|
||
}
|
||
|
||
// CheckAdmin 判断是否已存在管理员(方式 A:check + init)。
|
||
func (s *AuthService) CheckAdmin() (bool, error) {
|
||
count, err := s.repo.Count()
|
||
return count > 0, err
|
||
}
|
||
|
||
// InitAdmin 初始化管理员,已存在时拒绝。
|
||
func (s *AuthService) InitAdmin(username, password string) error {
|
||
initialized, err := s.CheckAdmin()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if initialized {
|
||
return ErrAdminExists
|
||
}
|
||
|
||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
user := &model.User{
|
||
Username: username,
|
||
PasswordHash: string(hash),
|
||
Role: "admin",
|
||
}
|
||
return s.repo.Create(user)
|
||
}
|
||
|
||
// Login 校验账号密码,签发 JWT token。
|
||
func (s *AuthService) Login(username, password string) (string, error) {
|
||
user, err := s.repo.FindByUsername(username)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return "", ErrInvalidCredentials
|
||
}
|
||
return "", err
|
||
}
|
||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil {
|
||
return "", ErrInvalidCredentials
|
||
}
|
||
|
||
claims := middleware.Claims{
|
||
UserID: user.ID,
|
||
Username: user.Username,
|
||
Role: user.Role,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.expire)),
|
||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||
},
|
||
}
|
||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(s.secret))
|
||
}
|
||
|
||
// Profile 按 ID 返回用户信息。
|
||
func (s *AuthService) Profile(userID uint) (*model.User, error) {
|
||
return s.repo.FindByID(userID)
|
||
}
|