parent
b3c82d7d6d
commit
fa896b66b6
@ -0,0 +1 @@ |
|||||||
|
package main |
@ -1,140 +0,0 @@ |
|||||||
package middleware |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/golang-jwt/jwt/v5" |
|
||||||
"github.com/labstack/echo-jwt/v4" |
|
||||||
"github.com/labstack/echo/v4" |
|
||||||
) |
|
||||||
|
|
||||||
// JWTConfig defines the config for JWT middleware.
|
|
||||||
type JWTConfig struct { |
|
||||||
// Skipper defines a function to skip middleware.
|
|
||||||
Skipper Skipper |
|
||||||
|
|
||||||
// BeforeFunc defines a function which is executed just before the middleware.
|
|
||||||
BeforeFunc BeforeFunc |
|
||||||
|
|
||||||
// SuccessHandler defines a function which is executed for a valid token.
|
|
||||||
SuccessHandler func(c echo.Context) |
|
||||||
|
|
||||||
// ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator
|
|
||||||
// function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key.
|
|
||||||
// It may be used to define a custom JWT error.
|
|
||||||
//
|
|
||||||
// Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler.
|
|
||||||
// This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users
|
|
||||||
// In that case you can use ErrorHandler to set default public JWT token value to request and continue with handler chain.
|
|
||||||
ErrorHandler func(c echo.Context, err error) error |
|
||||||
|
|
||||||
// ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to
|
|
||||||
// ignore the error (by returning `nil`).
|
|
||||||
// This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality.
|
|
||||||
// In that case you can use ErrorHandler to set a default public JWT token value in the request context
|
|
||||||
// and continue. Some logic down the remaining execution chain needs to check that (public) token value then.
|
|
||||||
ContinueOnIgnoredError bool |
|
||||||
|
|
||||||
// Context key to store user information from the token into context.
|
|
||||||
// Optional. Default value "user".
|
|
||||||
ContextKey string |
|
||||||
|
|
||||||
// Signing key to validate token.
|
|
||||||
// This is one of the three options to provide a token validation key.
|
|
||||||
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
|
|
||||||
// Required if neither user-defined KeyFunc nor SigningKeys is provided.
|
|
||||||
SigningKey any |
|
||||||
|
|
||||||
// Map of signing keys to validate token with kid field usage.
|
|
||||||
// This is one of the three options to provide a token validation key.
|
|
||||||
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
|
|
||||||
// Required if neither user-defined KeyFunc nor SigningKey is provided.
|
|
||||||
SigningKeys map[string]any |
|
||||||
|
|
||||||
// Signing method used to check the token's signing algorithm.
|
|
||||||
// Optional. Default value HS256.
|
|
||||||
SigningMethod string |
|
||||||
|
|
||||||
// KeyFunc defines a user-defined function that supplies the public key for a token validation.
|
|
||||||
// The function shall take care of verifying the signing algorithm and selecting the proper key.
|
|
||||||
// A user-defined KeyFunc can be useful if tokens are issued by an external party.
|
|
||||||
// Used by default ParseTokenFunc implementation.
|
|
||||||
//
|
|
||||||
// When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.
|
|
||||||
// This is one of the three options to provide a token validation key.
|
|
||||||
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
|
|
||||||
// Required if neither SigningKeys nor SigningKey is provided.
|
|
||||||
// Not used if custom ParseTokenFunc is set.
|
|
||||||
// Default to an internal implementation verifying the signing algorithm and selecting the proper key.
|
|
||||||
KeyFunc jwt.Keyfunc |
|
||||||
|
|
||||||
// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
|
|
||||||
// to extract token from the request.
|
|
||||||
// Optional. Default value "header:Authorization".
|
|
||||||
// Possible values:
|
|
||||||
// - "header:<name>" or "header:<name>:<cut-prefix>"
|
|
||||||
// `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
|
|
||||||
// value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
|
|
||||||
// want to cut is `<auth-scheme> ` note the space at the end.
|
|
||||||
// In case of JWT tokens `Authorization: Bearer <token>` prefix we cut is `Bearer `.
|
|
||||||
// If prefix is left empty the whole value is returned.
|
|
||||||
// - "query:<name>"
|
|
||||||
// - "param:<name>"
|
|
||||||
// - "cookie:<name>"
|
|
||||||
// - "form:<name>"
|
|
||||||
// Multiple sources example:
|
|
||||||
// - "header:Authorization:Bearer ,cookie:myowncookie"
|
|
||||||
TokenLookup string |
|
||||||
|
|
||||||
// TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context.
|
|
||||||
// This is one of the two options to provide a token extractor.
|
|
||||||
// The order of precedence is user-defined TokenLookupFuncs, and TokenLookup.
|
|
||||||
// You can also provide both if you want.
|
|
||||||
TokenLookupFuncs []ValuesExtractor |
|
||||||
|
|
||||||
// ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token
|
|
||||||
// parsing fails or parsed token is invalid.
|
|
||||||
// Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library
|
|
||||||
ParseTokenFunc func(c echo.Context, auth string) (any, error) |
|
||||||
|
|
||||||
// Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation.
|
|
||||||
// Not used if custom ParseTokenFunc is set.
|
|
||||||
// Optional. Defaults to function returning jwt.MapClaims
|
|
||||||
NewClaimsFunc func(c echo.Context) jwt.Claims |
|
||||||
} |
|
||||||
|
|
||||||
// Errors
|
|
||||||
var ( |
|
||||||
ErrJWTMissing = echojwt.ErrJWTMissing |
|
||||||
ErrJWTInvalid = echojwt.ErrJWTInvalid |
|
||||||
) |
|
||||||
|
|
||||||
func (config *JWTConfig) ToMiddleware() echo.MiddlewareFunc { |
|
||||||
return echojwt.WithConfig(echojwt.Config{ |
|
||||||
Skipper: config.Skipper, |
|
||||||
BeforeFunc: config.BeforeFunc, |
|
||||||
SuccessHandler: config.SuccessHandler, |
|
||||||
ErrorHandler: config.ErrorHandler, |
|
||||||
ContinueOnIgnoredError: config.ContinueOnIgnoredError, |
|
||||||
ContextKey: config.ContextKey, |
|
||||||
SigningKey: config.SigningKey, |
|
||||||
SigningKeys: config.SigningKeys, |
|
||||||
SigningMethod: config.SigningMethod, |
|
||||||
KeyFunc: config.KeyFunc, |
|
||||||
TokenLookup: config.TokenLookup, |
|
||||||
TokenLookupFuncs: config.TokenLookupFuncs, |
|
||||||
ParseTokenFunc: config.ParseTokenFunc, |
|
||||||
NewClaimsFunc: nil, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// JWT returns a JSON Web Token (JWT) auth middleware.
|
|
||||||
//
|
|
||||||
// For valid token, it sets the user in context and calls next handler.
|
|
||||||
// For invalid token, it returns "401 - Unauthorized" error.
|
|
||||||
// For missing token, it returns "400 - Bad Request" error.
|
|
||||||
//
|
|
||||||
// See: https://jwt.io/introduction
|
|
||||||
// See `JWTConfig.TokenLookup`
|
|
||||||
// See https://github.com/labstack/echo-jwt
|
|
||||||
func JWT(signingKey any) echo.MiddlewareFunc { |
|
||||||
return echojwt.JWT(signingKey) |
|
||||||
} |
|
@ -0,0 +1,104 @@ |
|||||||
|
package middleware |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/rsa" |
||||||
|
"github.com/golang-jwt/jwt/v5" |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"slices" |
||||||
|
"sorbet/pkg/env" |
||||||
|
"sorbet/pkg/ticket" |
||||||
|
) |
||||||
|
|
||||||
|
var ticketPublicKey *rsa.PublicKey |
||||||
|
|
||||||
|
type TicketConfig struct { |
||||||
|
Skipper Skipper |
||||||
|
Anonymously bool |
||||||
|
Audiences []string |
||||||
|
PublicKey *rsa.PublicKey |
||||||
|
TicketFinder ticket.Finder |
||||||
|
ClaimsLooker func(c echo.Context, claims *ticket.Claims) error |
||||||
|
ErrorHandler func(c echo.Context, err error) error |
||||||
|
SuccessHandler func(c echo.Context) |
||||||
|
} |
||||||
|
|
||||||
|
func Ticket(anonymously bool, roles ...string) echo.MiddlewareFunc { |
||||||
|
return TicketWithConfig(TicketConfig{ |
||||||
|
Anonymously: anonymously, |
||||||
|
TicketFinder: ticket.DefaultFinder, |
||||||
|
ClaimsLooker: func(c echo.Context, claims *ticket.Claims) error { |
||||||
|
if len(roles) > 0 && slices.Contains(roles, claims.Role) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return ticket.ErrUnauthorized |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TicketWithConfig(config TicketConfig) echo.MiddlewareFunc { |
||||||
|
return config.ToMiddleware() |
||||||
|
} |
||||||
|
|
||||||
|
func (t *TicketConfig) ToMiddleware() echo.MiddlewareFunc { |
||||||
|
if t.Skipper == nil { |
||||||
|
t.Skipper = DefaultSkipper |
||||||
|
} |
||||||
|
if len(t.Audiences) == 0 { |
||||||
|
t.Audiences = append(t.Audiences, "*") |
||||||
|
} |
||||||
|
if t.TicketFinder == nil { |
||||||
|
t.TicketFinder = ticket.DefaultFinder |
||||||
|
} |
||||||
|
if t.ErrorHandler == nil { |
||||||
|
t.ErrorHandler = func(c echo.Context, err error) error { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if t.ClaimsLooker == nil { |
||||||
|
t.ClaimsLooker = func(c echo.Context, claims *ticket.Claims) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc { |
||||||
|
return func(c echo.Context) error { |
||||||
|
if t.Skipper(c) { |
||||||
|
return next(c) |
||||||
|
} |
||||||
|
ticketString := t.TicketFinder(c.Request()) |
||||||
|
if ticketString == "" { |
||||||
|
if t.Anonymously { |
||||||
|
return next(c) |
||||||
|
} |
||||||
|
return t.ErrorHandler(c, ticket.ErrNoTicketFound) |
||||||
|
} |
||||||
|
publicKey := t.PublicKey |
||||||
|
if publicKey == nil { |
||||||
|
key, err := getTicketPublicKey() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
publicKey = key |
||||||
|
} |
||||||
|
claims, err := ticket.Verify(ticketString, publicKey, t.Audiences...) |
||||||
|
if err != nil { |
||||||
|
return t.ErrorHandler(c, err) |
||||||
|
} |
||||||
|
if err = t.ClaimsLooker(c, claims); err != nil { |
||||||
|
return t.ErrorHandler(c, err) |
||||||
|
} |
||||||
|
c.Set("ticket", ticketString) |
||||||
|
c.Set("ticket_claims", claims) |
||||||
|
return next(c) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getTicketPublicKey() (*rsa.PublicKey, error) { |
||||||
|
if ticketPublicKey != nil { |
||||||
|
return ticketPublicKey, nil |
||||||
|
} |
||||||
|
var err error |
||||||
|
source := []byte(env.String("TICKET_PUBLIC_KEY")) |
||||||
|
ticketPublicKey, err = jwt.ParseRSAPublicKeyFromPEM(source) |
||||||
|
return ticketPublicKey, err |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/company/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyController struct { |
||||||
|
util.Controller[entities.Company, request.CompanyUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *CompanyController) InitRoutes(r *echo.Group) { |
||||||
|
ctr.RegisterRoutes("/companies", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/company/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyDepartmentController struct { |
||||||
|
util.Controller[entities.CompanyDepartment, request.CompanyDepartmentUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyDepartmentController) InitRoutes(r *echo.Group) { |
||||||
|
c.RegisterRoutes("/company/departments", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/company/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyStaffController struct { |
||||||
|
util.Controller[entities.CompanyStaff, request.CompanyStaffUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyStaffController) InitRoutes(r *echo.Group) { |
||||||
|
c.RegisterRoutes("/company/staffs", r) |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/pkg/db" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyDepartmentUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PID uint `json:"pid" xml:"pid" form:"pid"` |
||||||
|
CompanyID uint `json:"company_id" xml:"company_id" form:"company_id"` |
||||||
|
PrincipalID uint `json:"principal_id" xml:"principal_id" form:"principal_id"` |
||||||
|
Name string `json:"name" xml:"name" form:"name"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyDepartmentUpsertRequest) GetID() any { |
||||||
|
return c.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyDepartmentUpsertRequest) ToEntity() any { |
||||||
|
return &entities.CompanyDepartment{ |
||||||
|
ID: c.ID, |
||||||
|
PID: &c.PID, |
||||||
|
CompanyID: c.CompanyID, |
||||||
|
PrincipalID: &c.PrincipalID, |
||||||
|
Name: c.Name, |
||||||
|
Sort: c.Sort, |
||||||
|
Version: db.Version{Int64: 1, Valid: true}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyDepartmentUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": c.ID, |
||||||
|
"pid": c.PID, |
||||||
|
"company_id": c.CompanyID, |
||||||
|
"principal_id": c.PrincipalID, |
||||||
|
"name": c.Name, |
||||||
|
"sort": c.Sort, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyStaffUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
CompanyID uint `json:"company_id" xml:"company_id" form:"company_id"` |
||||||
|
Name string `json:"name" xml:"name" form:"name"` |
||||||
|
Gender string `json:"gender" xml:"gender" form:"gender"` |
||||||
|
Position string `json:"position" xml:"position" form:"position"` |
||||||
|
PhoneNumber string `json:"phone_number" xml:"phone_number" form:"phone_number"` |
||||||
|
WechatOpenid string `json:"wechat_openid" xml:"wechat_openid" form:"wechat_openid"` |
||||||
|
WithoutStudy bool `json:"without_study" xml:"without_study" form:"without_study"` |
||||||
|
IsAdmin bool `json:"is_admin" xml:"is_admin" form:"is_admin"` |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyStaffUpsertRequest) GetID() any { |
||||||
|
return c.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyStaffUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": c.ID, |
||||||
|
"company_id": c.CompanyID, |
||||||
|
"name": c.Name, |
||||||
|
"gender": c.Gender, |
||||||
|
"position": c.Position, |
||||||
|
"phone_number": c.PhoneNumber, |
||||||
|
"wechat_openid": c.WechatOpenid, |
||||||
|
"without_study": c.WithoutStudy, |
||||||
|
"is_admin": c.IsAdmin, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *CompanyStaffUpsertRequest) ToEntity() any { |
||||||
|
return &entities.CompanyStaff{ |
||||||
|
ID: c.ID, |
||||||
|
CompanyID: c.CompanyID, |
||||||
|
Name: c.Name, |
||||||
|
Gender: c.Gender, |
||||||
|
Position: c.Position, |
||||||
|
PhoneNumber: c.PhoneNumber, |
||||||
|
WechatOpenid: c.WechatOpenid, |
||||||
|
WithoutStudy: c.WithoutStudy, |
||||||
|
IsAdmin: c.IsAdmin, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/pkg/db" |
||||||
|
) |
||||||
|
|
||||||
|
type CompanyUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PrincipalID uint `json:"principal_id" xml:"principal_id" form:"principal_id"` |
||||||
|
Name string `json:"name" xml:"name" form:"name"` |
||||||
|
Logo string `json:"logo" xml:"logo" form:"logo"` |
||||||
|
Status bool `json:"status" xml:"status" form:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
func (r *CompanyUpsertRequest) GetID() any { |
||||||
|
return r.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (r *CompanyUpsertRequest) ToEntity() *entities.Company { |
||||||
|
return &entities.Company{ |
||||||
|
ID: r.ID, |
||||||
|
PrincipalID: &r.PrincipalID, |
||||||
|
Name: r.Name, |
||||||
|
Logo: r.Logo, |
||||||
|
Status: r.Status, |
||||||
|
Version: db.Version{Int64: 1, Valid: true}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *CompanyUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": r.ID, |
||||||
|
"principal_id": r.PrincipalID, |
||||||
|
"name": r.Name, |
||||||
|
"logo": r.Logo, |
||||||
|
"status": r.Status, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package company |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/services/company/controller" |
||||||
|
"sorbet/pkg/app" |
||||||
|
) |
||||||
|
|
||||||
|
type Service struct{} |
||||||
|
|
||||||
|
func (s *Service) Init(ctx *app.Context) error { |
||||||
|
ctx.Routes(&controller.CompanyController{}) |
||||||
|
ctx.Routes(&controller.CompanyDepartmentController{}) |
||||||
|
ctx.Routes(&controller.CompanyStaffController{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Start() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Stop() error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/config/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type ConfigController struct { |
||||||
|
util.Controller[entities.Config, request.ConfigUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *ConfigController) InitRoutes(r *echo.Group) { |
||||||
|
ctr.RegisterRoutes("/config", r) |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/pkg/db" |
||||||
|
) |
||||||
|
|
||||||
|
type ConfigUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
GroupID uint `json:"group_id" xml:"group_id" form:"group_id"` |
||||||
|
Name string `json:"name" xml:"name" form:"name"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Description string `json:"description" xml:"description" form:"description"` |
||||||
|
DataType string `json:"data_type" xml:"data_type" form:"data_type"` |
||||||
|
Attributes map[string]any `json:"attributes" xml:"attributes" form:"attributes"` |
||||||
|
Value any `json:"value" xml:"value" form:"value"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ConfigUpsertRequest) GetID() any { |
||||||
|
return c.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ConfigUpsertRequest) ToEntity() *entities.Config { |
||||||
|
return &entities.Config{ |
||||||
|
ID: c.ID, |
||||||
|
GroupID: c.GroupID, |
||||||
|
Name: c.Name, |
||||||
|
Title: c.Title, |
||||||
|
Description: c.Description, |
||||||
|
DataType: c.DataType, |
||||||
|
Attributes: c.Attributes, |
||||||
|
Value: c.Value, |
||||||
|
Sort: c.Sort, |
||||||
|
Version: db.Version{Int64: 1, Valid: true}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *ConfigUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"group_id": c.GroupID, |
||||||
|
"name": c.Name, |
||||||
|
"title": c.Title, |
||||||
|
"description": c.Description, |
||||||
|
"data_type": c.DataType, |
||||||
|
"attributes": c.Attributes, |
||||||
|
"value": c.Value, |
||||||
|
"sort": c.Sort, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/feature/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureCategoryController struct { |
||||||
|
util.Controller[entities.FeatureCategory, request.FeatureCategoryUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureCategoryController) InitRoutes(r *echo.Group) { |
||||||
|
f.RegisterRoutes("/feature/categories", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/feature/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureConfigController struct { |
||||||
|
util.Controller[entities.FeatureConfig, request.FeatureConfigUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureConfigController) InitRoutes(r *echo.Group) { |
||||||
|
f.RegisterRoutes("/feature/configs", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/feature/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureContentChapterController struct { |
||||||
|
util.Controller[entities.FeatureContentChapter, request.FeatureContentChapterUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentChapterController) InitRoutes(r *echo.Group) { |
||||||
|
f.RegisterRoutes("/feature/content/chapters", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/feature/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureContentController struct { |
||||||
|
util.Controller[entities.FeatureContent, request.FeatureContentUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentController) InitRoutes(r *echo.Group) { |
||||||
|
f.RegisterRoutes("/feature/contents", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/feature/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureContentDetailController struct { |
||||||
|
util.Controller[entities.FeatureContentDetail, request.FeatureContentDetailUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentDetailController) InitRoutes(r *echo.Group) { |
||||||
|
f.RegisterRoutes("/feature/content/detail", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/feature/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureController struct { |
||||||
|
util.Controller[entities.Feature, request.FeatureUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureController) InitRoutes(r *echo.Group) { |
||||||
|
f.RegisterRoutes("/features", r) |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureCategoryUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PID uint `json:"pid" xml:"pid" form:"pid"` |
||||||
|
FeatureID uint `json:"feature_id" xml:"feature_id" form:"feature_id"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Description string `json:"description" xml:"description" form:"description"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
Status bool `json:"status" xml:"status" form:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureCategoryUpsertRequest) GetID() any { |
||||||
|
return f.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureCategoryUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": f.ID, |
||||||
|
"pid": f.PID, |
||||||
|
"feature_id": f.FeatureID, |
||||||
|
"title": f.Title, |
||||||
|
"description": f.Description, |
||||||
|
"sort": f.Sort, |
||||||
|
"status": f.Status, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureCategoryUpsertRequest) ToEntity() any { |
||||||
|
return &entities.FeatureCategory{ |
||||||
|
ID: f.ID, |
||||||
|
PID: &f.PID, |
||||||
|
FeatureID: f.FeatureID, |
||||||
|
Title: f.Title, |
||||||
|
Description: f.Description, |
||||||
|
Sort: f.Sort, |
||||||
|
Status: f.Status, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureConfigUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
FeatureID uint `json:"feature_id" xml:"feature_id" form:"feature_id"` |
||||||
|
Status bool `json:"status" xml:"status" form:"status"` |
||||||
|
Categorizable bool `json:"categorizable" xml:"categorizable" form:"categorizable"` |
||||||
|
CategoryDepth int64 `json:"category_depth" xml:"category_depth" form:"category_depth"` |
||||||
|
ContentTypes []string `json:"content_types" xml:"content_types" form:"content_types"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureConfigUpsertRequest) GetID() any { |
||||||
|
return f.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureConfigUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": f.ID, |
||||||
|
"feature_id": f.FeatureID, |
||||||
|
"status": f.Status, |
||||||
|
"categorizable": f.Categorizable, |
||||||
|
"category_depth": f.CategoryDepth, |
||||||
|
"content_types": f.ContentTypes, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureConfigUpsertRequest) ToEntity() any { |
||||||
|
return &entities.FeatureConfig{ |
||||||
|
ID: f.ID, |
||||||
|
FeatureID: f.FeatureID, |
||||||
|
Status: f.Status, |
||||||
|
Categorizable: f.Categorizable, |
||||||
|
CategoryDepth: f.CategoryDepth, |
||||||
|
ContentTypes: f.ContentTypes, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureContentChapterUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PID uint `json:"pid" xml:"pid" form:"pid"` |
||||||
|
FeatureID uint `json:"feature_id" xml:"feature_id" form:"feature_id"` |
||||||
|
ContentID uint `json:"content_id" xml:"content_id" form:"content_id"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Intro string `json:"intro" xml:"intro" form:"intro"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentChapterUpsertRequest) GetID() any { |
||||||
|
return f.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentChapterUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": f.ID, |
||||||
|
"pid": f.PID, |
||||||
|
"feature_id": f.FeatureID, |
||||||
|
"content_id": f.ContentID, |
||||||
|
"title": f.Title, |
||||||
|
"intro": f.Intro, |
||||||
|
"sort": f.Sort, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentChapterUpsertRequest) ToEntity() any { |
||||||
|
return &entities.FeatureContentChapter{ |
||||||
|
ID: f.ID, |
||||||
|
PID: &f.PID, |
||||||
|
FeatureID: f.FeatureID, |
||||||
|
ContentID: f.ContentID, |
||||||
|
Title: f.Title, |
||||||
|
Intro: f.Intro, |
||||||
|
Sort: f.Sort, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureContentDetailUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
FeatureID uint `json:"feature_id" xml:"feature_id" form:"feature_id"` |
||||||
|
ChapterID uint `json:"chapter_id" xml:"chapter_id" form:"chapter_id"` |
||||||
|
ContentID uint `json:"content_id" xml:"content_id" form:"content_id"` |
||||||
|
Type string `json:"type" xml:"type" form:"type"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Intro string `json:"intro" xml:"intro" form:"intro"` |
||||||
|
PosterUrl string `json:"poster_url" xml:"poster_url" form:"poster_url"` |
||||||
|
VideoUrl string `json:"video_url" xml:"video_url" form:"video_url"` |
||||||
|
Text string `json:"text" xml:"text" form:"text"` |
||||||
|
Attributes map[string]any `json:"attributes" xml:"attributes" form:"attributes"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentDetailUpsertRequest) GetID() any { |
||||||
|
return f.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentDetailUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": f.ID, |
||||||
|
"feature_id": f.FeatureID, |
||||||
|
"chapter_id": f.ChapterID, |
||||||
|
"content_id": f.ContentID, |
||||||
|
"type": f.Type, |
||||||
|
"title": f.Title, |
||||||
|
"intro": f.Intro, |
||||||
|
"poster_url": f.PosterUrl, |
||||||
|
"video_url": f.VideoUrl, |
||||||
|
"text": f.Text, |
||||||
|
"attributes": f.Attributes, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentDetailUpsertRequest) ToEntity() any { |
||||||
|
return &entities.FeatureContentDetail{ |
||||||
|
ID: f.ID, |
||||||
|
FeatureID: f.FeatureID, |
||||||
|
ChapterID: &f.ChapterID, |
||||||
|
ContentID: f.ContentID, |
||||||
|
Type: f.Type, |
||||||
|
Title: f.Title, |
||||||
|
Intro: f.Intro, |
||||||
|
PosterUrl: f.PosterUrl, |
||||||
|
VideoUrl: f.VideoUrl, |
||||||
|
Text: f.Text, |
||||||
|
Attributes: f.Attributes, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureContentUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
FeatureID uint `json:"feature_id" xml:"feature_id" form:"feature_id"` |
||||||
|
CategoryID *uint `json:"category_id" xml:"category_id" form:"category_id"` |
||||||
|
Type string `json:"type" xml:"type" form:"type"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Intro string `json:"intro" xml:"intro" form:"intro"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentUpsertRequest) GetID() any { |
||||||
|
return f.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": f.ID, |
||||||
|
"feature_id": f.FeatureID, |
||||||
|
"category_id": f.CategoryID, |
||||||
|
"type": f.Type, |
||||||
|
"title": f.Title, |
||||||
|
"intro": f.Intro, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureContentUpsertRequest) ToEntity() any { |
||||||
|
return &entities.FeatureContent{ |
||||||
|
ID: f.ID, |
||||||
|
FeatureID: f.FeatureID, |
||||||
|
CategoryID: f.CategoryID, |
||||||
|
Type: f.Type, |
||||||
|
Title: f.Title, |
||||||
|
Intro: f.Intro, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type FeatureUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Intro string `json:"intro" xml:"intro" form:"intro"` |
||||||
|
Icon string `json:"icon" xml:"icon" form:"icon"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureUpsertRequest) GetID() any { |
||||||
|
return f.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": f.ID, |
||||||
|
"title": f.Title, |
||||||
|
"intro": f.Intro, |
||||||
|
"icon": f.Icon, |
||||||
|
"sort": f.Sort, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (f *FeatureUpsertRequest) ToEntity() any { |
||||||
|
return &entities.Feature{ |
||||||
|
ID: f.ID, |
||||||
|
Title: f.Title, |
||||||
|
Intro: f.Intro, |
||||||
|
Icon: f.Icon, |
||||||
|
Sort: f.Sort, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
package feature |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/services/feature/controller" |
||||||
|
"sorbet/pkg/app" |
||||||
|
) |
||||||
|
|
||||||
|
type Service struct{} |
||||||
|
|
||||||
|
func (s *Service) Init(ctx *app.Context) error { |
||||||
|
ctx.Routes(&controller.FeatureCategoryController{}) |
||||||
|
ctx.Routes(&controller.FeatureConfigController{}) |
||||||
|
ctx.Routes(&controller.FeatureContentChapterController{}) |
||||||
|
ctx.Routes(&controller.FeatureContentDetailController{}) |
||||||
|
ctx.Routes(&controller.FeatureController{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Start() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Stop() error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/resource/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type ResourceCategoryController struct { |
||||||
|
util.Controller[entities.Resource, request.ResourceCategoryUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *ResourceCategoryController) InitRoutes(r *echo.Group) { |
||||||
|
ctr.RegisterRoutes("/resource/categories", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/resource/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type ResourceController struct { |
||||||
|
util.Controller[entities.Resource, request.ResourceUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *ResourceController) InitRoutes(r *echo.Group) { |
||||||
|
ctr.RegisterRoutes("/resources", r) |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type ResourceCategoryUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PID uint `json:"pid" xml:"pid" form:"pid"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
Status bool `json:"status" xml:"status" form:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
func (r *ResourceCategoryUpsertRequest) GetID() any { |
||||||
|
return r.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (r *ResourceCategoryUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": r.ID, |
||||||
|
"pid": r.PID, |
||||||
|
"title": r.Title, |
||||||
|
"sort": r.Sort, |
||||||
|
"status": r.Status, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *ResourceCategoryUpsertRequest) ToEntity() any { |
||||||
|
return &entities.ResourceCategory{ |
||||||
|
ID: r.ID, |
||||||
|
PID: &r.PID, |
||||||
|
Title: r.Title, |
||||||
|
Sort: r.Sort, |
||||||
|
Status: r.Status, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type ResourceUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
CategoryID uint `json:"category_id" xml:"category_id" form:"category_id"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Path string `json:"path" xml:"path" form:"path"` |
||||||
|
Width int32 `json:"width" xml:"width" form:"width"` |
||||||
|
Height int32 `json:"height" xml:"height" form:"height"` |
||||||
|
Duration int32 `json:"duration" xml:"duration" form:"duration"` |
||||||
|
MimeType string `json:"mime_type" xml:"mime_type" form:"mime_type"` |
||||||
|
Extension string `json:"extension" xml:"extension" form:"extension"` |
||||||
|
Size int64 `json:"size" xml:"size" form:"size"` |
||||||
|
} |
||||||
|
|
||||||
|
func (r *ResourceUpsertRequest) GetID() any { |
||||||
|
return r.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (r *ResourceUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": r.ID, |
||||||
|
"category_id": r.CategoryID, |
||||||
|
"title": r.Title, |
||||||
|
"path": r.Path, |
||||||
|
"width": r.Width, |
||||||
|
"height": r.Height, |
||||||
|
"duration": r.Duration, |
||||||
|
"mime_type": r.MimeType, |
||||||
|
"extension": r.Extension, |
||||||
|
"size": r.Size, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *ResourceUpsertRequest) ToEntity() any { |
||||||
|
return &entities.Resource{ |
||||||
|
ID: r.ID, |
||||||
|
CategoryID: r.CategoryID, |
||||||
|
Title: r.Title, |
||||||
|
Path: r.Path, |
||||||
|
Width: r.Width, |
||||||
|
Height: r.Height, |
||||||
|
Duration: r.Duration, |
||||||
|
MimeType: r.MimeType, |
||||||
|
Extension: r.Extension, |
||||||
|
Size: r.Size, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
package resource |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/services/resource/controller" |
||||||
|
"sorbet/pkg/app" |
||||||
|
) |
||||||
|
|
||||||
|
type Service struct{} |
||||||
|
|
||||||
|
func (s *Service) Init(ctx *app.Context) error { |
||||||
|
ctx.Routes(&controller.ResourceController{}) |
||||||
|
ctx.Routes(&controller.ResourceCategoryController{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Start() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Stop() error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
package services |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/pkg/app" |
||||||
|
) |
||||||
|
|
||||||
|
type Service struct { |
||||||
|
inners []Service |
||||||
|
} |
||||||
|
|
||||||
|
func (s Service) Init(ctx *app.Context) error { |
||||||
|
//TODO implement me
|
||||||
|
panic("implement me") |
||||||
|
} |
||||||
|
|
||||||
|
func (s Service) Start() error { |
||||||
|
//TODO implement me
|
||||||
|
panic("implement me") |
||||||
|
} |
||||||
|
|
||||||
|
func (s Service) Stop() error { |
||||||
|
//TODO implement me
|
||||||
|
panic("implement me") |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/system/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemLogController struct { |
||||||
|
util.Controller[entities.SystemLog, request.SystemLogUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemLogController) InitRoutes(r *echo.Group) { |
||||||
|
s.RegisterRoutes("/system/logs", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/system/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemMenuController struct { |
||||||
|
util.Controller[entities.SystemMenu, request.SystemMenuUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemMenuController) InitRoutes(r *echo.Group) { |
||||||
|
s.RegisterRoutes("/system/menus", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/system/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemPermissionController struct { |
||||||
|
util.Controller[entities.SystemPermission, request.SystemPermissionUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemPermissionController) InitRoutes(r *echo.Group) { |
||||||
|
s.RegisterRoutes("/system/permissions", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/system/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemRoleController struct { |
||||||
|
util.Controller[entities.SystemRole, request.SystemRoleUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRoleController) InitRoutes(r *echo.Group) { |
||||||
|
s.RegisterRoutes("/system/roles", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/system/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemRolePowerController struct { |
||||||
|
util.Controller[entities.SystemRolePower, request.SystemRolePowerUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRolePowerController) InitRoutes(r *echo.Group) { |
||||||
|
s.RegisterRoutes("/system/role/powers", r) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package controller |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/internal/entities" |
||||||
|
"sorbet/internal/services/system/request" |
||||||
|
"sorbet/internal/util" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemUserController struct { |
||||||
|
util.Controller[entities.SystemUser, request.SystemUserUpsertRequest] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemUserController) InitRoutes(r *echo.Group) { |
||||||
|
s.RegisterRoutes("/system/users", r) |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemLogUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
Table string `json:"table" xml:"table" form:"table"` |
||||||
|
RowID uint `json:"row_id" xml:"row_id" form:"row_id"` |
||||||
|
Operation string `json:"operation" xml:"operation" form:"operation"` |
||||||
|
IP string `json:"ip" xml:"ip" form:"ip"` |
||||||
|
Comment string `json:"comment" xml:"comment" form:"comment"` |
||||||
|
RequestID string `json:"request_id" xml:"request_id" form:"request_id"` |
||||||
|
RequestInfo string `json:"request_info" xml:"request_info" form:"request_info"` |
||||||
|
ColumnInfo string `json:"column_info" xml:"column_info" form:"column_info"` |
||||||
|
UserID int64 `json:"user_id" xml:"user_id" form:"user_id"` |
||||||
|
UserType int64 `json:"user_type" xml:"user_type" form:"user_type"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemLogUpsertRequest) GetID() any { |
||||||
|
return s.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemLogUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": s.ID, |
||||||
|
"table": s.Table, |
||||||
|
"row_id": s.RowID, |
||||||
|
"operation": s.Operation, |
||||||
|
"ip": s.IP, |
||||||
|
"comment": s.Comment, |
||||||
|
"request_id": s.RequestID, |
||||||
|
"request_info": s.RequestInfo, |
||||||
|
"column_info": s.ColumnInfo, |
||||||
|
"user_id": s.UserID, |
||||||
|
"user_type": s.UserType, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemLogUpsertRequest) ToEntity() any { |
||||||
|
return &entities.SystemLog{ |
||||||
|
ID: s.ID, |
||||||
|
Table: s.Table, |
||||||
|
RowID: s.RowID, |
||||||
|
Operation: s.Operation, |
||||||
|
IP: s.IP, |
||||||
|
Comment: s.Comment, |
||||||
|
RequestID: s.RequestID, |
||||||
|
RequestInfo: s.RequestInfo, |
||||||
|
ColumnInfo: s.ColumnInfo, |
||||||
|
UserID: s.UserID, |
||||||
|
UserType: s.UserType, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemMenuUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PID uint `json:"pid" xml:"pid" form:"pid"` |
||||||
|
Title string `json:"title" xml:"title" form:"title"` |
||||||
|
Icon string `json:"icon" xml:"icon" form:"icon"` |
||||||
|
Sort int32 `json:"sort" xml:"sort" form:"sort"` |
||||||
|
Path string `json:"path" xml:"path" form:"path"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemMenuUpsertRequest) GetID() any { |
||||||
|
return s.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemMenuUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": s.ID, |
||||||
|
"pid": s.PID, |
||||||
|
"title": s.Title, |
||||||
|
"icon": s.Icon, |
||||||
|
"sort": s.Sort, |
||||||
|
"path": s.Path, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemMenuUpsertRequest) ToEntity() any { |
||||||
|
return &entities.SystemMenu{ |
||||||
|
ID: s.ID, |
||||||
|
PID: &s.PID, |
||||||
|
Title: s.Title, |
||||||
|
Icon: s.Icon, |
||||||
|
Sort: s.Sort, |
||||||
|
Path: s.Path, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemPermissionUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
PID uint `json:"pid" xml:"pid" form:"pid"` |
||||||
|
Name string `json:"name" xml:"name" form:"name"` |
||||||
|
Type string `json:"type" xml:"type" form:"type"` |
||||||
|
Identifier string `json:"identifier" xml:"identifier" form:"identifier"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemPermissionUpsertRequest) GetID() any { |
||||||
|
return s.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemPermissionUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": s.ID, |
||||||
|
"pid": s.PID, |
||||||
|
"name": s.Name, |
||||||
|
"type": s.Type, |
||||||
|
"identifier": s.Identifier, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemPermissionUpsertRequest) ToEntity() any { |
||||||
|
return &entities.SystemPermission{ |
||||||
|
ID: s.ID, |
||||||
|
PID: &s.PID, |
||||||
|
Name: s.Name, |
||||||
|
Type: s.Type, |
||||||
|
Identifier: s.Identifier, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemRolePowerUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
RoleID uint `json:"role_id" xml:"role_id" form:"role_id"` |
||||||
|
WithType string `json:"with_type" xml:"with_type" form:"with_type"` |
||||||
|
WithID uint `json:"with_id" xml:"with_id" form:"with_id"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRolePowerUpsertRequest) GetID() any { |
||||||
|
return s.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRolePowerUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": s.ID, |
||||||
|
"role_id": s.RoleID, |
||||||
|
"with_type": s.WithType, |
||||||
|
"with_id": s.WithID, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRolePowerUpsertRequest) ToEntity() any { |
||||||
|
return &entities.SystemRolePower{ |
||||||
|
ID: s.ID, |
||||||
|
RoleID: s.RoleID, |
||||||
|
WithType: s.WithType, |
||||||
|
WithID: s.WithID, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemRoleUpsertRequest struct { |
||||||
|
ID uint `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
Name string `json:"name" xml:"name" form:"name"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRoleUpsertRequest) GetID() any { |
||||||
|
return s.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRoleUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": s.ID, |
||||||
|
"name": s.Name, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemRoleUpsertRequest) ToEntity() any { |
||||||
|
return &entities.SystemRole{ |
||||||
|
ID: s.ID, |
||||||
|
Name: s.Name, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package request |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/entities" |
||||||
|
) |
||||||
|
|
||||||
|
type SystemUserUpsertRequest struct { |
||||||
|
ID int64 `json:"id" xml:"id" form:"id" path:"id"` |
||||||
|
Username string `json:"username" xml:"username" form:"username"` |
||||||
|
Password string `json:"password" xml:"password" form:"password"` |
||||||
|
Status bool `json:"status" xml:"status" form:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemUserUpsertRequest) GetID() any { |
||||||
|
return s.ID |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemUserUpsertRequest) ToMap() map[string]any { |
||||||
|
return map[string]any{ |
||||||
|
"id": s.ID, |
||||||
|
"username": s.Username, |
||||||
|
"password": s.Password, |
||||||
|
"status": s.Status, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *SystemUserUpsertRequest) ToEntity() any { |
||||||
|
return &entities.SystemUser{ |
||||||
|
ID: s.ID, |
||||||
|
Username: s.Username, |
||||||
|
Password: s.Password, |
||||||
|
Status: s.Status, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package system |
||||||
|
|
||||||
|
import ( |
||||||
|
"sorbet/internal/services/system/controller" |
||||||
|
"sorbet/pkg/app" |
||||||
|
) |
||||||
|
|
||||||
|
type Service struct{} |
||||||
|
|
||||||
|
func (s *Service) Init(ctx *app.Context) error { |
||||||
|
ctx.Routes(&controller.SystemLogController{}) |
||||||
|
ctx.Routes(&controller.SystemMenuController{}) |
||||||
|
ctx.Routes(&controller.SystemPermissionController{}) |
||||||
|
ctx.Routes(&controller.SystemRoleController{}) |
||||||
|
ctx.Routes(&controller.SystemRolePowerController{}) |
||||||
|
ctx.Routes(&controller.SystemUserController{}) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Start() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) Stop() error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,312 @@ |
|||||||
|
package util |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"gorm.io/gorm" |
||||||
|
"net/url" |
||||||
|
"reflect" |
||||||
|
"sorbet/pkg/db" |
||||||
|
"sorbet/pkg/ioc" |
||||||
|
"sorbet/pkg/rsp" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
type GetID interface { |
||||||
|
GetID() any |
||||||
|
} |
||||||
|
|
||||||
|
type ToMap interface { |
||||||
|
ToMap() map[string]any |
||||||
|
} |
||||||
|
|
||||||
|
type ToEntity interface { |
||||||
|
ToEntity() any |
||||||
|
} |
||||||
|
|
||||||
|
type ControllerRequest interface { |
||||||
|
GetID |
||||||
|
ToMap |
||||||
|
ToEntity |
||||||
|
} |
||||||
|
|
||||||
|
func ParseQuery[T any](query url.Values, qb *db.QueryBuilder[T]) (page, limit int, err error) { |
||||||
|
var paginating bool |
||||||
|
for key, values := range query { |
||||||
|
switch key { |
||||||
|
case "sortby": |
||||||
|
for _, s := range values { |
||||||
|
if s[0] == '+' { |
||||||
|
qb.AscentBy(s[1:]) |
||||||
|
} else if s[0] == '-' { |
||||||
|
qb.DescentBy(s[1:]) |
||||||
|
} else { |
||||||
|
qb.AscentBy(s) |
||||||
|
} |
||||||
|
} |
||||||
|
case "limit", "page": |
||||||
|
var v int |
||||||
|
if values[0] != "" { |
||||||
|
v, err = strconv.Atoi(values[0]) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
if v <= 0 { |
||||||
|
return 0, 0, rsp.ErrInternal |
||||||
|
} |
||||||
|
if key == "limit" { |
||||||
|
qb.Limit(v) |
||||||
|
limit = v |
||||||
|
} else { |
||||||
|
paginating = true |
||||||
|
page = max(v, 1) |
||||||
|
} |
||||||
|
default: |
||||||
|
v := values[0] |
||||||
|
i := strings.IndexByte(key, '#') |
||||||
|
if i == -1 { |
||||||
|
qb.Eq(key, v) |
||||||
|
continue |
||||||
|
} |
||||||
|
switch k, op := key[:i], key[i+1:]; op { |
||||||
|
case "=": |
||||||
|
qb.Eq(k, v) |
||||||
|
case "!=": |
||||||
|
qb.Neq(k, v) |
||||||
|
case "<": |
||||||
|
qb.Lt(k, v) |
||||||
|
case "<=": |
||||||
|
qb.Lte(k, v) |
||||||
|
case ">": |
||||||
|
qb.Gt(k, v) |
||||||
|
case ">=": |
||||||
|
qb.Gte(k, v) |
||||||
|
case "<>", "><": |
||||||
|
var less, more any |
||||||
|
switch len(values) { |
||||||
|
case 2: |
||||||
|
less, more = values[0], values[1] |
||||||
|
case 1: |
||||||
|
vs := strings.Split(v, ",") |
||||||
|
if len(vs) != 2 || vs[0] == "" || vs[1] == "" { |
||||||
|
return 0, 0, rsp.ErrBadParams |
||||||
|
} |
||||||
|
less, more = vs[0], vs[1] |
||||||
|
} |
||||||
|
if op == "<>" { |
||||||
|
qb.Between(k, less, more) |
||||||
|
} else { |
||||||
|
qb.NotBetween(k, key, more) |
||||||
|
} |
||||||
|
case "nil": |
||||||
|
qb.IsNull(k) |
||||||
|
case "!nil": |
||||||
|
qb.NotNull(k) |
||||||
|
case "~": |
||||||
|
qb.Like(k, v) |
||||||
|
case "!~": |
||||||
|
qb.NotLike(k, v) |
||||||
|
case "in", "!in": |
||||||
|
if len(values) == 1 { |
||||||
|
values = strings.Split(v, ",") |
||||||
|
} |
||||||
|
vs := make([]any, len(values)) |
||||||
|
for i, value := range values { |
||||||
|
vs[i] = value |
||||||
|
} |
||||||
|
if op == "in" { |
||||||
|
qb.In(k, vs...) |
||||||
|
} else { |
||||||
|
qb.NotIn(k, vs...) |
||||||
|
} |
||||||
|
default: |
||||||
|
qb.Eq(key, v) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if paginating { |
||||||
|
return |
||||||
|
} |
||||||
|
if limit == 0 { |
||||||
|
limit = 30 |
||||||
|
qb.Limit(limit) |
||||||
|
} |
||||||
|
qb.Offset((page - 1) * limit) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func getID(req any) (val any) { |
||||||
|
defer func() { |
||||||
|
if recover() != nil { |
||||||
|
val = nil |
||||||
|
} |
||||||
|
}() |
||||||
|
val = req.(GetID).GetID() |
||||||
|
rv := reflect.ValueOf(val) |
||||||
|
if rv.IsZero() || rv.IsNil() { |
||||||
|
val = nil |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func getValues(req any) map[string]any { |
||||||
|
if v, ok := req.(ToMap); ok { |
||||||
|
return v.ToMap() |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func getEntity[T any](request any) *T { |
||||||
|
v, ok := request.(ToEntity) |
||||||
|
if !ok { |
||||||
|
return nil |
||||||
|
} |
||||||
|
ent, ok := v.ToEntity().(*T) |
||||||
|
if ok { |
||||||
|
return ent |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Controller 控制器基类
|
||||||
|
//
|
||||||
|
// 泛型 [Entity] 表示操作的具体数据;
|
||||||
|
// 泛型 [Upsert] 表示创建或更新时需要的数据。
|
||||||
|
type Controller[Entity any, Upsert any] struct{} |
||||||
|
|
||||||
|
func (ctr *Controller[Entity, Upsert]) RegisterRoutes(path string, r *echo.Group) { |
||||||
|
r.PUT(path, ctr.Create) |
||||||
|
r.DELETE(path+"/:id", ctr.Delete) |
||||||
|
r.POST(path+"/:id", ctr.Update) |
||||||
|
r.GET(path+"/:id", ctr.Get) |
||||||
|
r.GET(path, ctr.List) |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *Controller[Entity, Upsert]) ORM(c echo.Context) (*gorm.DB, error) { |
||||||
|
orm, err := ioc.Get[gorm.DB]() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return orm.WithContext(c.Request().Context()), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *Controller[Entity, Upsert]) Repository() (*db.Repository[Entity], error) { |
||||||
|
orm, err := ioc.Get[gorm.DB]() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return db.NewRepository[Entity](orm), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Create 创建数据
|
||||||
|
func (ctr *Controller[Entity, Upsert]) Create(c echo.Context) error { |
||||||
|
return ctr.upsert(c, true) |
||||||
|
} |
||||||
|
|
||||||
|
// Update 更新数据
|
||||||
|
func (ctr *Controller[Entity, Upsert]) Update(c echo.Context) error { |
||||||
|
return ctr.upsert(c, false) |
||||||
|
} |
||||||
|
|
||||||
|
func (ctr *Controller[Entity, Upsert]) upsert(c echo.Context, isCreate bool) error { |
||||||
|
request, err := Bind[Upsert](c) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
repo, err := ctr.Repository() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
id := getID(request) |
||||||
|
if isCreate != reflect.ValueOf(id).IsZero() { |
||||||
|
return rsp.BadParams(c, "参数错误") |
||||||
|
} |
||||||
|
// 更新数据
|
||||||
|
if !isCreate { |
||||||
|
values := getValues(request) |
||||||
|
if values == nil { |
||||||
|
return rsp.ErrInternal |
||||||
|
} |
||||||
|
err = repo.UpdateByID(c.Request().Context(), id, values) |
||||||
|
if err == nil { |
||||||
|
// TODO(hupeh): 返回更新后的实体数据
|
||||||
|
return rsp.Ok(c, nil) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
// 创建数据
|
||||||
|
group := getEntity[Entity](request) |
||||||
|
if group == nil { |
||||||
|
return rsp.ErrInternal |
||||||
|
} |
||||||
|
err = repo.Create(c.Request().Context(), group) |
||||||
|
if err != nil { |
||||||
|
return rsp.Created(c, group) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Delete 通过ID删除数据
|
||||||
|
func (ctr *Controller[Entity, Upsert]) Delete(c echo.Context) error { |
||||||
|
id, err := BindId(c, true) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
repo, err := ctr.Repository() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
err = repo.DeleteByID(c.Request().Context(), id) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return rsp.Ok(c, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// Get 通过ID获取数据
|
||||||
|
func (ctr *Controller[Entity, Upsert]) Get(c echo.Context) error { |
||||||
|
id, err := BindId(c, true) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
repo, err := ctr.Repository() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
entity, err := repo.GetByID(c.Request().Context(), id) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return rsp.Ok(c, entity) |
||||||
|
} |
||||||
|
|
||||||
|
// List 获取数据列表
|
||||||
|
func (ctr *Controller[Entity, Upsert]) List(c echo.Context) error { |
||||||
|
repo, err := ctr.Repository() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
qb := repo.NewQueryBuilder(c.Request().Context()) |
||||||
|
_, _, err = ParseQuery[Entity](c.QueryParams(), qb) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// 不是分页查询
|
||||||
|
if !c.QueryParams().Has("page") { |
||||||
|
var result []*Entity |
||||||
|
err = qb.Find(&result) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return rsp.Ok(c, result) |
||||||
|
} |
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
pager, err := qb.Paginate() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return rsp.Ok(c, pager) |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
package util |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sorbet/pkg/rsp" |
||||||
|
) |
||||||
|
|
||||||
|
// RequestGuarder 参数守卫函数签名
|
||||||
|
type RequestGuarder[T any] func(c echo.Context, req *T) error |
||||||
|
|
||||||
|
// Bind 将提交的参数绑定到泛型 T 的实例上
|
||||||
|
func Bind[T any](c echo.Context, guards ...RequestGuarder[T]) (*T, error) { |
||||||
|
var req T |
||||||
|
if err := c.Bind(&req); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := c.Validate(&req); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
for _, guard := range guards { |
||||||
|
if err := guard(c, &req); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return &req, nil |
||||||
|
} |
||||||
|
|
||||||
|
func BindId(c echo.Context, must bool) (uint, error) { |
||||||
|
request, err := Bind[struct { |
||||||
|
ID uint `json:"id" xml:"id" path:"id"` |
||||||
|
}](c) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
if must { |
||||||
|
if request.ID <= 0 { |
||||||
|
return 0, rsp.ErrBadParams |
||||||
|
} |
||||||
|
} else if request.ID < 0 { |
||||||
|
return 0, rsp.ErrBadParams |
||||||
|
} |
||||||
|
return request.ID, err |
||||||
|
} |
@ -1,7 +0,0 @@ |
|||||||
package util |
|
||||||
|
|
||||||
import "github.com/labstack/echo/v4" |
|
||||||
|
|
||||||
type EchoContext struct { |
|
||||||
echo.Context |
|
||||||
} |
|
@ -1,88 +1,19 @@ |
|||||||
package main |
package main |
||||||
|
|
||||||
import ( |
import ( |
||||||
"github.com/labstack/echo/v4" |
"log" |
||||||
"github.com/swaggo/echo-swagger" |
|
||||||
"gorm.io/gorm" |
|
||||||
"net/http" |
|
||||||
_ "sorbet/docs" // 开发文档
|
|
||||||
"sorbet/internal" |
"sorbet/internal" |
||||||
"sorbet/internal/entities" |
|
||||||
"sorbet/internal/middleware" |
|
||||||
"sorbet/internal/repositories" |
|
||||||
"sorbet/internal/util" |
|
||||||
"sorbet/pkg/env" |
"sorbet/pkg/env" |
||||||
"sorbet/pkg/ioc" |
|
||||||
"sorbet/pkg/rsp" |
|
||||||
) |
) |
||||||
|
|
||||||
// @title 博客系统
|
|
||||||
// @version 1.0
|
|
||||||
// @description 基于 Echo 框架的基本库
|
|
||||||
//
|
|
||||||
// @contact.name API Support
|
|
||||||
// @contact.url http://www.swagger.io/support
|
|
||||||
// @contact.email support@swagger.io
|
|
||||||
//
|
|
||||||
// @license.name Apache 2.0
|
|
||||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
|
||||||
//
|
|
||||||
// @accept json
|
|
||||||
func main() { |
func main() { |
||||||
if err := env.Init(); err != nil { |
if err := env.Init(); err != nil { |
||||||
panic(err) |
panic(err) |
||||||
} |
} |
||||||
|
|
||||||
if err := internal.Init(); err != nil { |
if err := internal.Init(); err != nil { |
||||||
panic(err) |
panic(err) |
||||||
} |
} |
||||||
|
if err := internal.Start(); err != nil { |
||||||
repositories.Init() |
log.Panicln(err) |
||||||
|
|
||||||
e := echo.New() |
|
||||||
e.HideBanner = true |
|
||||||
e.HidePort = true |
|
||||||
e.HTTPErrorHandler = func(err error, c echo.Context) { |
|
||||||
if !c.Response().Committed { |
|
||||||
http.Error(c.Response(), err.Error(), 500) |
|
||||||
} |
|
||||||
} |
|
||||||
e.Logger = util.NewLogger() |
|
||||||
e.Use(middleware.Recover()) |
|
||||||
e.Use(middleware.CORS()) |
|
||||||
e.Use(middleware.Logger) |
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { |
|
||||||
return func(c echo.Context) error { |
|
||||||
db := ioc.MustGet[gorm.DB]().WithContext(c.Request().Context()) |
|
||||||
ci := ioc.Fork() |
|
||||||
ci.Bind(db) |
|
||||||
c.Set("db", db) |
|
||||||
c.Set("ioc", ci) |
|
||||||
return next(c) |
|
||||||
} |
|
||||||
}) |
|
||||||
e.GET("/swagger/*", echoSwagger.WrapHandler) |
|
||||||
e.GET("/", func(c echo.Context) error { |
|
||||||
repo := repositories.NewCompanyRepository(c.Get("db").(*gorm.DB)) |
|
||||||
//err := c.Get("ioc").(*ioc.Container).Resolve(&repo)
|
|
||||||
//if err != nil {
|
|
||||||
// return err
|
|
||||||
//}
|
|
||||||
//db := ioc.MustGet[gorm.DB]().WithContext(c.Request().Context())
|
|
||||||
//ioc.Fork().Bind(db)
|
|
||||||
//repo := ioc.MustGet[repositories.CompanyRepository]()
|
|
||||||
repo.Create(c.Request().Context(), &entities.Company{Name: "海苔一诺"}) |
|
||||||
pager, err := repo.Paginate(c.Request().Context()) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return rsp.Ok(c, pager) |
|
||||||
}) |
|
||||||
e.Logger.Fatal(e.Start(":1323")) |
|
||||||
} |
|
||||||
|
|
||||||
func panicIf(e error) { |
|
||||||
if e != nil { |
|
||||||
panic(e) |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -1,67 +0,0 @@ |
|||||||
package logs |
|
||||||
|
|
||||||
import ( |
|
||||||
"log/slog" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
type Attr = slog.Attr |
|
||||||
|
|
||||||
// String returns an Attr for a string value.
|
|
||||||
func String(key, value string) Attr { |
|
||||||
return slog.String(key, value) |
|
||||||
} |
|
||||||
|
|
||||||
// Int64 returns an Attr for an int64.
|
|
||||||
func Int64(key string, value int64) Attr { |
|
||||||
return slog.Int64(key, value) |
|
||||||
} |
|
||||||
|
|
||||||
// Int converts an int to an int64 and returns
|
|
||||||
// an Attr with that value.
|
|
||||||
func Int(key string, value int) Attr { |
|
||||||
return slog.Int(key, value) |
|
||||||
} |
|
||||||
|
|
||||||
// Uint64 returns an Attr for a uint64.
|
|
||||||
func Uint64(key string, v uint64) Attr { |
|
||||||
return slog.Uint64(key, v) |
|
||||||
} |
|
||||||
|
|
||||||
// Float64 returns an Attr for a floating-point number.
|
|
||||||
func Float64(key string, v float64) Attr { |
|
||||||
return slog.Float64(key, v) |
|
||||||
} |
|
||||||
|
|
||||||
// Bool returns an Attr for a bool.
|
|
||||||
func Bool(key string, v bool) Attr { |
|
||||||
return slog.Bool(key, v) |
|
||||||
} |
|
||||||
|
|
||||||
// Time returns an Attr for a time.Time.
|
|
||||||
// It discards the monotonic portion.
|
|
||||||
func Time(key string, v time.Time) Attr { |
|
||||||
return slog.Time(key, v) |
|
||||||
} |
|
||||||
|
|
||||||
// Duration returns an Attr for a time.Duration.
|
|
||||||
func Duration(key string, v time.Duration) Attr { |
|
||||||
return slog.Duration(key, v) |
|
||||||
} |
|
||||||
|
|
||||||
// Group returns an Attr for a Group Instance.
|
|
||||||
// The first argument is the key; the remaining arguments
|
|
||||||
// are converted to Attrs as in [Logger.Log].
|
|
||||||
//
|
|
||||||
// Use Group to collect several key-value pairs under a single
|
|
||||||
// key on a log line, or as the result of LogValue
|
|
||||||
// in order to log a single value as multiple Attrs.
|
|
||||||
func Group(key string, args ...any) Attr { |
|
||||||
return slog.Group(key, args...) |
|
||||||
} |
|
||||||
|
|
||||||
// Any returns an Attr for the supplied value.
|
|
||||||
// See [AnyValue] for how values are treated.
|
|
||||||
func Any(key string, value any) Attr { |
|
||||||
return slog.Any(key, value) |
|
||||||
} |
|
@ -1,44 +0,0 @@ |
|||||||
package logs |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/mattn/go-isatty" |
|
||||||
"os" |
|
||||||
) |
|
||||||
|
|
||||||
type Color string |
|
||||||
|
|
||||||
var ( |
|
||||||
//fgBlack Color = "\x1b[30m"
|
|
||||||
//fgWhiteItalic Color = "\x1b[37;3m"
|
|
||||||
|
|
||||||
FgRed Color = "\x1b[31m" |
|
||||||
FgGreen Color = "\x1b[32m" |
|
||||||
FgYellow Color = "\x1b[33m" |
|
||||||
FgBlue Color = "\x1b[34m" |
|
||||||
FgMagenta Color = "\x1b[35m" |
|
||||||
FgCyan Color = "\x1b[36m" |
|
||||||
FgWhite Color = "\x1b[37m" |
|
||||||
FgHiBlack Color = "\x1b[90m" |
|
||||||
fgGreenItalic Color = "\x1b[32;3m" |
|
||||||
|
|
||||||
// NoColor defines if the output is colorized or not. It's dynamically set to
|
|
||||||
// false or true based on the stdout's file descriptor referring to a terminal
|
|
||||||
// or not. It's also set to true if the NO_COLOR environment variable is
|
|
||||||
// set (regardless of its value). This is a global option and affects all
|
|
||||||
// colors. For more control over each Color block use the methods
|
|
||||||
// DisableColor() individually.
|
|
||||||
noColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || |
|
||||||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) |
|
||||||
) |
|
||||||
|
|
||||||
// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string.
|
|
||||||
func noColorIsSet() bool { |
|
||||||
return os.Getenv("NO_COLOR") != "" |
|
||||||
} |
|
||||||
|
|
||||||
func (c Color) Wrap(msg string) string { |
|
||||||
if noColorIsSet() || noColor { |
|
||||||
return msg |
|
||||||
} |
|
||||||
return string(c) + msg + "\x1b[0m" |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
package logs |
|
@ -1,59 +0,0 @@ |
|||||||
package logs |
|
||||||
|
|
||||||
import ( |
|
||||||
"log/slog" |
|
||||||
) |
|
||||||
|
|
||||||
type Level int |
|
||||||
|
|
||||||
const ( |
|
||||||
LevelTrace Level = iota |
|
||||||
LevelDebug // 用于程序调试
|
|
||||||
LevelInfo // 用于程序运行
|
|
||||||
LevelWarn // 潜在错误或非预期结果
|
|
||||||
LevelError // 发生错误,但不影响系统的继续运行
|
|
||||||
LevelFatal |
|
||||||
LevelSilent |
|
||||||
) |
|
||||||
|
|
||||||
// 越界取近值
|
|
||||||
func (l Level) real() Level { |
|
||||||
return min(LevelSilent, max(l, LevelTrace)) |
|
||||||
} |
|
||||||
|
|
||||||
// Level 实现 slog.Leveler 接口
|
|
||||||
func (l Level) Level() slog.Level { |
|
||||||
return slog.Level(16 - int(LevelSilent-l.real())*4) |
|
||||||
} |
|
||||||
|
|
||||||
func (l Level) slog() slog.Leveler { |
|
||||||
return l.Level() |
|
||||||
} |
|
||||||
|
|
||||||
func (l Level) String() string { |
|
||||||
switch l { |
|
||||||
case LevelTrace: |
|
||||||
return "TRACE" |
|
||||||
case LevelDebug: |
|
||||||
return "DEBUG" |
|
||||||
case LevelInfo: |
|
||||||
return "INFO" |
|
||||||
case LevelWarn: |
|
||||||
return "WARN" |
|
||||||
case LevelError: |
|
||||||
return "ERROR" |
|
||||||
case LevelFatal: |
|
||||||
return "FATAL" |
|
||||||
case LevelSilent: |
|
||||||
return "OFF" |
|
||||||
} |
|
||||||
if l < LevelTrace { |
|
||||||
return "TRACE" |
|
||||||
} else { |
|
||||||
return "OFF" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func parseSlogLevel(level slog.Level) Level { |
|
||||||
return Level(level/4 + 2).real() |
|
||||||
} |
|
@ -1,38 +0,0 @@ |
|||||||
package logs |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
var std = New(&Options{Level: LevelInfo}) |
|
||||||
|
|
||||||
func Default() Logger { return std } |
|
||||||
|
|
||||||
func SetFlags(flags int) { Default().SetFlags(flags) } |
|
||||||
func Flags() int { return Default().Flags() } |
|
||||||
func SetTimezone(loc *time.Location) { Default().SetTimezone(loc) } |
|
||||||
func Timezone() *time.Location { return Default().Timezone() } |
|
||||||
func SetLevel(level Level) { Default().SetLevel(level) } |
|
||||||
func GetLevel() Level { return Default().Level() } |
|
||||||
func SetPersistWriter(w io.Writer) { Default().SetPersistWriter(w) } |
|
||||||
func SetWriter(w io.Writer) { Default().SetWriter(w) } |
|
||||||
func With(args ...Attr) Logger { return Default().With(args...) } |
|
||||||
func WithGroup(name string) Logger { return Default().WithGroup(name) } |
|
||||||
func Enabled(level Level) bool { return Default().Enabled(level) } |
|
||||||
func Log(level Level, msg string, args ...any) { Default().Log(level, msg, args...) } |
|
||||||
func ForkLevel(level Level, msg string, args ...any) ChildLogger { |
|
||||||
return Default().ForkLevel(level, msg, args...) |
|
||||||
} |
|
||||||
func Trace(msg string, args ...any) { Default().Trace(msg, args...) } |
|
||||||
func ForkTrace(msg string, args ...any) ChildLogger { return Default().ForkTrace(msg, args...) } |
|
||||||
func Debug(msg string, args ...any) { Default().Debug(msg, args...) } |
|
||||||
func ForkDebug(msg string, args ...any) ChildLogger { return Default().ForkDebug(msg, args...) } |
|
||||||
func Info(msg string, args ...any) { Default().Info(msg) } |
|
||||||
func ForkInfo(msg string, args ...any) ChildLogger { return Default().ForkInfo(msg, args...) } |
|
||||||
func Warn(msg string, args ...any) { Default().Warn(msg, args...) } |
|
||||||
func ForkWarn(msg string, args ...any) ChildLogger { return Default().ForkWarn(msg, args...) } |
|
||||||
func Error(msg string, args ...any) { Default().Error(msg, args...) } |
|
||||||
func ForkError(msg string, args ...any) ChildLogger { return Default().ForkError(msg, args...) } |
|
||||||
func Fatal(msg string, args ...any) { Default().Fatal(msg, args...) } |
|
||||||
func ForkFatal(msg string, args ...any) ChildLogger { return Default().ForkFatal(msg, args...) } |
|
@ -1,512 +0,0 @@ |
|||||||
package logs |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"encoding/json" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"log" |
|
||||||
"log/slog" |
|
||||||
"os" |
|
||||||
"runtime" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
"sync/atomic" |
|
||||||
"time" |
|
||||||
"unsafe" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
// 使用东八区时间
|
|
||||||
// https://cloud.tencent.com/developer/article/1805859
|
|
||||||
cstZone = time.FixedZone("CST", 8*3600) |
|
||||||
childLoggerKey = "sorbet/log:ChildLogger" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
Ldate = 1 << iota |
|
||||||
Ltime |
|
||||||
Lmicroseconds |
|
||||||
Llongfile |
|
||||||
Lshortfile |
|
||||||
Lfields |
|
||||||
Lcolor |
|
||||||
LstdFlags = Ltime | Lmicroseconds | Lfields | Lcolor |
|
||||||
) |
|
||||||
|
|
||||||
type Logger interface { |
|
||||||
SetFlags(flags int) |
|
||||||
Flags() int |
|
||||||
SetTimezone(loc *time.Location) |
|
||||||
Timezone() *time.Location |
|
||||||
SetLevel(level Level) |
|
||||||
Level() Level |
|
||||||
SetPersistWriter(w io.Writer) |
|
||||||
SetWriter(w io.Writer) |
|
||||||
With(args ...Attr) Logger |
|
||||||
WithGroup(name string) Logger |
|
||||||
Enabled(level Level) bool |
|
||||||
Log(level Level, msg string, args ...any) |
|
||||||
ForkLevel(level Level, msg string, args ...any) ChildLogger |
|
||||||
Trace(msg string, args ...any) |
|
||||||
ForkTrace(msg string, args ...any) ChildLogger |
|
||||||
Debug(msg string, args ...any) |
|
||||||
ForkDebug(msg string, args ...any) ChildLogger |
|
||||||
Info(msg string, args ...any) |
|
||||||
ForkInfo(msg string, args ...any) ChildLogger |
|
||||||
Warn(msg string, args ...any) |
|
||||||
ForkWarn(msg string, args ...any) ChildLogger |
|
||||||
Error(msg string, args ...any) |
|
||||||
ForkError(msg string, args ...any) ChildLogger |
|
||||||
Fatal(msg string, args ...any) |
|
||||||
ForkFatal(msg string, args ...any) ChildLogger |
|
||||||
} |
|
||||||
|
|
||||||
type ChildLogger interface { |
|
||||||
Print(msg string, args ...any) |
|
||||||
Finish() |
|
||||||
} |
|
||||||
|
|
||||||
type Options struct { |
|
||||||
Flags int |
|
||||||
Level Level |
|
||||||
Timezone *time.Location |
|
||||||
PersistWriter io.Writer |
|
||||||
Writer io.Writer |
|
||||||
} |
|
||||||
|
|
||||||
type logger struct { |
|
||||||
parent *logger |
|
||||||
isChild int32 |
|
||||||
indent int32 |
|
||||||
|
|
||||||
level int32 |
|
||||||
flags int32 |
|
||||||
timezone unsafe.Pointer |
|
||||||
|
|
||||||
outMu *sync.Mutex |
|
||||||
isPersistDiscard int32 |
|
||||||
isDiscard int32 |
|
||||||
persistWriter io.Writer |
|
||||||
writer io.Writer |
|
||||||
|
|
||||||
handler slog.Handler |
|
||||||
l *log.Logger |
|
||||||
} |
|
||||||
|
|
||||||
func New(opts *Options) Logger { |
|
||||||
if opts.Flags == 0 { |
|
||||||
opts.Flags = LstdFlags |
|
||||||
} |
|
||||||
if opts.Timezone == nil { |
|
||||||
opts.Timezone = cstZone |
|
||||||
} |
|
||||||
if opts.PersistWriter == nil { |
|
||||||
opts.PersistWriter = io.Discard |
|
||||||
} |
|
||||||
if opts.Writer == nil { |
|
||||||
opts.Writer = os.Stderr |
|
||||||
} |
|
||||||
|
|
||||||
var l *logger |
|
||||||
l = &logger{ |
|
||||||
outMu: &sync.Mutex{}, |
|
||||||
persistWriter: opts.PersistWriter, |
|
||||||
writer: opts.Writer, |
|
||||||
l: log.New(opts.Writer, "", 0), |
|
||||||
} |
|
||||||
|
|
||||||
l.SetLevel(opts.Level) |
|
||||||
l.SetFlags(opts.Flags) |
|
||||||
l.SetTimezone(opts.Timezone) |
|
||||||
l.SetPersistWriter(opts.PersistWriter) |
|
||||||
l.SetWriter(opts.Writer) |
|
||||||
|
|
||||||
l.handler = slog.NewJSONHandler(opts.PersistWriter, &slog.HandlerOptions{ |
|
||||||
AddSource: true, |
|
||||||
Level: opts.Level, |
|
||||||
ReplaceAttr: l.onAttr, |
|
||||||
}) |
|
||||||
|
|
||||||
return l |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) SetFlags(flags int) { |
|
||||||
atomic.StoreInt32(&l.flags, int32(flags)) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) Flags() int { |
|
||||||
return int(atomic.LoadInt32(&l.flags)) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) SetTimezone(loc *time.Location) { |
|
||||||
// FIXME(hupeh): 如何原子化储存结构体实例
|
|
||||||
atomic.StorePointer(&l.timezone, unsafe.Pointer(loc)) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) Timezone() *time.Location { |
|
||||||
return (*time.Location)(atomic.LoadPointer(&l.timezone)) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) SetLevel(level Level) { |
|
||||||
atomic.StoreInt32(&l.level, int32(level)) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) Level() Level { |
|
||||||
return Level(int(atomic.LoadInt32(&l.level))) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) SetPersistWriter(w io.Writer) { |
|
||||||
l.outMu.Lock() |
|
||||||
defer l.outMu.Unlock() |
|
||||||
l.persistWriter = w |
|
||||||
atomic.StoreInt32(&l.isPersistDiscard, discard(w)) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) SetWriter(w io.Writer) { |
|
||||||
l.outMu.Lock() |
|
||||||
defer l.outMu.Unlock() |
|
||||||
l.writer = w |
|
||||||
atomic.StoreInt32(&l.isDiscard, discard(w)) |
|
||||||
} |
|
||||||
|
|
||||||
func discard(w io.Writer) int32 { |
|
||||||
if w == io.Discard { |
|
||||||
return 1 |
|
||||||
} |
|
||||||
return 0 |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) onAttr(_ []string, a slog.Attr) slog.Attr { |
|
||||||
switch a.Key { |
|
||||||
case slog.LevelKey: |
|
||||||
level := a.Value.Any().(slog.Level) |
|
||||||
levelLabel := parseSlogLevel(level).String() |
|
||||||
a.Value = slog.StringValue(levelLabel) |
|
||||||
case slog.TimeKey: |
|
||||||
t := a.Value.Any().(time.Time) |
|
||||||
a.Value = slog.TimeValue(t.In(l.Timezone())) |
|
||||||
case slog.SourceKey: |
|
||||||
s := a.Value.Any().(*slog.Source) |
|
||||||
var as []Attr |
|
||||||
if s.Function != "" { |
|
||||||
as = append(as, String("func", s.Function)) |
|
||||||
} |
|
||||||
if s.File != "" { |
|
||||||
as = append(as, String("file", s.File)) |
|
||||||
} |
|
||||||
if s.Line != 0 { |
|
||||||
as = append(as, Int("line", s.Line)) |
|
||||||
} |
|
||||||
a.Value = slog.GroupValue(as...) |
|
||||||
} |
|
||||||
return a |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) Handle(ctx context.Context, r slog.Record) error { |
|
||||||
if atomic.LoadInt32(&l.isDiscard) == 0 { |
|
||||||
child, ok := ctx.Value(childLoggerKey).(*childLogger) |
|
||||||
indent, err := l.println(child, r) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if ok && indent > 0 { |
|
||||||
atomic.StoreInt32(&child.indent, indent) |
|
||||||
} |
|
||||||
} |
|
||||||
if atomic.LoadInt32(&l.isPersistDiscard) == 0 { |
|
||||||
return l.handler.Handle(ctx, r) |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) println(child *childLogger, r slog.Record) (int32, error) { |
|
||||||
var output string |
|
||||||
var sep string |
|
||||||
var indent int32 |
|
||||||
|
|
||||||
write := func(s string) { |
|
||||||
if sep == "" { |
|
||||||
sep = " " |
|
||||||
} else { |
|
||||||
output += sep |
|
||||||
} |
|
||||||
output += s |
|
||||||
} |
|
||||||
|
|
||||||
flags := l.Flags() |
|
||||||
colorful := flags&Lcolor != 0 |
|
||||||
msg := r.Message |
|
||||||
level := parseSlogLevel(r.Level) |
|
||||||
levelStr := level.String() |
|
||||||
withChild := child != nil |
|
||||||
|
|
||||||
if withChild { |
|
||||||
indent = atomic.LoadInt32(&child.indent) |
|
||||||
withChild = indent > 0 |
|
||||||
} |
|
||||||
|
|
||||||
if withChild { |
|
||||||
write(strings.Repeat(" ", int(indent))) |
|
||||||
indent = 0 |
|
||||||
} else { |
|
||||||
if flags&(Ldate|Ltime|Lmicroseconds) != 0 { |
|
||||||
t := r.Time.In(l.Timezone()) |
|
||||||
if flags&Ldate != 0 { |
|
||||||
write(t.Format("2006/01/02")) |
|
||||||
} |
|
||||||
if flags&(Ltime|Lmicroseconds) != 0 { |
|
||||||
if flags&Lmicroseconds != 0 { |
|
||||||
write(t.Format("15:04:05.000")) |
|
||||||
} else { |
|
||||||
write(t.Format("15:04:05")) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 保存缩进
|
|
||||||
indent += int32(len(output) + len(levelStr) + 3) |
|
||||||
|
|
||||||
if colorful { |
|
||||||
switch level { |
|
||||||
case LevelDebug: |
|
||||||
levelStr = FgCyan.Wrap(levelStr) |
|
||||||
msg = FgCyan.Wrap(msg) |
|
||||||
case LevelInfo: |
|
||||||
levelStr = FgBlue.Wrap(levelStr) + " " |
|
||||||
msg = FgBlue.Wrap(msg) |
|
||||||
indent += 1 |
|
||||||
case LevelWarn: |
|
||||||
levelStr = FgYellow.Wrap(levelStr) + " " |
|
||||||
msg = FgYellow.Wrap(msg) |
|
||||||
indent += 1 |
|
||||||
case LevelError: |
|
||||||
levelStr = FgRed.Wrap(levelStr) |
|
||||||
msg = FgRed.Wrap(msg) |
|
||||||
case LevelFatal: |
|
||||||
levelStr = FgMagenta.Wrap(levelStr) |
|
||||||
msg = FgMagenta.Wrap(msg) |
|
||||||
} |
|
||||||
levelStr = FgHiBlack.Wrap("[") + levelStr + FgHiBlack.Wrap("]") |
|
||||||
} else { |
|
||||||
levelStr = "[" + r.Level.String() + "]" |
|
||||||
} |
|
||||||
|
|
||||||
write(levelStr) |
|
||||||
} |
|
||||||
|
|
||||||
write(msg) |
|
||||||
|
|
||||||
if flags&(Lshortfile|Llongfile) != 0 && r.PC > 0 { |
|
||||||
var fileStr string |
|
||||||
fs := runtime.CallersFrames([]uintptr{r.PC}) |
|
||||||
f, _ := fs.Next() |
|
||||||
file := f.File |
|
||||||
if flags&Lshortfile != 0 { |
|
||||||
i := strings.LastIndexAny(file, "\\/") |
|
||||||
if i > -1 { |
|
||||||
file = file[i+1:] |
|
||||||
} |
|
||||||
} |
|
||||||
fileStr = fmt.Sprintf("%s:%s:%d", f.Function, file, f.Line) |
|
||||||
if colorful { |
|
||||||
fileStr = fgGreenItalic.Wrap(fileStr) |
|
||||||
} |
|
||||||
write(fileStr) |
|
||||||
} |
|
||||||
|
|
||||||
if numAttrs := r.NumAttrs(); flags&Lfields != 0 && numAttrs > 0 { |
|
||||||
fields := make(map[string]any, numAttrs) |
|
||||||
r.Attrs(func(a Attr) bool { |
|
||||||
fields[a.Key] = a.Value.Any() |
|
||||||
return true |
|
||||||
}) |
|
||||||
b, err := json.Marshal(fields) |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
fieldsStr := string(bytes.TrimSpace(b)) |
|
||||||
if fieldsStr != "" { |
|
||||||
if colorful { |
|
||||||
fieldsStr = FgHiBlack.Wrap(fieldsStr) |
|
||||||
} |
|
||||||
write(fieldsStr) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
l.l.Println(output) |
|
||||||
|
|
||||||
return indent, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) clone() *logger { |
|
||||||
c := *l |
|
||||||
return &c |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) With(args ...Attr) Logger { |
|
||||||
if len(args) == 0 { |
|
||||||
return l |
|
||||||
} |
|
||||||
c := l.clone() |
|
||||||
c.handler = c.handler.WithAttrs(args) |
|
||||||
return c |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) WithGroup(name string) Logger { |
|
||||||
if name == "" { |
|
||||||
return l |
|
||||||
} |
|
||||||
c := l.clone() |
|
||||||
c.handler = c.handler.WithGroup(name) |
|
||||||
return c |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) Enabled(level Level) bool { |
|
||||||
return l.handler.Enabled(nil, level.slog().Level()) |
|
||||||
} |
|
||||||
|
|
||||||
// Log logs at level.
|
|
||||||
func (l *logger) Log(level Level, msg string, args ...any) { |
|
||||||
l.log(nil, level, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkLevel(level Level, msg string, args ...any) ChildLogger { |
|
||||||
c := &childLogger{ |
|
||||||
parent: l, |
|
||||||
level: level, |
|
||||||
indent: 0, |
|
||||||
records: make([]slog.Record, 0), |
|
||||||
closed: make(chan struct{}), |
|
||||||
} |
|
||||||
c.Print(msg, args...) |
|
||||||
return c |
|
||||||
} |
|
||||||
|
|
||||||
// Trace logs at LevelTrace.
|
|
||||||
func (l *logger) Trace(msg string, args ...any) { |
|
||||||
l.log(nil, LevelTrace, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkTrace(msg string, args ...any) ChildLogger { |
|
||||||
return l.ForkLevel(LevelTrace, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
// Debug logs at LevelDebug.
|
|
||||||
func (l *logger) Debug(msg string, args ...any) { |
|
||||||
l.log(nil, LevelDebug, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkDebug(msg string, args ...any) ChildLogger { |
|
||||||
return l.ForkLevel(LevelDebug, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
// Info logs at LevelInfo.
|
|
||||||
func (l *logger) Info(msg string, args ...any) { |
|
||||||
l.log(nil, LevelInfo, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkInfo(msg string, args ...any) ChildLogger { |
|
||||||
return l.ForkLevel(LevelInfo, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
// Warn logs at LevelWarn.
|
|
||||||
func (l *logger) Warn(msg string, args ...any) { |
|
||||||
l.log(nil, LevelWarn, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkWarn(msg string, args ...any) ChildLogger { |
|
||||||
return l.ForkLevel(LevelWarn, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
// Error logs at LevelError.
|
|
||||||
func (l *logger) Error(msg string, args ...any) { |
|
||||||
l.log(nil, LevelError, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkError(msg string, args ...any) ChildLogger { |
|
||||||
return l.ForkLevel(LevelError, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
// Fatal logs at LevelFatal.
|
|
||||||
func (l *logger) Fatal(msg string, args ...any) { |
|
||||||
l.log(nil, LevelFatal, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) ForkFatal(msg string, args ...any) ChildLogger { |
|
||||||
return l.ForkLevel(LevelFatal, msg, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (l *logger) log(ctx context.Context, level Level, msg string, args ...any) { |
|
||||||
if !l.Enabled(level) { |
|
||||||
return |
|
||||||
} |
|
||||||
if ctx == nil { |
|
||||||
ctx = context.Background() |
|
||||||
} |
|
||||||
_ = l.Handle(ctx, newRecord(level, msg, args)) |
|
||||||
} |
|
||||||
|
|
||||||
func newRecord(level Level, msg string, args []any) slog.Record { |
|
||||||
//var pc uintptr
|
|
||||||
//if !internal.IgnorePC {
|
|
||||||
// var pcs [1]uintptr
|
|
||||||
// // skip [runtime.Callers, this function, this function's caller]
|
|
||||||
// runtime.Callers(3, pcs[:])
|
|
||||||
// pc = pcs[0]
|
|
||||||
//}
|
|
||||||
//r := slog.NewRecord(time.Now(), level.slog().Level(), msg, pc)
|
|
||||||
r := slog.NewRecord(time.Now(), level.slog().Level(), msg, 0) |
|
||||||
if len(args) > 0 { |
|
||||||
var sprintfArgs []any |
|
||||||
for _, arg := range args { |
|
||||||
switch v := arg.(type) { |
|
||||||
case Attr: |
|
||||||
r.AddAttrs(v) |
|
||||||
default: |
|
||||||
sprintfArgs = append(sprintfArgs, arg) |
|
||||||
} |
|
||||||
} |
|
||||||
if len(sprintfArgs) > 0 { |
|
||||||
r.Message = fmt.Sprintf(msg, sprintfArgs...) |
|
||||||
} |
|
||||||
} |
|
||||||
return r |
|
||||||
} |
|
||||||
|
|
||||||
type childLogger struct { |
|
||||||
parent *logger |
|
||||||
level Level |
|
||||||
indent int32 |
|
||||||
begin slog.Record |
|
||||||
finish slog.Record |
|
||||||
records []slog.Record |
|
||||||
closed chan struct{} |
|
||||||
} |
|
||||||
|
|
||||||
func (c *childLogger) Print(msg string, args ...any) { |
|
||||||
select { |
|
||||||
case <-c.closed: |
|
||||||
default: |
|
||||||
c.records = append( |
|
||||||
c.records, |
|
||||||
newRecord(c.level, msg, args), |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (c *childLogger) Finish() { |
|
||||||
select { |
|
||||||
case <-c.closed: |
|
||||||
return |
|
||||||
default: |
|
||||||
close(c.closed) |
|
||||||
} |
|
||||||
|
|
||||||
ctx := context.Background() |
|
||||||
ctx = context.WithValue(ctx, childLoggerKey, c) |
|
||||||
for _, record := range c.records { |
|
||||||
_ = c.parent.Handle(ctx, record) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,83 @@ |
|||||||
|
package rsp |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"net/http" |
||||||
|
) |
||||||
|
|
||||||
|
// GuardFunc 参数守卫函数前面
|
||||||
|
type GuardFunc[T any] func(c echo.Context, req *T) error |
||||||
|
|
||||||
|
// ServeFunc 参数处理函数签名
|
||||||
|
type ServeFunc[T any, R any] func(c echo.Context, req *T) (*R, error) |
||||||
|
|
||||||
|
// ServeWithDataFunc 参数处理函数签名,支持自定义数据
|
||||||
|
type ServeWithDataFunc[T any, R any, O any] func(c echo.Context, req *T, opt O) (*R, error) |
||||||
|
|
||||||
|
// RespondFunc 数据响应函数前面
|
||||||
|
type RespondFunc[R any] func(c echo.Context, res *R) error |
||||||
|
|
||||||
|
// Handle 通用 CRUD 函数构造器,具体参数与函数 HandleWithData 保持一致
|
||||||
|
func Handle[T any, R any](guard GuardFunc[T], serve ServeFunc[T, R], respond ...RespondFunc[R]) echo.HandlerFunc { |
||||||
|
return HandleWithData[T, R, any](guard, func(c echo.Context, req *T, opt any) (*R, error) { |
||||||
|
return serve(c, req) |
||||||
|
}, nil, respond...) |
||||||
|
} |
||||||
|
|
||||||
|
// HandleWithData 通用 CRUD 函数构造器,可以预置数据
|
||||||
|
//
|
||||||
|
// 参数 guard 可以为空值,表示无参数守卫;
|
||||||
|
// 参数 serve 必传;
|
||||||
|
// 参数 data 为自定义数据,该值最好不可被修改;
|
||||||
|
// 参数 respond 为自定义响应函数,若未指定,内部将使用 Ok 或 Created 来响应结果。
|
||||||
|
func HandleWithData[T any, R any, D any](guard GuardFunc[T], serve ServeWithDataFunc[T, R, D], data D, respond ...RespondFunc[R]) echo.HandlerFunc { |
||||||
|
if serve == nil { |
||||||
|
panic("miss ServeFunc") |
||||||
|
} |
||||||
|
return func(c echo.Context) error { |
||||||
|
var req T |
||||||
|
if err := c.Bind(&req); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := c.Validate(&req); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if guard != nil { |
||||||
|
err := guard(c, &req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
res, err := serve(c, &req, data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, send := range respond { |
||||||
|
if send != nil { |
||||||
|
return send(c, res) |
||||||
|
} |
||||||
|
} |
||||||
|
// 我们认为凡是 PUT 请求,都是创建数据
|
||||||
|
// 所以这里使用 Created 函数来响应数据。
|
||||||
|
if c.Request().Method == http.MethodPut { |
||||||
|
return Created(c, res) |
||||||
|
} |
||||||
|
return Ok(c, res) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Bind[T any](c echo.Context, guards ...GuardFunc[T]) (*T, error) { |
||||||
|
var req T |
||||||
|
if err := c.Bind(&req); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := c.Validate(&req); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
for _, guard := range guards { |
||||||
|
if err := guard(c, &req); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return &req, nil |
||||||
|
} |
Loading…
Reference in new issue