Compare commits

...

5 Commits

  1. 4
      internal/entities/company.go
  2. 19
      internal/entities/company_auth_ticket.go
  3. 10
      internal/entities/company_department.go
  4. 7
      internal/entities/company_employee.go
  5. 2
      internal/entities/feature_category.go
  6. 2
      internal/entities/feature_content_chapter.go
  7. 2
      internal/entities/resource_category.go
  8. 2
      internal/entities/system_menu.go
  9. 2
      internal/entities/system_permission.go
  10. 2
      internal/entities/system_user.go
  11. 11
      internal/errs/errs.go
  12. 35
      internal/init.go
  13. 136
      internal/middleware/cors.go
  14. 92
      internal/repositories/company_auth_ticket.go
  15. 18
      internal/repositories/company_employee.go
  16. 18
      internal/repositories/company_staff.go
  17. 26
      internal/runtime/server.go
  18. 2
      internal/runtime/server_logger.go
  19. 6
      internal/runtime/server_recover.go
  20. 78
      internal/services/company/controller/company_employee_controller.go
  21. 16
      internal/services/company/controller/company_staff_controller.go
  22. 10
      internal/services/company/request/company_employee_upsert_request.go
  23. 2
      internal/services/company/service.go
  24. 67
      internal/services/system/controller/system_auth_controller.go
  25. 15
      internal/services/system/request/system_auth_login_request.go
  26. 1
      internal/services/system/service.go
  27. 13
      internal/util/password.go
  28. 1
      internal/util/redis.go
  29. 16
      pkg/crud/controller.go
  30. 9
      pkg/crud/echo_utils.go
  31. 104
      pkg/db/delete_builder.go
  32. 4
      pkg/db/expr.go
  33. 96
      pkg/db/query_builder.go
  34. 16
      pkg/db/repository.go
  35. 144
      pkg/db/update_builder.go
  36. 154
      pkg/rs/error.go
  37. 27
      pkg/rsp/error.go
  38. 12
      pkg/rsp/respond_utils.go
  39. 2
      scripts/gen/gen.go

@ -18,7 +18,7 @@ type Company struct {
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"` UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"` DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Principal *CompanyStaff `json:"principal" xml:"principal"` Principal *CompanyEmployee `json:"principal" xml:"principal"`
Staffs []*CompanyStaff `json:"staffs" xml:"staffs"` Employees []*CompanyEmployee `json:"employees" xml:"employees"`
Departments []*CompanyDepartment `json:"departments" xml:"departments"` Departments []*CompanyDepartment `json:"departments" xml:"departments"`
} }

@ -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"`
}

