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 ( |
||||
"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 |
||||
|
||||
import "gorm.io/gorm" |
||||
import ( |
||||
"gorm.io/gorm" |
||||
) |
||||
|
||||
type DeleteBuilder[T any] struct { |
||||
Expr |
||||
db *gorm.DB |
||||
expr *Expr |
||||
} |
||||
|
||||
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 |
||||
res := b.db.Scopes(b.Scopes).Delete(&t) |
||||
res := d.db.Scopes(d.expr.Scopes).Delete(&t) |
||||
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