From 865b824c0945406ea1ef275535951608c6d6c6f0 Mon Sep 17 00:00:00 2001 From: hupeh Date: Wed, 25 Oct 2023 17:42:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=92=8C=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=AB=AF=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/entities/company_auth_ticket.go | 19 ++++ internal/entities/company_employee.go | 1 + internal/entities/system_user.go | 2 +- internal/repositories/company_auth_ticket.go | 92 +++++++++++++++++++ .../controller/company_employee_controller.go | 66 ++++++++++++- .../controller/system_auth_controller.go | 67 ++++++++++++++ .../request/system_auth_login_request.go | 15 +++ internal/services/system/service.go | 1 + internal/util/password.go | 13 +++ internal/util/redis.go | 1 + pkg/crud/controller.go | 16 ++++ pkg/crud/echo_utils.go | 9 +- 12 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 internal/entities/company_auth_ticket.go create mode 100644 internal/repositories/company_auth_ticket.go create mode 100644 internal/services/system/controller/system_auth_controller.go create mode 100644 internal/services/system/request/system_auth_login_request.go create mode 100644 internal/util/password.go create mode 100644 internal/util/redis.go diff --git a/internal/entities/company_auth_ticket.go b/internal/entities/company_auth_ticket.go new file mode 100644 index 0000000..026dae7 --- /dev/null +++ b/internal/entities/company_auth_ticket.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"` +} diff --git a/internal/entities/company_employee.go b/internal/entities/company_employee.go index dc34cc1..c014f72 100644 --- a/internal/entities/company_employee.go +++ b/internal/entities/company_employee.go @@ -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"` } diff --git a/internal/entities/system_user.go b/internal/entities/system_user.go index 6b8fb78..be88b49 100644 --- a/internal/entities/system_user.go +++ b/internal/entities/system_user.go @@ -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:创建时间"` diff --git a/internal/repositories/company_auth_ticket.go b/internal/repositories/company_auth_ticket.go new file mode 100644 index 0000000..6172116 --- /dev/null +++ b/internal/repositories/company_auth_ticket.go @@ -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 +} diff --git a/internal/services/company/controller/company_employee_controller.go b/internal/services/company/controller/company_employee_controller.go index 37bcd3d..8172c2c 100644 --- a/internal/services/company/controller/company_employee_controller.go +++ b/internal/services/company/controller/company_employee_controller.go @@ -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, + }) } diff --git a/internal/services/system/controller/system_auth_controller.go b/internal/services/system/controller/system_auth_controller.go new file mode 100644 index 0000000..8a9396f --- /dev/null +++ b/internal/services/system/controller/system_auth_controller.go @@ -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, + }) +} diff --git a/internal/services/system/request/system_auth_login_request.go b/internal/services/system/request/system_auth_login_request.go new file mode 100644 index 0000000..e94ed62 --- /dev/null +++ b/internal/services/system/request/system_auth_login_request.go @@ -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), + ) +} diff --git a/internal/services/system/service.go b/internal/services/system/service.go index 9d99ffc..e5d5d2d 100644 --- a/internal/services/system/service.go +++ b/internal/services/system/service.go @@ -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{}, diff --git a/internal/util/password.go b/internal/util/password.go new file mode 100644 index 0000000..4ff660c --- /dev/null +++ b/internal/util/password.go @@ -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 +} diff --git a/internal/util/redis.go b/internal/util/redis.go new file mode 100644 index 0000000..c7d8682 --- /dev/null +++ b/internal/util/redis.go @@ -0,0 +1 @@ +package util diff --git a/pkg/crud/controller.go b/pkg/crud/controller.go index 19122eb..05791a8 100644 --- a/pkg/crud/controller.go +++ b/pkg/crud/controller.go @@ -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) diff --git a/pkg/crud/echo_utils.go b/pkg/crud/echo_utils.go index ad2371b..d19f78c 100644 --- a/pkg/crud/echo_utils.go +++ b/pkg/crud/echo_utils.go @@ -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 { + 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 {