@ -11,7 +11,7 @@ import (
// CompanyDepartment 公司部门表 // CompanyDepartment 公司部门表
type CompanyDepartment struct { type CompanyDepartment struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:部门编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:部门编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级部门编号"` PID *uint `json:"pid" xml:"pid" gorm:"column:pid;comment:上级部门编号"`
CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"` CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"`
PrincipalID *uint `json:"principal_id" xml:"principal_id" gorm:"comment:负责人编号(员工)"` PrincipalID *uint `json:"principal_id" xml:"principal_id" gorm:"comment:负责人编号(员工)"`
Name string `json:"name" xml:"name" gorm:"size:50;not null;comment:部门名称"` Name string `json:"name" xml:"name" gorm:"size:50;not null;comment:部门名称"`
@ -21,12 +21,12 @@ type CompanyDepartment struct {
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"` UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"` DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Staffs []*CompanyStaff `json:"staffs" xml:"staffs" gorm:"many2many:company_staff_to_department_relations"` Employees []*CompanyEmployee `json:"employees" xml:"employees" gorm:"many2many:company_employee_to_department_relations"`
Courses []*FeatureContent `json:"courses" xml:"courses" gorm:"many2many:company_course_to_department_relations"` Courses []*FeatureContent `json:"courses" xml:"courses" gorm:"many2many:company_course_to_department_relations"`
Children []*CompanyDepartment `json:"children" xml:"children" gorm:"foreignKey:PID"` Children []*CompanyDepartment `json:"children" xml:"children" gorm:"foreignKey:PID"`
} }
func (c *CompanyDepartment) BeforeCreate(tx *gorm.DB) error { func (c *CompanyDepartment) BeforeCreate(_ *gorm.DB) error {
if c.CompanyID == 0 { if c.CompanyID == 0 {
return errors.New("缺少所属公司编号") return errors.New("缺少所属公司编号")
} }

@ -6,8 +6,8 @@ import (
"time" "time"
) )
// CompanyStaff 公司员工表 // CompanyEmployee 公司员工表
type CompanyStaff struct { type CompanyEmployee struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:员工编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:员工编号"`
CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"` CompanyID uint `json:"company_id" xml:"company_id" gorm:"comment:所属公司编号"`
Name string `json:"name" xml:"name" gorm:"size:20;not null;comment:员工姓名"` Name string `json:"name" xml:"name" gorm:"size:20;not null;comment:员工姓名"`
@ -22,5 +22,6 @@ type CompanyStaff struct {
UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"` UpdatedAt time.Time `json:"update_time" xml:"update_time" gorm:"<-:false;comment:更新时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"` DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Departments []*CompanyDepartment `json:"departments" xml:"departments" gorm:"many2many:company_staff_to_department_relations"` Company *Company `json:"company" xml:"company" gorm:"foreignKey:CompanyID"`
Departments []*CompanyDepartment `json:"departments" xml:"departments" gorm:"many2many:company_employee_to_department_relations"`
} }

@ -9,7 +9,7 @@ import (
// FeatureCategory 栏目分类表 // FeatureCategory 栏目分类表
type FeatureCategory struct { type FeatureCategory struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:栏目分类编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:栏目分类编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级分类编号"` PID *uint `json:"pid" xml:"pid" gorm:"column:pid;comment:上级分类编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"index:,unique,composite:idx_title_with_feature;comment:所属栏目编号"` FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"index:,unique,composite:idx_title_with_feature;comment:所属栏目编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;index:,unique,composite:idx_title_with_feature;comment:栏目分类标题"` Title string `json:"title" xml:"title" gorm:"size:25;not null;index:,unique,composite:idx_title_with_feature;comment:栏目分类标题"`
Description string `json:"description" xml:"description" gorm:"size:250;comment:栏目分类描述"` Description string `json:"description" xml:"description" gorm:"size:250;comment:栏目分类描述"`

@ -9,7 +9,7 @@ import (
// FeatureContentChapter 栏目内容章回表 // FeatureContentChapter 栏目内容章回表
type FeatureContentChapter struct { type FeatureContentChapter struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:章回编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:章回编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级章回编号"` PID *uint `json:"pid" xml:"pid" gorm:"column:pid;comment:上级章回编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"` FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
ContentID uint `json:"content_id" xml:"content_id" gorm:"comment:所属内容编号"` ContentID uint `json:"content_id" xml:"content_id" gorm:"comment:所属内容编号"`
Title string `json:"title" xml:"title" gorm:"size:100;not null;comment:章回标题"` Title string `json:"title" xml:"title" gorm:"size:100;not null;comment:章回标题"`

@ -9,7 +9,7 @@ import (
// ResourceCategory 资源分类表 // ResourceCategory 资源分类表
type ResourceCategory struct { type ResourceCategory struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:资源分类编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:资源分类编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级分类编号"` PID *uint `json:"pid" xml:"pid" gorm:"column:pid;comment:上级分类编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;uniqueIndex;comment:资源分类名称"` Title string `json:"title" xml:"title" gorm:"size:25;not null;uniqueIndex;comment:资源分类名称"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"` Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Status bool `json:"status" xml:"status" gorm:"comment:状态"` Status bool `json:"status" xml:"status" gorm:"comment:状态"`

@ -9,7 +9,7 @@ import (
// SystemMenu 系统菜单表 // SystemMenu 系统菜单表
type SystemMenu struct { type SystemMenu struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:菜单编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:菜单编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级菜单编号"` PID *uint `json:"pid" xml:"pid" gorm:"column:pid;comment:上级菜单编号"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:菜单标题"` Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:菜单标题"`
Icon string `json:"icon" xml:"icon" gorm:"comment:菜单图标"` Icon string `json:"icon" xml:"icon" gorm:"comment:菜单图标"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"` Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`

@ -7,7 +7,7 @@ import (
type SystemPermission struct { type SystemPermission struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:权限编号"` ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:权限编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级权限编号"` PID *uint `json:"pid" xml:"pid" gorm:"column:pid;comment:上级权限编号"`
Name string `json:"name" xml:"name" gorm:"size:25;not null;comment:权限名称"` Name string `json:"name" xml:"name" gorm:"size:25;not null;comment:权限名称"`
Type string `json:"type" xml:"type" gorm:"size:25;not null;index;comment:权限类型"` Type string `json:"type" xml:"type" gorm:"size:25;not null;index;comment:权限类型"`
Identifier string `json:"identifier" xml:"identifier" gorm:"size:25;not null;uniqueIndex;comment:权限标识"` Identifier string `json:"identifier" xml:"identifier" gorm:"size:25;not null;uniqueIndex;comment:权限标识"`

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

@ -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, "用户名或密码错误")
)

@ -9,14 +9,14 @@ import (
"sorbet/internal/services/feature" "sorbet/internal/services/feature"
"sorbet/internal/services/resource" "sorbet/internal/services/resource"
"sorbet/internal/services/system" "sorbet/internal/services/system"
"sorbet/internal/util"
"sorbet/pkg/db" "sorbet/pkg/db"
"sorbet/pkg/env" "sorbet/pkg/env"
"sorbet/pkg/log" "sorbet/pkg/log"
) )
func Init() error { func Init() error {
err := syncEntities() if err := syncEntities(); err != nil {
if err != nil {
if !errors.Is(err, db.ErrNoCodeFirst) { if !errors.Is(err, db.ErrNoCodeFirst) {
return err return err
} }
@ -24,6 +24,9 @@ func Init() error {
log.Warn("同步数据表结构需要开启 [DB_CODE_FIRST],在生产模式下请务必关闭。") log.Warn("同步数据表结构需要开启 [DB_CODE_FIRST],在生产模式下请务必关闭。")
} }
} }
if err := initSystemUser(); err != nil {
return err
}
return useServlets() return useServlets()
} }
@ -32,17 +35,18 @@ func syncEntities() error {
return db.Sync( return db.Sync(
&entities.Company{}, &entities.Company{},
&entities.CompanyDepartment{}, &entities.CompanyDepartment{},
&entities.CompanyStaff{}, &entities.CompanyEmployee{},
&entities.Config{}, &entities.CompanyAuthTicket{},
&entities.ConfigGroup{}, &entities.ConfigGroup{},
&entities.Config{},
&entities.Feature{}, &entities.Feature{},
&entities.FeatureCategory{}, &entities.FeatureCategory{},
&entities.FeatureConfig{}, &entities.FeatureConfig{},
&entities.FeatureContent{}, &entities.FeatureContent{},
&entities.FeatureContentChapter{}, &entities.FeatureContentChapter{},
&entities.FeatureContentDetail{}, &entities.FeatureContentDetail{},
&entities.Resource{},
&entities.ResourceCategory{}, &entities.ResourceCategory{},
&entities.Resource{},
&entities.SystemLog{}, &entities.SystemLog{},
&entities.SystemMenu{}, &entities.SystemMenu{},
&entities.SystemPermission{}, &entities.SystemPermission{},
@ -52,6 +56,27 @@ func syncEntities() error {
) )
} }
func initSystemUser() error {
var count int64
err := db.
Model(&entities.SystemUser{}).
Where("username", "admin").
Count(&count).
Error
if err != nil {
return err
}
if count > 0 {
return nil
}
hash, _ := util.PasswordHash("111111")
_, err = db.Create(&entities.SystemUser{
Username: "admin",
Password: hash,
})
return err
}
func useServlets() error { func useServlets() error {
return runtime.Use( return runtime.Use(
&config.Service{}, &config.Service{},

@ -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"),
}
}

@ -34,15 +34,35 @@ func newEchoFramework() (*echo.Echo, error) {
e.HTTPErrorHandler = rsp.HTTPErrorHandler e.HTTPErrorHandler = rsp.HTTPErrorHandler
e.Debug = !env.IsEnv("prod") e.Debug = !env.IsEnv("prod")
e.Logger = util.NewEchoLogger() e.Logger = util.NewEchoLogger()
e.Use(middleware.Recover())
e.Use(middleware.CORS()) // 配置错误捕获
e.Use(middleware.Logger()) e.Use(Recover)
// 配置跨域
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: env.List("CORS_ALLOW_ORIGINS", []string{"*"}),
AllowMethods: env.List("CORS_ALLOW_METHODS", []string{"GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"}),
AllowCredentials: env.Bool("CORS_ALLOW_CREDENTIALS", false),
AllowHeaders: env.List("CORS_ALLOW_HEADERS", []string{"X-TICKET"}),
ExposeHeaders: env.List("CORS_EXPOSE_HEADERS"),
MaxAge: env.Int("CORS_MAX_AGE", 0),
}))
// 配置日志
e.Use(Logger)
// 配置静态服务
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: env.String("STATIC_ROOT", "web"),
Index: env.String("STATIC_INDEX", "index.html"),
HTML5: env.Bool("STATIC_HTML5", false),
Browse: env.Bool("STATIC_DIRECTORY_BROWSE", false),
}))
for _, servlet := range servlets { for _, servlet := range servlets {
group := e.Group("") group := e.Group("")
for _, routable := range servlet.Routes() { for _, routable := range servlet.Routes() {
routable.InitRoutes(group) routable.InitRoutes(group)
} }
} }
routes := e.Routes() routes := e.Routes()
e.GET("/_routes", func(c echo.Context) error { e.GET("/_routes", func(c echo.Context) error {
return c.JSON(http.StatusOK, routes) return c.JSON(http.StatusOK, routes)

@ -1,4 +1,4 @@
package middleware package runtime
import ( import (
"context" "context"

@ -1,4 +1,4 @@
package middleware package runtime
import ( import (
"fmt" "fmt"
@ -55,8 +55,8 @@ var DefaultRecoverConfig = RecoverConfig{
// Recover returns a middleware which recovers from panics anywhere in the chain // Recover returns a middleware which recovers from panics anywhere in the chain
// and handles the control to the centralized HTTPErrorHandler. // and handles the control to the centralized HTTPErrorHandler.
func Recover() echo.MiddlewareFunc { func Recover(next echo.HandlerFunc) echo.HandlerFunc {
return RecoverWithConfig(DefaultRecoverConfig) return RecoverWithConfig(DefaultRecoverConfig)(next)
} }
func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc { func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {

@ -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)
}

@ -4,7 +4,7 @@ import (
"sorbet/internal/entities" "sorbet/internal/entities"
) )
type CompanyStaffUpsertRequest struct { type CompanyEmployeeUpsertRequest struct {
ID uint `json:"id" xml:"id" form:"id" path:"id"` ID uint `json:"id" xml:"id" form:"id" path:"id"`
CompanyID uint `json:"company_id" xml:"company_id" form:"company_id"` CompanyID uint `json:"company_id" xml:"company_id" form:"company_id"`
Name string `json:"name" xml:"name" form:"name"` Name string `json:"name" xml:"name" form:"name"`
@ -16,11 +16,11 @@ type CompanyStaffUpsertRequest struct {
IsAdmin bool `json:"is_admin" xml:"is_admin" form:"is_admin"` IsAdmin bool `json:"is_admin" xml:"is_admin" form:"is_admin"`
} }
func (c *CompanyStaffUpsertRequest) GetID() any { func (c *CompanyEmployeeUpsertRequest) GetID() any {
return c.ID return c.ID
} }
func (c *CompanyStaffUpsertRequest) ToMap() map[string]any { func (c *CompanyEmployeeUpsertRequest) ToMap() map[string]any {
return map[string]any{ return map[string]any{
"id": c.ID, "id": c.ID,
"company_id": c.CompanyID, "company_id": c.CompanyID,
@ -34,8 +34,8 @@ func (c *CompanyStaffUpsertRequest) ToMap() map[string]any {
} }
} }
func (c *CompanyStaffUpsertRequest) ToEntity() any { func (c *CompanyEmployeeUpsertRequest) ToEntity() any {
return &entities.CompanyStaff{ return &entities.CompanyEmployee{
ID: c.ID, ID: c.ID,
CompanyID: c.CompanyID, CompanyID: c.CompanyID,
Name: c.Name, Name: c.Name,

@ -24,7 +24,7 @@ func (s *Service) Routes() []runtime.Routable {
return []runtime.Routable{ return []runtime.Routable{
&controller.CompanyController{}, &controller.CompanyController{},
&controller.CompanyDepartmentController{}, &controller.CompanyDepartmentController{},
&controller.CompanyStaffController{}, &controller.CompanyEmployeeController{},
} }
} }

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

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

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

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

@ -0,0 +1 @@
package util

@ -77,6 +77,14 @@ func (ctr *Controller[Entity, Upsert]) ORM(c echo.Context) (*gorm.DB, error) {
return db.DB().WithContext(c.Request().Context()), nil 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 实例 // Repository 获取 Repository 实例
func (ctr *Controller[Entity, Upsert]) Repository(c echo.Context) (*db.Repository[Entity], error) { func (ctr *Controller[Entity, Upsert]) Repository(c echo.Context) (*db.Repository[Entity], error) {
orm, err := ctr.ORM(c) 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 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 创建数据 // Create 创建数据
func (ctr *Controller[Entity, Upsert]) Create(c echo.Context) error { func (ctr *Controller[Entity, Upsert]) Create(c echo.Context) error {
return ctr.upsert(c, true) return ctr.upsert(c, true)

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

@ -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
} }

@ -9,6 +9,10 @@ type Expr struct {
clauses []clause.Expression clauses []clause.Expression
} }
func NewExpr() *Expr {
return &Expr{}
}
func (e *Expr) add(expr clause.Expression) *Expr { func (e *Expr) add(expr clause.Expression) *Expr {
e.clauses = append(e.clauses, expr) e.clauses = append(e.clauses, expr)
return e return e

@ -9,10 +9,10 @@ import (
// QueryBuilder 查询构造器 // QueryBuilder 查询构造器
// TODO(hupeh):实现 joins 和表别名 // TODO(hupeh):实现 joins 和表别名
type QueryBuilder[T any] struct { type QueryBuilder[T any] struct {
Expr
db *gorm.DB db *gorm.DB
selects []string selects []string
omits []string omits []string
expr *Expr
orders []string orders []string
limit int limit int
offset int offset int
@ -21,7 +21,7 @@ type QueryBuilder[T any] struct {
} }
func NewQueryBuilder[T any](db *gorm.DB) *QueryBuilder[T] { func NewQueryBuilder[T any](db *gorm.DB) *QueryBuilder[T] {
return &QueryBuilder[T]{Expr: Expr{}, db: db} return &QueryBuilder[T]{expr: &Expr{}, db: db}
} }
type preload struct { type preload struct {
@ -46,6 +46,96 @@ func (q *QueryBuilder[T]) Omit(columns ...string) *QueryBuilder[T] {
return q return q
} }
func (q *QueryBuilder[T]) Eq(col string, val any) *QueryBuilder[T] {
q.expr.Eq(col, val)
return q
}
func (q *QueryBuilder[T]) Neq(col string, val any) *QueryBuilder[T] {
q.expr.Neq(col, val)
return q
}
func (q *QueryBuilder[T]) Lt(col string, val any) *QueryBuilder[T] {
q.expr.Lt(col, val)
return q
}
func (q *QueryBuilder[T]) Lte(col string, val any) *QueryBuilder[T] {
q.expr.Lte(col, val)
return q
}
func (q *QueryBuilder[T]) Gt(col string, val any) *QueryBuilder[T] {
q.expr.Gt(col, val)
return q
}
func (q *QueryBuilder[T]) Gte(col string, val any) *QueryBuilder[T] {
q.expr.Gte(col, val)
return q
}
func (q *QueryBuilder[T]) Between(col string, less, more any) *QueryBuilder[T] {
q.expr.Between(col, less, more)
return q
}
func (q *QueryBuilder[T]) NotBetween(col string, less, more any) *QueryBuilder[T] {
q.expr.NotBetween(col, less, more)
return q
}
func (q *QueryBuilder[T]) IsNull(col string) *QueryBuilder[T] {
q.expr.IsNull(col)
return q
}
func (q *QueryBuilder[T]) NotNull(col string) *QueryBuilder[T] {
q.expr.NotNull(col)
return q
}
func (q *QueryBuilder[T]) Like(col, tpl string) *QueryBuilder[T] {
q.expr.Like(col, tpl)
return q
}
func (q *QueryBuilder[T]) NotLike(col, tpl string) *QueryBuilder[T] {
q.expr.NotLike(col, tpl)
return q
}
func (q *QueryBuilder[T]) In(col string, values ...any) *QueryBuilder[T] {
q.expr.In(col, values...)
return q
}
func (q *QueryBuilder[T]) NotIn(col string, values ...any) *QueryBuilder[T] {
q.expr.NotIn(col, values...)
return q
}
func (q *QueryBuilder[T]) When(condition bool, then func(ex *Expr), elses ...func(ex *Expr)) *QueryBuilder[T] {
q.expr.When(condition, then, elses...)
return q
}
func (q *QueryBuilder[T]) Or(or func(ex *Expr)) *QueryBuilder[T] {
q.expr.Or(or)
return q
}
func (q *QueryBuilder[T]) And(and func(ex *Expr)) *QueryBuilder[T] {
q.expr.And(and)
return q
}
func (q *QueryBuilder[T]) Not(not func(ex *Expr)) *QueryBuilder[T] {
q.expr.Not(not)
return q
}
func (q *QueryBuilder[T]) DescentBy(columns ...string) *QueryBuilder[T] { func (q *QueryBuilder[T]) DescentBy(columns ...string) *QueryBuilder[T] {
for _, col := range columns { for _, col := range columns {
q.orders = append(q.orders, col+" DESC") q.orders = append(q.orders, col+" DESC")
@ -113,7 +203,7 @@ func (q *QueryBuilder[T]) scopesWithoutEffect(tx *gorm.DB) *gorm.DB {
if len(q.distinct) > 0 { if len(q.distinct) > 0 {
tx = tx.Distinct(q.distinct...) tx = tx.Distinct(q.distinct...)
} }
return q.Expr.Scopes(tx) return q.expr.Scopes(tx)
} }
func (q *QueryBuilder[T]) Count() (int64, error) { func (q *QueryBuilder[T]) Count() (int64, error) {

@ -70,6 +70,20 @@ func (r *Repository[T]) GetByID(ctx context.Context, id any) (*T, error) {
return &entity, nil return &entity, nil
} }
func (r *Repository[T]) GetBy(ctx context.Context, expr ...*Expr) (*T, error) {
var entity T
err := r.DB(ctx).Model(&entity).Scopes(func(tx *gorm.DB) *gorm.DB {
for _, e := range expr {
tx = e.Scopes(tx)
}
return tx
}).First(&entity).Error
if err != nil {
return nil, err
}
return &entity, nil
}
func (r *Repository[T]) Find(ctx context.Context, expr ...*Expr) ([]*T, error) { func (r *Repository[T]) Find(ctx context.Context, expr ...*Expr) ([]*T, error) {
var entity T var entity T
var items []*T var items []*T
@ -88,7 +102,7 @@ func (r *Repository[T]) Find(ctx context.Context, expr ...*Expr) ([]*T, error) {
func (r *Repository[T]) Paginate(ctx context.Context, expr ...*Expr) (*Pager[T], error) { func (r *Repository[T]) Paginate(ctx context.Context, expr ...*Expr) (*Pager[T], error) {
qb := NewQueryBuilder[T](r.DB(ctx)) qb := NewQueryBuilder[T](r.DB(ctx))
for _, e := range expr { for _, e := range expr {
qb.Expr = *e qb.expr = e
} }
return qb.Paginate() return qb.Paginate()
} }

@ -7,55 +7,145 @@ import (
) )
type UpdateBuilder[T any] struct { type UpdateBuilder[T any] struct {
Expr
db *gorm.DB db *gorm.DB
selects []string selects []string
omits []string omits []string
onConflict *clause.OnConflict onConflict *clause.OnConflict
expr *Expr
} }
func NewUpdateBuilder[T any](db *gorm.DB) *UpdateBuilder[T] { func NewUpdateBuilder[T any](db *gorm.DB) *UpdateBuilder[T] {
return &UpdateBuilder[T]{Expr: Expr{}, db: db} return &UpdateBuilder[T]{expr: &Expr{}, db: db}
} }
func (b *UpdateBuilder[T]) Select(columns ...string) *UpdateBuilder[T] { func (u *UpdateBuilder[T]) Select(columns ...string) *UpdateBuilder[T] {
b.selects = append(b.selects, columns...) u.selects = append(u.selects, columns...)
return b return u
} }
func (b *UpdateBuilder[T]) Omit(columns ...string) *UpdateBuilder[T] { func (u *UpdateBuilder[T]) Omit(columns ...string) *UpdateBuilder[T] {
b.omits = append(b.omits, columns...) u.omits = append(u.omits, columns...)
return b return u
} }
func (b *UpdateBuilder[T]) OnConflict(conflict clause.OnConflict) *UpdateBuilder[T] { func (u *UpdateBuilder[T]) Eq(col string, val any) *UpdateBuilder[T] {
if b.onConflict == nil { u.expr.Eq(col, val)
b.onConflict = &conflict return u
}
func (u *UpdateBuilder[T]) Neq(col string, val any) *UpdateBuilder[T] {
u.expr.Neq(col, val)
return u
}
func (u *UpdateBuilder[T]) Lt(col string, val any) *UpdateBuilder[T] {
u.expr.Lt(col, val)
return u
}
func (u *UpdateBuilder[T]) Lte(col string, val any) *UpdateBuilder[T] {
u.expr.Lte(col, val)
return u
}
func (u *UpdateBuilder[T]) Gt(col string, val any) *UpdateBuilder[T] {
u.expr.Gt(col, val)
return u
}
func (u *UpdateBuilder[T]) Gte(col string, val any) *UpdateBuilder[T] {
u.expr.Gte(col, val)
return u
}
func (u *UpdateBuilder[T]) Between(col string, less, more any) *UpdateBuilder[T] {
u.expr.Between(col, less, more)
return u
}
func (u *UpdateBuilder[T]) NotBetween(col string, less, more any) *UpdateBuilder[T] {
u.expr.NotBetween(col, less, more)
return u
}
func (u *UpdateBuilder[T]) IsNull(col string) *UpdateBuilder[T] {
u.expr.IsNull(col)
return u
}
func (u *UpdateBuilder[T]) NotNull(col string) *UpdateBuilder[T] {
u.expr.NotNull(col)
return u
}
func (u *UpdateBuilder[T]) Like(col, tpl string) *UpdateBuilder[T] {
u.expr.Like(col, tpl)
return u
}
func (u *UpdateBuilder[T]) NotLike(col, tpl string) *UpdateBuilder[T] {
u.expr.NotLike(col, tpl)
return u
}
func (u *UpdateBuilder[T]) In(col string, values ...any) *UpdateBuilder[T] {
u.expr.In(col, values...)
return u
}
func (u *UpdateBuilder[T]) NotIn(col string, values ...any) *UpdateBuilder[T] {
u.expr.NotIn(col, values...)
return u
}
func (u *UpdateBuilder[T]) When(condition bool, then func(ex *Expr), elses ...func(ex *Expr)) *UpdateBuilder[T] {
u.expr.When(condition, then, elses...)
return u
}
func (u *UpdateBuilder[T]) Or(or func(ex *Expr)) *UpdateBuilder[T] {
u.expr.Or(or)
return u
}
func (u *UpdateBuilder[T]) And(and func(ex *Expr)) *UpdateBuilder[T] {
u.expr.And(and)
return u
}
func (u *UpdateBuilder[T]) Not(not func(ex *Expr)) *UpdateBuilder[T] {
u.expr.Not(not)
return u
}
func (u *UpdateBuilder[T]) OnConflict(conflict clause.OnConflict) *UpdateBuilder[T] {
if u.onConflict == nil {
u.onConflict = &conflict
} else { } else {
b.onConflict.Columns = conflict.Columns u.onConflict.Columns = conflict.Columns
b.onConflict.Where = conflict.Where u.onConflict.Where = conflict.Where
b.onConflict.TargetWhere = conflict.TargetWhere u.onConflict.TargetWhere = conflict.TargetWhere
b.onConflict.OnConstraint = conflict.OnConstraint u.onConflict.OnConstraint = conflict.OnConstraint
b.onConflict.DoNothing = conflict.DoNothing u.onConflict.DoNothing = conflict.DoNothing
b.onConflict.DoUpdates = conflict.DoUpdates u.onConflict.DoUpdates = conflict.DoUpdates
b.onConflict.UpdateAll = conflict.UpdateAll u.onConflict.UpdateAll = conflict.UpdateAll
} }
return b return u
} }
func (b *UpdateBuilder[T]) Scopes(tx *gorm.DB) *gorm.DB { func (u *UpdateBuilder[T]) Scopes(tx *gorm.DB) *gorm.DB {
if b.selects != nil { if u.selects != nil {
tx = tx.Select(b.selects) tx = tx.Select(u.selects)
} }
if b.omits != nil { if u.omits != nil {
tx = tx.Omit(b.omits...) tx = tx.Omit(u.omits...)
} }
return b.Expr.Scopes(tx) return u.expr.Scopes(tx)
} }
func (b *UpdateBuilder[T]) Commit(values map[string]any) (int64, error) { func (u *UpdateBuilder[T]) Commit(values map[string]any) (int64, error) {
var entity T var entity T
res := b.db.Model(&entity).Scopes(b.Scopes).Updates(values) res := u.db.Model(&entity).Scopes(u.Scopes).Updates(values)
if err := res.Error; err != nil { if err := res.Error; err != nil {
return res.RowsAffected, err return res.RowsAffected, err
} }

@ -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()
}

@ -67,10 +67,11 @@ type Error struct {
status int // HTTP 状态码 status int // HTTP 状态码
code int // 请求错误码 code int // 请求错误码
text string // 响应提示消息 text string // 响应提示消息
data any // 错误携带的响应数据
} }
func NewError(status, code int, text string) *Error { func NewError(status, code int, text string) *Error {
return &Error{nil, status, code, text} return &Error{nil, status, code, text, nil}
} }
func (e *Error) Code() int { func (e *Error) Code() int {
@ -81,6 +82,18 @@ func (e *Error) Text() string {
return e.text return e.text
} }
func (e *Error) Data() any {
return e.data
}
func (e *Error) Internal() error {
return e.internal
}
func (e *Error) Unwrap() error {
return e.Internal()
}
func (e *Error) WithInternal(err error) *Error { func (e *Error) WithInternal(err error) *Error {
c := *e c := *e
c.internal = err c.internal = err
@ -88,6 +101,9 @@ func (e *Error) WithInternal(err error) *Error {
} }
func (e *Error) WithStatus(status int) *Error { func (e *Error) WithStatus(status int) *Error {
if e.status == status {
return e
}
c := *e c := *e
c.status = status c.status = status
return &c return &c
@ -104,6 +120,15 @@ func (e *Error) WithText(text ...string) *Error {
return e return e
} }
func (e *Error) WithData(data any) *Error {
if e.data == data {
return e
}
c := *e
c.data = data
return &c
}
func (e *Error) AsProblem(label string) *Problem { func (e *Error) AsProblem(label string) *Problem {
return &Problem{ return &Problem{
Label: label, Label: label,

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"gorm.io/gorm"
"net/http" "net/http"
"runtime" "runtime"
"sorbet/pkg/ticket" "sorbet/pkg/ticket"
@ -129,6 +130,9 @@ func (r *response) result(c echo.Context) (m map[string]any, status int) {
m["code"] = ex.code m["code"] = ex.code
m["success"] = errors.Is(ex, ErrOK) m["success"] = errors.Is(ex, ErrOK)
m["message"] = ex.text m["message"] = ex.text
if data := ex.Data(); data != nil {
m["data"] = data
}
if status < 400 { if status < 400 {
status = ex.status status = ex.status
} }
@ -253,7 +257,7 @@ func respond(c echo.Context, o *response) error {
func relevantCaller() []string { func relevantCaller() []string {
pc := make([]uintptr, 16) pc := make([]uintptr, 16)
n := runtime.Callers(1, pc) n := runtime.Callers(3, pc)
frames := runtime.CallersFrames(pc[:n]) frames := runtime.CallersFrames(pc[:n])
var traces []string var traces []string
for { for {
@ -287,7 +291,11 @@ func Fail(c echo.Context, err error, opts ...Option) error {
for _, option := range opts { for _, option := range opts {
option(&o) option(&o)
} }
o.err = err if errors.Is(err, gorm.ErrRecordNotFound) {
o.err = ErrRecordNotFound
} else {
o.err = err
}
return respond(c, &o) return respond(c, &o)
} }

@ -31,7 +31,7 @@ func New{{pascal}}Repository(orm *gorm.DB) *{{pascal}}Repository {
var labels = map[string]string{ var labels = map[string]string{
"Company": "公司", "Company": "公司",
"CompanyDepartment": "公司部门", "CompanyDepartment": "公司部门",
"CompanyStaff": "公司员工", "CompanyEmployee": "公司员工",
"Config": "配置", "Config": "配置",
"ConfigGroup": "配置组", "ConfigGroup": "配置组",
"Feature": "栏目", "Feature": "栏目",

Loading…
Cancel
Save