Compare commits
No commits in common. '9b751c066c8e147d313ed674cb3b43893fe2a7f5' and 'faded4e634563291c0cb419df16ddbc681627925' have entirely different histories.
9b751c066c
...
faded4e634
@ -1,19 +0,0 @@ |
|||||||
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"` |
|
||||||
} |
|
@ -1,11 +0,0 @@ |
|||||||
package errs |
|
||||||
|
|
||||||
import ( |
|
||||||
"net/http" |
|
||||||
"sorbet/pkg/rsp" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
ErrWechatNotBound = rsp.NewError(http.StatusBadRequest, 10001, "微信尚未绑定") |
|
||||||
ErrLoginWithUserpwd = rsp.NewError(http.StatusUnauthorized, 10002, "用户名或密码错误") |
|
||||||
) |
|
@ -0,0 +1,136 @@ |
|||||||
|
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() |
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
package runtime |
package middleware |
||||||
|
|
||||||
import ( |
import ( |
||||||
"context" |
"context" |
@ -1,92 +0,0 @@ |
|||||||
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,18 +0,0 @@ |
|||||||
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"), |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,18 @@ |
|||||||
|
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,78 +0,0 @@ |
|||||||
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, |
|
||||||
}) |
|
||||||
} |
|
@ -0,0 +1,16 @@ |
|||||||
|
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) |
||||||
|
} |
@ -1,67 +0,0 @@ |
|||||||
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, |
|
||||||
}) |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
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), |
|
||||||
) |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
package util |
|
@ -1,110 +1,18 @@ |
|||||||
package db |
package db |
||||||
|
|
||||||
import ( |
import "gorm.io/gorm" |
||||||
"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]{db: db, expr: &Expr{}} |
return &DeleteBuilder[T]{Expr{}, db} |
||||||
} |
} |
||||||
|
|
||||||
func (d *DeleteBuilder[T]) Eq(col string, val any) *DeleteBuilder[T] { |
func (b *DeleteBuilder[T]) Commit() (int64, error) { |
||||||
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 := d.db.Scopes(d.expr.Scopes).Delete(&t) |
res := b.db.Scopes(b.Scopes).Delete(&t) |
||||||
return res.RowsAffected, res.Error |
return res.RowsAffected, res.Error |
||||||
} |
} |
||||||
|
@ -1,154 +0,0 @@ |
|||||||
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