feat: 实现后台和小程序端登录

main
熊二 1 year ago
parent fc483bdd60
commit 865b824c09
  1. 19
      internal/entities/company_auth_ticket.go
  2. 1
      internal/entities/company_employee.go
  3. 2
      internal/entities/system_user.go
  4. 92
      internal/repositories/company_auth_ticket.go
  5. 66
      internal/services/company/controller/company_employee_controller.go
  6. 67
      internal/services/system/controller/system_auth_controller.go
  7. 15
      internal/services/system/request/system_auth_login_request.go
  8. 1
      internal/services/system/service.go
  9. 13
      internal/util/password.go
  10. 1
      internal/util/redis.go
  11. 16
      pkg/crud/controller.go
  12. 7
      pkg/crud/echo_utils.go

@ -0,0 +1,19 @@
package entities
import (
"gorm.io/gorm"
"time"
)
type CompanyAuthTicket struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:员工编号"`
CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"`
EmployeeID uint `json:"employee_id" xml:"employee_id" gorm:"comment:所属员工编号"`
WechatOpenid string `json:"wechat_openid" xml:"wechat_openid" gorm:"size:100;not null;comment:关联微信号"`
Ticket string `json:"ticket" xml:"ticket" gorm:"comment:认证和授权凭证"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Employee *CompanyEmployee `json:"employee" xml:"employee" gorm:"foreignKey:EmployeeID"`
}

@ -22,5 +22,6 @@ type CompanyEmployee struct {
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Company *Company `json:"company" xml:"company" gorm:"foreignKey:CompanyID"`
Departments []*CompanyDepartment `json:"departments" xml:"departments" gorm:"many2many:company_employee_to_department_relations"`
}

@ -10,7 +10,7 @@ import (
type SystemUser struct {
ID int64 `json:"id" xml:"id" gorm:"primaryKey;not null;comment:系统用户编号"`
Username string `json:"username" xml:"username" gorm:"size:25;not null;uniqueIndex;comment:用户名"`
Password string `json:"password" xml:"password" gorm:"size:25;not null;comment:登录密码"`
Password string `json:"-" xml:"-" gorm:"size:250;not null;comment:登录密码"`
Status bool `json:"status" xml:"status" gorm:"comment:状态"`
Version db.Version `json:"-" xml:"-" gorm:"comment:乐观锁"`
CreatedAt time.Time `json:"create_time" xml:"create_time" gorm:"<-:false;comment:创建时间"`

@ -0,0 +1,92 @@
package repositories
import (
"context"
"errors"
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/pkg/db"
"sorbet/pkg/ticket"
)
type CompanyAuthTicketRepository struct {
*db.Repository[entities.CompanyAuthTicket]
}
// NewCompanyAuthTicketRepository 创建公司部门仓库
func NewCompanyAuthTicketRepository(orm *gorm.DB) *CompanyAuthTicketRepository {
return &CompanyAuthTicketRepository{
db.NewRepositoryWith[entities.CompanyAuthTicket](orm, "id"),
}
}
// TicketForLastLogin 最后一次登录生成的 ticket 信息
func (r *CompanyAuthTicketRepository) TicketForLastLogin(ctx context.Context, openid string) (*entities.CompanyAuthTicket, error) {
var authTicket entities.CompanyAuthTicket
res := r.DB(ctx).
Model(&authTicket).
Where("wechat_openid=?", openid).
Order("updated_at DESC").
Preload("Employee", func(tx *gorm.DB) *gorm.DB {
//return tx.
// Preload("Company"). // 当前员工所在公司
// Preload("Departments", func(tx *gorm.DB) *gorm.DB {
// // 只查询当前公司的部门
// return tx.Where("company_id = ?")
// }) // 当前员工所在部门
return tx.Preload("Company")
}).
Last(&authTicket)
if err := res.Error; err != nil {
return nil, err
}
return &authTicket, nil
}
// EmployeeForLastLogin 使用微信最后一次登录的员工信息
func (r *CompanyAuthTicketRepository) EmployeeForLastLogin(ctx context.Context, openid string) (*entities.CompanyEmployee, error) {
authTicket, err := r.TicketForLastLogin(ctx, openid)
if err != nil {
return nil, err
}
return authTicket.Employee, nil
}
func (r *CompanyAuthTicketRepository) GenerateForWechat(ctx context.Context, openid string) (employee *entities.CompanyEmployee, ticketString string, err error) {
employee, err = r.EmployeeForLastLogin(ctx, openid)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return
}
// 通过最后一个 ticket 生成新的
if employee == nil {
// 随机取一个员工信息
var temp entities.CompanyEmployee
res := r.DB(ctx).
Where("wechat_openid=?", openid).
Preload("Departments").
Preload("Company").
Order("updated_at").
First(&temp)
if err = res.Error; err != nil {
return
}
employee = &temp
}
ticketString, err = ticket.Create(&ticket.Claims{
UID: employee.ID,
Role: "company:employee",
})
authTicket := &entities.CompanyAuthTicket{
CompanyID: employee.CompanyID,
EmployeeID: employee.ID,
WechatOpenid: employee.WechatOpenid,
Ticket: ticketString,
}
err = r.Create(ctx, authTicket)
return
}

@ -1,16 +1,78 @@
package controller
import (
"errors"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/internal/errs"
"sorbet/internal/repositories"
"sorbet/internal/services/company/request"
"sorbet/pkg/crud"
"sorbet/pkg/db"
"sorbet/pkg/rsp"
"sorbet/pkg/wx"
)
type CompanyEmployeeController struct {
crud.Controller[entities.CompanyEmployee, request.CompanyEmployeeUpsertRequest]
}
func (c *CompanyEmployeeController) InitRoutes(r *echo.Group) {
c.RegisterRoutes("/company/employees", r)
func (ctr *CompanyEmployeeController) InitRoutes(r *echo.Group) {
ctr.RegisterRoutes("/company/employees", r)
}
func (ctr *CompanyEmployeeController) RegisterRoutes(path string, r *echo.Group) {
ctr.Controller.RegisterRoutes(path, r)
r.POST(path+"/auth/login", ctr.Login)
}
// Login 员工登录
func (ctr *CompanyEmployeeController) Login(c echo.Context) error {
var req struct {
Code string `json:"code" xml:"code" form:"code"`
}
if err := c.Bind(&req); err != nil {
return err
}
login, err := wx.Login(c.Request().Context(), req.Code)
if err != nil {
return err
}
if err = login.Err(); err != nil {
return err
}
employee, ticketString, err := repositories.
NewCompanyAuthTicketRepository(ctr.MustORM(c)).
GenerateForWechat(c.Request().Context(), login.Openid)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 微信尚未绑定
return errs.ErrWechatNotBound.WithData(echo.Map{
"openid": login.Openid,
})
}
var companies []*entities.Company
err = ctr.MustORM(c).
Model(&entities.Company{}).
Where(
"EXISTS(?)",
db.Model(&entities.CompanyEmployee{}).Where("wechat_openid=?", employee.WechatOpenid),
).
Find(&companies).
Error
if err != nil {
return err
}
return rsp.Ok(c, echo.Map{
"ticket": ticketString,
"openid": login.Openid,
"companies": companies,
"employee": employee,
})
}

@ -0,0 +1,67 @@
package controller
import (
"errors"
"github.com/labstack/echo/v4"
"github.com/rs/xid"
"gorm.io/gorm"
"sorbet/internal/entities"
"sorbet/internal/errs"
"sorbet/internal/services/system/request"
"sorbet/internal/util"
"sorbet/pkg/crud"
"sorbet/pkg/db"
"sorbet/pkg/rsp"
"sync"
)
// 简单的使用内存实现,也就是重启的话必须重新登录
var logins sync.Map
type SystemAuthController struct{}
func (s *SystemAuthController) InitRoutes(r *echo.Group) {
r.POST("/system/auth/login", s.Login)
}
func (s *SystemAuthController) Login(c echo.Context) error {
req, err := crud.Bind[request.SystemAuthLoginRequest](c)
if err != nil {
return err
}
var user entities.SystemUser
err = db.
WithContext(c.Request().Context()).
Model(&user).
Where("username=?", req.Username).
First(&user).
Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errs.ErrLoginWithUserpwd
}
return err
}
if !util.PasswordVerify(req.Password, user.Password) {
return errs.ErrLoginWithUserpwd
}
var ticket string
logins.Range(func(key, value any) bool {
x := value.(*entities.SystemUser)
if x.ID == user.ID {
ticket = key.(string)
return false
}
return true
})
if ticket == "" {
ticket = xid.New().String()
logins.Store(ticket, &user)
}
// todo 用户状态
return rsp.Ok(c, echo.Map{
"user": user,
"ticket": ticket,
})
}

@ -0,0 +1,15 @@
package request
import "sorbet/pkg/v"
type SystemAuthLoginRequest struct {
Username string `json:"username" xml:"username" form:"username"`
Password string `json:"password" xml:"password" form:"password"`
}
func (r *SystemAuthLoginRequest) Validate() error {
return v.Validate(
v.Value(r.Username, "username", "用户名").Required().MinLength(2),
v.Value(r.Password, "password", "登录密码").Required().MinLength(6),
)
}

@ -22,6 +22,7 @@ func (s *Service) Init(context.Context) error {
func (s *Service) Routes() []runtime.Routable {
return []runtime.Routable{
&controller.SystemAuthController{},
&controller.SystemLogController{},
&controller.SystemMenuController{},
&controller.SystemPermissionController{},

@ -0,0 +1,13 @@
package util
import "golang.org/x/crypto/bcrypt"
func PasswordHash(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func PasswordVerify(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

@ -0,0 +1 @@
package util

@ -77,6 +77,14 @@ func (ctr *Controller[Entity, Upsert]) ORM(c echo.Context) (*gorm.DB, error) {
return db.DB().WithContext(c.Request().Context()), nil
}
func (ctr *Controller[Entity, Upsert]) MustORM(c echo.Context) *gorm.DB {
orm, err := ctr.ORM(c)
if err != nil {
panic(err)
}
return orm
}
// Repository 获取 Repository 实例
func (ctr *Controller[Entity, Upsert]) Repository(c echo.Context) (*db.Repository[Entity], error) {
orm, err := ctr.ORM(c)
@ -86,6 +94,14 @@ func (ctr *Controller[Entity, Upsert]) Repository(c echo.Context) (*db.Repositor
return db.NewRepository[Entity](orm), nil
}
func (ctr *Controller[Entity, Upsert]) MustRepository(c echo.Context) *db.Repository[Entity] {
repository, err := ctr.Repository(c)
if err != nil {
panic(err)
}
return repository
}
// Create 创建数据
func (ctr *Controller[Entity, Upsert]) Create(c echo.Context) error {
return ctr.upsert(c, true)

@ -1,6 +1,7 @@
package crud
import (
"errors"
"github.com/labstack/echo/v4"
"net/http"
"reflect"
@ -27,9 +28,15 @@ var binder BodyBinder
// Validate 验证数据
func Validate(c echo.Context, t any, guards ...RequestGuarder) error {
err := c.Validate(t)
if err != nil && !errors.Is(err, echo.ErrValidatorNotRegistered) {
return err
}
if v, ok := t.(interface{ Validate() error }); ok {
err = v.Validate()
if err != nil {
return err
}
}
for _, guard := range guards {
err = guard(c, t)
if err != nil {

Loading…
Cancel
Save