Compare commits
5 Commits
faded4e634
...
9b751c066c
Author | SHA1 | Date |
---|---|---|
熊二 | 9b751c066c | 1 year ago |
熊二 | 865b824c09 | 1 year ago |
熊二 | fc483bdd60 | 1 year ago |
熊二 | 1b27c50a90 | 1 year ago |
熊二 | e18ec70269 | 1 year ago |
@ -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"` |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
package errs |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"sorbet/pkg/rsp" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
ErrWechatNotBound = rsp.NewError(http.StatusBadRequest, 10001, "微信尚未绑定") |
||||||
|
ErrLoginWithUserpwd = rsp.NewError(http.StatusUnauthorized, 10002, "用户名或密码错误") |
||||||
|
) |
@ -1,136 +0,0 @@ |
|||||||
package middleware |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/labstack/echo/v4" |
|
||||||
"github.com/labstack/echo/v4/middleware" |
|
||||||
"net/http" |
|
||||||
) |
|
||||||
|
|
||||||
type CORSConfig struct { |
|
||||||
// Skipper defines a function to skip middleware.
|
|
||||||
Skipper func(c echo.Context) bool |
|
||||||
|
|
||||||
// AllowOrigins determines the value of the Access-Control-Allow-Origin
|
|
||||||
// response header. This header defines a list of origins that may access the
|
|
||||||
// resource. The wildcard characters '*' and '?' are supported and are
|
|
||||||
// converted to regex fragments '.*' and '.' accordingly.
|
|
||||||
//
|
|
||||||
// Security: use extreme caution when handling the origin, and carefully
|
|
||||||
// validate any logic. Remember that attackers may register hostile domain names.
|
|
||||||
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
|
|
||||||
//
|
|
||||||
// Optional. Default value []string{"*"}.
|
|
||||||
//
|
|
||||||
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
|
||||||
AllowOrigins []string |
|
||||||
|
|
||||||
// AllowOriginFunc is a custom function to validate the origin. It takes the
|
|
||||||
// origin as an argument and returns true if allowed or false otherwise. If
|
|
||||||
// an error is returned, it is returned by the handler. If this option is
|
|
||||||
// set, AllowOrigins is ignored.
|
|
||||||
//
|
|
||||||
// Security: use extreme caution when handling the origin, and carefully
|
|
||||||
// validate any logic. Remember that attackers may register hostile domain names.
|
|
||||||
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
|
|
||||||
//
|
|
||||||
// Optional.
|
|
||||||
AllowOriginFunc func(origin string) (bool, error) |
|
||||||
|
|
||||||
// AllowMethods determines the value of the Access-Control-Allow-Methods
|
|
||||||
// response header. This header specified the list of methods allowed when
|
|
||||||
// accessing the resource. This is used in response to a preflight request.
|
|
||||||
//
|
|
||||||
// Optional. Default value DefaultCORSConfig.AllowMethods.
|
|
||||||
// If `allowMethods` is left empty, this middleware will fill for preflight
|
|
||||||
// request `Access-Control-Allow-Methods` header value
|
|
||||||
// from `Allow` header that echo.Router set into context.
|
|
||||||
//
|
|
||||||
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
|
||||||
AllowMethods []string |
|
||||||
|
|
||||||
// AllowHeaders determines the value of the Access-Control-Allow-Headers
|
|
||||||
// response header. This header is used in response to a preflight request to
|
|
||||||
// indicate which HTTP headers can be used when making the actual request.
|
|
||||||
//
|
|
||||||
// Optional. Default value []string{}.
|
|
||||||
//
|
|
||||||
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
|
||||||
AllowHeaders []string |
|
||||||
|
|
||||||
// AllowCredentials determines the value of the
|
|
||||||
// Access-Control-Allow-Credentials response header. This header indicates
|
|
||||||
// whether the response to the request can be exposed when the
|
|
||||||
// credentials mode (Request.credentials) is true. When used as part of a
|
|
||||||
// response to a preflight request, this indicates whether or not the actual
|
|
||||||
// request can be made using credentials. See also
|
|
||||||
// [MDN: Access-Control-Allow-Credentials].
|
|
||||||
//
|
|
||||||
// Optional. Default value false, in which case the header is not set.
|
|
||||||
//
|
|
||||||
// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.
|
|
||||||
// See "Exploiting CORS misconfigurations for Bitcoins and bounties",
|
|
||||||
// https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
|
|
||||||
//
|
|
||||||
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
|
|
||||||
AllowCredentials bool |
|
||||||
|
|
||||||
// UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials
|
|
||||||
// flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header.
|
|
||||||
//
|
|
||||||
// This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties)
|
|
||||||
// attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject.
|
|
||||||
//
|
|
||||||
// Optional. Default value is false.
|
|
||||||
UnsafeWildcardOriginWithAllowCredentials bool |
|
||||||
|
|
||||||
// ExposeHeaders determines the value of Access-Control-Expose-Headers, which
|
|
||||||
// defines a list of headers that clients are allowed to access.
|
|
||||||
//
|
|
||||||
// Optional. Default value []string{}, in which case the header is not set.
|
|
||||||
//
|
|
||||||
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header
|
|
||||||
ExposeHeaders []string |
|
||||||
|
|
||||||
// MaxAge determines the value of the Access-Control-Max-Age response header.
|
|
||||||
// This header indicates how long (in seconds) the results of a preflight
|
|
||||||
// request can be cached.
|
|
||||||
//
|
|
||||||
// Optional. Default value 0. The header is set only if MaxAge > 0.
|
|
||||||
//
|
|
||||||
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
|
||||||
MaxAge int |
|
||||||
} |
|
||||||
|
|
||||||
// DefaultCORSConfig is the default CORS middleware config.
|
|
||||||
var DefaultCORSConfig = CORSConfig{ |
|
||||||
Skipper: func(c echo.Context) bool { |
|
||||||
return false |
|
||||||
}, |
|
||||||
AllowOrigins: []string{"*"}, |
|
||||||
AllowMethods: []string{ |
|
||||||
http.MethodGet, |
|
||||||
http.MethodHead, |
|
||||||
http.MethodPut, |
|
||||||
http.MethodPatch, |
|
||||||
http.MethodPost, |
|
||||||
http.MethodDelete, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
func (c *CORSConfig) ToMiddleware() echo.MiddlewareFunc { |
|
||||||
return middleware.CORSWithConfig(middleware.CORSConfig{ |
|
||||||
Skipper: c.Skipper, |
|
||||||
AllowOrigins: c.AllowOrigins, |
|
||||||
AllowOriginFunc: c.AllowOriginFunc, |
|
||||||
AllowMethods: c.AllowMethods, |
|
||||||
AllowHeaders: c.AllowHeaders, |
|
||||||
AllowCredentials: c.AllowCredentials, |
|
||||||
UnsafeWildcardOriginWithAllowCredentials: c.UnsafeWildcardOriginWithAllowCredentials, |
|
||||||
ExposeHeaders: c.ExposeHeaders, |
|
||||||
MaxAge: c.MaxAge, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func CORS() echo.MiddlewareFunc { |
|
||||||
return DefaultCORSConfig.ToMiddleware() |
|
||||||
} |
|
@ -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 |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
package repositories |
||||||
|
|
||||||
|
import ( |
||||||
|
"gorm.io/gorm" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/pkg/db" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyEmployeeRepository struct { |
||||||
|
*db.Repository[entities.CompanyEmployee] |
||||||
|
} |
||||||
|
|
||||||
|
// NewCompanyEmployeeRepository 创建公司员工仓库
|
||||||
|
func NewCompanyEmployeeRepository(orm *gorm.DB) *CompanyEmployeeRepository { |
||||||
|
return &CompanyEmployeeRepository{ |
||||||
|
db.NewRepositoryWith[entities.CompanyEmployee](orm, "id"), |
||||||
|
} |
||||||
|
} |
@ -1,18 +0,0 @@ |
|||||||
package repositories |
|
||||||
|
|
||||||
import ( |
|
||||||
"gorm.io/gorm" |
|
||||||
"sorbet/internal/entities" |
|
||||||
"sorbet/pkg/db" |
|
||||||
) |
|
||||||
|
|
||||||
type CompanyStaffRepository struct { |
|
||||||
*db.Repository[entities.CompanyStaff] |
|
||||||
} |
|
||||||
|
|
||||||
// NewCompanyStaffRepository 创建公司员工仓库
|
|
||||||
func NewCompanyStaffRepository(orm *gorm.DB) *CompanyStaffRepository { |
|
||||||
return &CompanyStaffRepository{ |
|
||||||
db.NewRepositoryWith[entities.CompanyStaff](orm, "id"), |
|
||||||
} |
|
||||||
} |
|
@ -1,4 +1,4 @@ |
|||||||
package middleware |
package runtime |
||||||
|
|
||||||
import ( |
import ( |
||||||
"context" |
"context" |
@ -0,0 +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 (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, |
||||||
|
}) |
||||||
|
} |
@ -1,16 +0,0 @@ |
|||||||
package controller |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/labstack/echo/v4" |
|
||||||
"sorbet/internal/entities" |
|
||||||
"sorbet/internal/services/company/request" |
|
||||||
"sorbet/pkg/crud" |
|
||||||
) |
|
||||||
|
|
||||||
type CompanyStaffController struct { |
|
||||||
crud.Controller[entities.CompanyStaff, request.CompanyStaffUpsertRequest] |
|
||||||
} |
|
||||||
|
|
||||||
func (c *CompanyStaffController) InitRoutes(r *echo.Group) { |
|
||||||
c.RegisterRoutes("/company/staffs", r) |
|
||||||
} |
|
@ -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), |
||||||
|
) |
||||||
|
} |
@ -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 |
@ -1,18 +1,110 @@ |
|||||||
package db |
package db |
||||||
|
|
||||||
import "gorm.io/gorm" |
import ( |
||||||
|
"gorm.io/gorm" |
||||||
|
) |
||||||
|
|
||||||
type DeleteBuilder[T any] struct { |
type DeleteBuilder[T any] struct { |
||||||
Expr |
db *gorm.DB |
||||||
db *gorm.DB |
expr *Expr |
||||||
} |
} |
||||||
|
|
||||||
func NewDeleteBuilder[T any](db *gorm.DB) *DeleteBuilder[T] { |
func NewDeleteBuilder[T any](db *gorm.DB) *DeleteBuilder[T] { |
||||||
return &DeleteBuilder[T]{Expr{}, db} |
return &DeleteBuilder[T]{db: db, expr: &Expr{}} |
||||||
} |
} |
||||||
|
|
||||||
func (b *DeleteBuilder[T]) Commit() (int64, error) { |
func (d *DeleteBuilder[T]) Eq(col string, val any) *DeleteBuilder[T] { |
||||||
|
d.expr.Eq(col, val) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Neq(col string, val any) *DeleteBuilder[T] { |
||||||
|
d.expr.Neq(col, val) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Lt(col string, val any) *DeleteBuilder[T] { |
||||||
|
d.expr.Lt(col, val) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Lte(col string, val any) *DeleteBuilder[T] { |
||||||
|
d.expr.Lte(col, val) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Gt(col string, val any) *DeleteBuilder[T] { |
||||||
|
d.expr.Gt(col, val) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Gte(col string, val any) *DeleteBuilder[T] { |
||||||
|
d.expr.Gte(col, val) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Between(col string, less, more any) *DeleteBuilder[T] { |
||||||
|
d.expr.Between(col, less, more) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) NotBetween(col string, less, more any) *DeleteBuilder[T] { |
||||||
|
d.expr.NotBetween(col, less, more) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) IsNull(col string) *DeleteBuilder[T] { |
||||||
|
d.expr.IsNull(col) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) NotNull(col string) *DeleteBuilder[T] { |
||||||
|
d.expr.NotNull(col) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Like(col, tpl string) *DeleteBuilder[T] { |
||||||
|
d.expr.Like(col, tpl) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) NotLike(col, tpl string) *DeleteBuilder[T] { |
||||||
|
d.expr.NotLike(col, tpl) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) In(col string, values ...any) *DeleteBuilder[T] { |
||||||
|
d.expr.In(col, values...) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) NotIn(col string, values ...any) *DeleteBuilder[T] { |
||||||
|
d.expr.NotIn(col, values...) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) When(condition bool, then func(ex *Expr), elses ...func(ex *Expr)) *DeleteBuilder[T] { |
||||||
|
d.expr.When(condition, then, elses...) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Or(or func(ex *Expr)) *DeleteBuilder[T] { |
||||||
|
d.expr.Or(or) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) And(and func(ex *Expr)) *DeleteBuilder[T] { |
||||||
|
d.expr.And(and) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Not(not func(ex *Expr)) *DeleteBuilder[T] { |
||||||
|
d.expr.Not(not) |
||||||
|
return d |
||||||
|
} |
||||||
|
|
||||||
|
func (d *DeleteBuilder[T]) Commit() (int64, error) { |
||||||
var t T |
var t T |
||||||
res := b.db.Scopes(b.Scopes).Delete(&t) |
res := d.db.Scopes(d.expr.Scopes).Delete(&t) |
||||||
return res.RowsAffected, res.Error |
return res.RowsAffected, res.Error |
||||||
} |
} |
||||||
|
@ -0,0 +1,154 @@ |
|||||||
|
package rs |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// ErrOK 表示没有任何错误。
|
||||||
|
// 对应 HTTP 响应状态码为 500。
|
||||||
|
ErrOK = NewError(http.StatusOK, 0, "OK") |
||||||
|
|
||||||
|
// ErrInternal 客户端请求有效,但服务器处理时发生了意外。
|
||||||
|
// 对应 HTTP 响应状态码为 500。
|
||||||
|
ErrInternal = NewError(http.StatusInternalServerError, -100, "系统内部错误") |
||||||
|
|
||||||
|
// ErrServiceUnavailable 服务器无法处理请求,一般用于网站维护状态。
|
||||||
|
// 对应 HTTP 响应状态码为 503。
|
||||||
|
ErrServiceUnavailable = NewError(http.StatusServiceUnavailable, -101, "服务不可用") |
||||||
|
|
||||||
|
// ErrUnauthorized 用户未提供身份验证凭据,或者没有通过身份验证。
|
||||||
|
// 响应的 HTTP 状态码为 401。
|
||||||
|
ErrUnauthorized = NewError(http.StatusUnauthorized, -102, "身份验证失败") |
||||||
|
|
||||||
|
// ErrForbidden 用户通过了身份验证,但是不具有访问资源所需的权限。
|
||||||
|
// 响应的 HTTP 状态码为 403。
|
||||||
|
ErrForbidden = NewError(http.StatusForbidden, -103, "不具有访问资源所需的权限") |
||||||
|
|
||||||
|
// ErrGone 所请求的资源已从这个地址转移,不再可用。
|
||||||
|
// 响应的 HTTP 状态码为 410。
|
||||||
|
ErrGone = NewError(http.StatusGone, -104, "所请求的资源不存在") |
||||||
|
|
||||||
|
// ErrUnsupportedMediaType 客户端要求的返回格式不支持。
|
||||||
|
// 比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。
|
||||||
|
// 响应的 HTTP 状态码为 415。
|
||||||
|
ErrUnsupportedMediaType = NewError(http.StatusUnsupportedMediaType, -105, "请求的数据格式错误") |
||||||
|
|
||||||
|
// ErrUnprocessableEntity 无法处理客户端上传的附件,导致请求失败。
|
||||||
|
// 响应的 HTTP 状态码为 422。
|
||||||
|
ErrUnprocessableEntity = NewError(http.StatusUnprocessableEntity, -106, "上传了不被支持的附件") |
||||||
|
|
||||||
|
// ErrTooManyRequests 客户端的请求次数超过限额。
|
||||||
|
// 响应的 HTTP 状态码为 429。
|
||||||
|
ErrTooManyRequests = NewError(http.StatusTooManyRequests, -107, "请求次数超过限额") |
||||||
|
|
||||||
|
// ErrSeeOther 表示需要参考另一个 URL 才能完成接收的请求操作,
|
||||||
|
// 当请求方式使用 POST、PUT 和 DELETE 时,对应的 HTTP 状态码为 303,
|
||||||
|
// 其它的请求方式在大多数情况下应该使用 400 状态码。
|
||||||
|
ErrSeeOther = NewError(http.StatusSeeOther, -108, "需要更进一步才能完成操作") |
||||||
|
|
||||||
|
// ErrBadRequest 服务器不理解客户端的请求。
|
||||||
|
// 对应 HTTP 状态码为 400。
|
||||||
|
ErrBadRequest = NewError(http.StatusBadRequest, -109, "请求错误") |
||||||
|
|
||||||
|
// ErrBadParams 客户端提交的参数不符合要求
|
||||||
|
// 对应 HTTP 状态码为 400。
|
||||||
|
ErrBadParams = NewError(http.StatusBadRequest, -110, "参数错误") |
||||||
|
|
||||||
|
// ErrRecordNotFound 访问的数据不存在
|
||||||
|
// 对应 HTTP 状态码为 404。
|
||||||
|
ErrRecordNotFound = NewError(http.StatusNotFound, -111, "访问的数据不存在") |
||||||
|
) |
||||||
|
|
||||||
|
type Error struct { |
||||||
|
// 被包装的错误对象
|
||||||
|
internal error |
||||||
|
// 响应的 HTTP 状态码
|
||||||
|
status int |
||||||
|
// 错误码
|
||||||
|
code int |
||||||
|
// 错误提示消息
|
||||||
|
text string |
||||||
|
// 错误携带的响应数据
|
||||||
|
data any |
||||||
|
} |
||||||
|
|
||||||
|
func NewError(status, code int, text string) *Error { |
||||||
|
return &Error{nil, status, code, text, nil} |
||||||
|
} |
||||||
|
|
||||||
|
// Code 返回错误码
|
||||||
|
func (e *Error) Code() int { |
||||||
|
return e.code |
||||||
|
} |
||||||
|
|
||||||
|
// Text 返回错误提示
|
||||||
|
func (e *Error) Text() string { |
||||||
|
return e.text |
||||||
|
} |
||||||
|
|
||||||
|
// Data 返回携带的数据
|
||||||
|
func (e *Error) Data() any { |
||||||
|
return e.data |
||||||
|
} |
||||||
|
|
||||||
|
// Internal 返回原始错误
|
||||||
|
func (e *Error) Internal() error { |
||||||
|
return e.internal |
||||||
|
} |
||||||
|
|
||||||
|
// Unwrap 支持 errors.Unwrap() 方法
|
||||||
|
func (e *Error) Unwrap() error { |
||||||
|
return e.Internal() |
||||||
|
} |
||||||
|
|
||||||
|
// WithInternal 通过实际错误对象派生新的实例
|
||||||
|
func (e *Error) WithInternal(err error) *Error { |
||||||
|
// 由于错误比较复杂,不好做完全等于,
|
||||||
|
// 在这里就直接复制当前对象
|
||||||
|
c := *e |
||||||
|
c.internal = err |
||||||
|
return &c |
||||||
|
} |
||||||
|
|
||||||
|
// WithStatus 通过 HTTP 状态码派生新的实例
|
||||||
|
func (e *Error) WithStatus(status int) *Error { |
||||||
|
if e.status != status { |
||||||
|
c := *e |
||||||
|
c.status = status |
||||||
|
return &c |
||||||
|
} |
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
// WithText 通过新的错误提示派生新的实例
|
||||||
|
func (e *Error) WithText(text string) *Error { |
||||||
|
if text != e.text { |
||||||
|
c := *e |
||||||
|
c.text = text |
||||||
|
return &c |
||||||
|
} |
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
// WithData 通过携带数据派生新的实例
|
||||||
|
func (e *Error) WithData(data any) *Error { |
||||||
|
if e.data != data { |
||||||
|
c := *e |
||||||
|
c.data = data |
||||||
|
return &c |
||||||
|
} |
||||||
|
return e |
||||||
|
} |
||||||
|
|
||||||
|
// String 实现 fmt.Stringer 接口
|
||||||
|
func (e *Error) String() string { |
||||||
|
return strings.TrimSpace(fmt.Sprintf("%d %s", e.code, e.text)) |
||||||
|
} |
||||||
|
|
||||||
|
// Error 实现错误接口
|
||||||
|
func (e *Error) Error() string { |
||||||
|
return e.String() |
||||||
|
} |
Loading…
Reference in new issue