main
熊二 1 year ago
parent b3c82d7d6d
commit fa896b66b6
  1. 18
      .env.example
  2. 17
      README.md
  3. 1
      cmd/main.go
  4. 2
      internal/entities/company.go
  5. 2
      internal/entities/company_department.go
  6. 2
      internal/entities/company_staff.go
  7. 2
      internal/entities/config.go
  8. 4
      internal/entities/config_group.go
  9. 2
      internal/entities/feature.go
  10. 2
      internal/entities/feature_category.go
  11. 2
      internal/entities/feature_config.go
  12. 21
      internal/entities/feature_content.go
  13. 23
      internal/entities/feature_content_chapter.go
  14. 31
      internal/entities/feature_content_detail.go
  15. 2
      internal/entities/resource.go
  16. 2
      internal/entities/resource_category.go
  17. 2
      internal/entities/system_menu.go
  18. 2
      internal/entities/system_permission.go
  19. 2
      internal/entities/system_role.go
  20. 5
      internal/entities/system_user.go
  21. 64
      internal/init.go
  22. 140
      internal/middleware/jwt.go
  23. 104
      internal/middleware/ticket.go
  24. 5
      internal/repositories/company.go
  25. 5
      internal/repositories/company_department.go
  26. 5
      internal/repositories/company_staff.go
  27. 5
      internal/repositories/config.go
  28. 5
      internal/repositories/config_group.go
  29. 5
      internal/repositories/feature.go
  30. 5
      internal/repositories/feature_category.go
  31. 5
      internal/repositories/feature_config.go
  32. 5
      internal/repositories/feature_content.go
  33. 5
      internal/repositories/feature_content_chapter.go
  34. 5
      internal/repositories/feature_content_detail.go
  35. 5
      internal/repositories/resource.go
  36. 5
      internal/repositories/resource_category.go
  37. 5
      internal/repositories/system_log.go
  38. 5
      internal/repositories/system_menu.go
  39. 5
      internal/repositories/system_permission.go
  40. 5
      internal/repositories/system_role.go
  41. 5
      internal/repositories/system_role_power.go
  42. 5
      internal/repositories/system_user.go
  43. 16
      internal/services/company/controller/company_controller.go
  44. 16
      internal/services/company/controller/company_department_controller.go
  45. 16
      internal/services/company/controller/company_staff_controller.go
  46. 42
      internal/services/company/request/company_department_upsert_request.go
  47. 49
      internal/services/company/request/company_staff_upsert_request.go
  48. 39
      internal/services/company/request/company_upsert_request.go
  49. 23
      internal/services/company/service.go
  50. 16
      internal/services/config/controller/config_controller.go
  51. 50
      internal/services/config/request/config_upsert_request.go
  52. 16
      internal/services/feature/controller/feature_category_controller.go
  53. 16
      internal/services/feature/controller/feature_config_controller.go
  54. 16
      internal/services/feature/controller/feature_content_chapter_controller.go
  55. 16
      internal/services/feature/controller/feature_content_controller.go
  56. 16
      internal/services/feature/controller/feature_content_detail_controller.go
  57. 16
      internal/services/feature/controller/feature_controller.go
  58. 43
      internal/services/feature/request/feature_category_upsert_request.go
  59. 40
      internal/services/feature/request/feature_config_upsert_request.go
  60. 43
      internal/services/feature/request/feature_content_chapter_upsert_request.go
  61. 55
      internal/services/feature/request/feature_content_detail_upsert_request.go
  62. 40
      internal/services/feature/request/feature_content_upsert_request.go
  63. 37
      internal/services/feature/request/feature_upsert_request.go
  64. 25
      internal/services/feature/service.go
  65. 16
      internal/services/resource/controller/resource_category_controller.go
  66. 16
      internal/services/resource/controller/resource_controller.go
  67. 37
      internal/services/resource/request/resource_category_upsert_request.go
  68. 52
      internal/services/resource/request/resource_upsert_request.go
  69. 22
      internal/services/resource/service.go
  70. 24
      internal/services/service.go
  71. 16
      internal/services/system/controller/system_log_controller.go
  72. 16
      internal/services/system/controller/system_menu_controller.go
  73. 16
      internal/services/system/controller/system_permission_controller.go
  74. 16
      internal/services/system/controller/system_role_controller.go
  75. 16
      internal/services/system/controller/system_role_power_controller.go
  76. 16
      internal/services/system/controller/system_user_controller.go
  77. 55
      internal/services/system/request/system_log_upsert_request.go
  78. 40
      internal/services/system/request/system_menu_upsert_request.go
  79. 37
      internal/services/system/request/system_permission_upsert_request.go
  80. 34
      internal/services/system/request/system_role_power_upsert_request.go
  81. 28
      internal/services/system/request/system_role_upsert_request.go
  82. 34
      internal/services/system/request/system_user_upsert_request.go
  83. 26
      internal/services/system/service.go
  84. 312
      internal/util/controller.go
  85. 43
      internal/util/echo_bind.go
  86. 7
      internal/util/echo_context.go
  87. 75
      main.go
  88. 10
      pkg/db/query_builder.go
  89. 67
      pkg/logs/attr.go
  90. 44
      pkg/logs/color.go
  91. 1
      pkg/logs/handler.go
  92. 59
      pkg/logs/level.go
  93. 38
      pkg/logs/log.go
  94. 512
      pkg/logs/logger.go
  95. 4
      pkg/rsp/error.go
  96. 83
      pkg/rsp/handler.go
  97. 18
      pkg/rsp/respond_utils.go
  98. 0
      pkg/rsp/server_sent_events.go

@ -31,10 +31,22 @@ DB_CONN_MAX_LIFETIME=
DB_CODE_FIRST=
################
# 授权认证
################
JWT_PRIVATE_KEY=
JWT_PUBLIC_KEY=
# 在线获取公私钥网站 https://travistidwell.com/jsencrypt/demo/
# 或者使用 scripts/rs256/rs256.go 生成
# 授权令牌私钥
TICKET_PRIVATE_KEY=
# 授权令牌公钥
TICKET_PUBLIC_KEY=
# 令牌时长
TICKET_TTL=
# 令牌签发者
TICKET_ISSUER=
# 令牌主题
TICKET_SUBJECT=
# 令牌受众,多个值可以使用逗号分开,比如:app,pc,wap
TICKET_AUDIENCE=

@ -1,20 +1,3 @@
## 开发接口文档
> [swag 文档地址](https://github.com/swaggo/swag)
生成开发文档命令
```shell
swag init
```
注释文档格式化命令
```shell
swag fmt
```
## 关于 Api
参考 Restful 设计风格。

@ -0,0 +1 @@
package main

@ -16,7 +16,7 @@ type Company struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Principal *CompanyStaff `json:"principal" xml:"principal"`
Staffs []*CompanyStaff `json:"staffs" xml:"staffs"`

@ -19,7 +19,7 @@ type CompanyDepartment struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Staffs []*CompanyStaff `json:"staffs" xml:"staffs" gorm:"many2many:company_staff_to_department_relations"`
Courses []*FeatureContent `json:"courses" xml:"courses" gorm:"many2many:company_course_to_department_relations"`

@ -20,7 +20,7 @@ type CompanyStaff struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Departments []*CompanyDepartment `json:"departments" xml:"departments" gorm:"many2many:company_staff_to_department_relations"`
}

@ -20,5 +20,5 @@ type Config struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
}

@ -8,14 +8,14 @@ import (
// ConfigGroup 配置组表
type ConfigGroup struct {
ID int64 `json:"id" xml:"id" gorm:"primaryKey;not null;comment:配置组编号"`
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:配置组编号"`
Name string `json:"name" xml:"name" gorm:"size:25;not null;uniqueIndex;comment:配置组名称"`
Description string `json:"description" xml:"description" gorm:"comment:配置组描述"`
Sort int32 `json:"sort" xml:"sort" gorm:"default:0;comment:排序"`
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Configs []*Config `json:"configs" xml:"configs" gorm:"foreignKey:GroupID"`
}

@ -16,7 +16,7 @@ type Feature struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Config *FeatureConfig `json:"config" xml:"config"`
Categories []*FeatureCategory `json:"categories" xml:"categories"`

@ -18,7 +18,7 @@ type FeatureCategory struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Children []*FeatureCategory `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -17,5 +17,5 @@ type FeatureConfig struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
}

@ -2,21 +2,22 @@ package entities
import (
"gorm.io/gorm"
"gorm.io/plugin/optimisticlock"
"time"
)
// FeatureContent 栏目内容表(文章、视频、课程)
type FeatureContent struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:内容编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
CategoryID *uint `json:"category_id" xml:"category_id" gorm:"default:null;comment:所属分类编号"`
Type string `json:"type" xml:"type" gorm:"not null;comment:内容类型"`
Title string `json:"title" xml:"title" gorm:"size:100;not null;comment:内容标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:内容简介"`
Version int `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:内容编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
CategoryID *uint `json:"category_id" xml:"category_id" gorm:"default:null;comment:所属分类编号"`
Type string `json:"type" xml:"type" gorm:"not null;comment:内容类型"`
Title string `json:"title" xml:"title" gorm:"size:100;not null;comment:内容标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:内容简介"`
Version optimisticlock.Version `json:"-" xml:"-" 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:删除时间"`
Chapters []*FeatureContentChapter `json:"chapters" xml:"chapters" gorm:"foreignKey:ContentID"`
Details []*FeatureContentDetail `json:"details" xml:"details" gorm:"foreignKey:ContentID"`

@ -2,22 +2,23 @@ package entities
import (
"gorm.io/gorm"
"gorm.io/plugin/optimisticlock"
"time"
)
// FeatureContentChapter 栏目内容章回表
type FeatureContentChapter struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:章回编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级章回编号"`
FeatureID uint `json:"feature_id" xml:"feature_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:章回标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:章回描述"`
Sort int32 `json:"sort" xml:"sort" gorm:"size:4;default:0;comment:排序"`
Version int `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:章回编号"`
PID *uint `json:"pid" xml:"pid" gorm:"comment:上级章回编号"`
FeatureID uint `json:"feature_id" xml:"feature_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:章回标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:章回描述"`
Sort int32 `json:"sort" xml:"sort" gorm:"size:4;default:0;comment:排序"`
Version optimisticlock.Version `json:"-" xml:"-" 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:删除时间"`
Details []FeatureContentDetail `json:"details" xml:"details" gorm:"foreignKey:ChapterID"`
Children []*FeatureContentChapter `json:"children" xml:"children" gorm:"foreignKey:PID"`

@ -2,26 +2,27 @@ package entities
import (
"gorm.io/gorm"
"gorm.io/plugin/optimisticlock"
"time"
)
// FeatureContentDetail 内容详情表
type FeatureContentDetail struct {
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:内容详情编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
ChapterID *uint `json:"chapter_id" xml:"chapter_id" gorm:"comment:所属章回编号"`
ContentID uint `json:"content_id" xml:"content_id" gorm:"comment:所属内容编号"`
Type string `json:"type" xml:"type" gorm:"not null;comment:内容类型"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:简介"`
PosterUrl string `json:"poster_url" xml:"poster_url" gorm:"size:250;comment:封面链接"`
VideoUrl string `json:"video_url" xml:"video_url" gorm:"size:250;comment:视频描述"`
Text string `json:"text" xml:"text" gorm:"type:longtext;comment:具体内容"`
Attributes map[string]any `json:"attributes" xml:"attributes" gorm:"serializer:json;type:text;comment:相关属性值"`
Version int `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
ID uint `json:"id" xml:"id" gorm:"primaryKey;not null;comment:内容详情编号"`
FeatureID uint `json:"feature_id" xml:"feature_id" gorm:"comment:所属栏目编号"`
ChapterID *uint `json:"chapter_id" xml:"chapter_id" gorm:"comment:所属章回编号"`
ContentID uint `json:"content_id" xml:"content_id" gorm:"comment:所属内容编号"`
Type string `json:"type" xml:"type" gorm:"not null;comment:内容类型"`
Title string `json:"title" xml:"title" gorm:"size:25;not null;comment:标题"`
Intro string `json:"intro" xml:"intro" gorm:"size:250;comment:简介"`
PosterUrl string `json:"poster_url" xml:"poster_url" gorm:"size:250;comment:封面链接"`
VideoUrl string `json:"video_url" xml:"video_url" gorm:"size:250;comment:视频描述"`
Text string `json:"text" xml:"text" gorm:"type:longtext;comment:具体内容"`
Attributes map[string]any `json:"attributes" xml:"attributes" gorm:"serializer:json;type:text;comment:相关属性值"`
Version optimisticlock.Version `json:"-" xml:"-" 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:删除时间"`
Chapter *FeatureContentChapter `json:"chapter" xml:"chapter" gorm:"foreignKey:ChapterID"`
Content *FeatureContent `json:"content" xml:"content" gorm:"foreignKey:ContentID"`

@ -21,5 +21,5 @@ type Resource struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
}

@ -16,7 +16,7 @@ type ResourceCategory struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Resources []*Resource `json:"resources" xml:"resources" gorm:"foreignKey:CategoryID"`
Children []*ResourceCategory `json:"children" xml:"children" gorm:"foreignKey:PID"`

@ -17,7 +17,7 @@ type SystemMenu struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Children []*SystemMenu `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -13,7 +13,7 @@ type SystemPermission struct {
Identifier string `json:"identifier" xml:"identifier" gorm:"size:25;not null;uniqueIndex;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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Children []*SystemPermission `json:"children" xml:"children" gorm:"foreignKey:PID"`
}

@ -13,7 +13,7 @@ type SystemRole struct {
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Powers []*SystemRolePower `json:"powers" xml:"powers" gorm:"foreignKey:RoleID"`
Users []*SystemUser `json:"users" xml:"users" gorm:"many2many:system_user_to_role_relations"`

@ -2,6 +2,7 @@ package entities
import (
"gorm.io/gorm"
"sorbet/pkg/db"
"time"
)
@ -11,10 +12,10 @@ type SystemUser struct {
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:登录密码"`
Status bool `json:"status" xml:"status" gorm:"comment:状态"`
Version int `json:"-" xml:"-" gorm:"comment:乐观锁"`
Version db.Version `json:"-" xml:"-" 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:"delete_time" xml:"delete_time" gorm:"comment:删除时间"`
DeletedAt gorm.DeletedAt `json:"-" xml:"-" gorm:"comment:删除时间"`
Roles []*SystemRole `json:"roles" xml:"roles" gorm:"many2many:system_user_to_role_relations"`
}

@ -2,12 +2,20 @@ package internal
import (
"errors"
"github.com/labstack/echo/v4"
echoSwagger "github.com/swaggo/echo-swagger"
"gorm.io/gorm"
"net/http"
"sorbet/internal/entities"
"sorbet/internal/middleware"
"sorbet/internal/repositories"
"sorbet/internal/util"
"sorbet/pkg/db"
"sorbet/pkg/env"
"sorbet/pkg/ioc"
"sorbet/pkg/log"
"sorbet/pkg/rsp"
"sorbet/pkg/ticket"
)
func Init() error {
@ -51,3 +59,59 @@ func syncEntities() error {
&entities.SystemUser{},
)
}
func Start() error {
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))
company, err := repo.GetByID(c.Request().Context(), 1)
if err != nil {
return err
}
token, err := util.CreateTicket(&ticket.Claims{
UID: company.ID,
Role: "system",
Issuer: "chshs",
Subject: "subject",
})
if err != nil {
return err
}
return rsp.Ok(c, echo.Map{
"token": token,
})
})
e.Group("", middleware.Ticket(false, "system")).GET("/u", func(c echo.Context) error {
return rsp.Ok(c, echo.Map{
"ticket": c.Get("ticket"),
"claims": c.Get("ticket_claims").(*ticket.Claims),
})
})
e.Logger.Fatal(e.Start(":1323"))
return nil
}

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

@ -13,7 +13,6 @@ type CompanyRepository struct {
// NewCompanyRepository 创建公司仓库
func NewCompanyRepository(orm *gorm.DB) *CompanyRepository {
return &CompanyRepository{
db.NewRepositoryWith[entities.Company](orm, "id"),
}
db.NewRepositoryWith[entities.Company](orm, "id"),
}
}

@ -13,7 +13,6 @@ type CompanyDepartmentRepository struct {
// NewCompanyDepartmentRepository 创建公司部门仓库
func NewCompanyDepartmentRepository(orm *gorm.DB) *CompanyDepartmentRepository {
return &CompanyDepartmentRepository{
db.NewRepositoryWith[entities.CompanyDepartment](orm, "id"),
}
db.NewRepositoryWith[entities.CompanyDepartment](orm, "id"),
}
}

@ -13,7 +13,6 @@ type CompanyStaffRepository struct {
// NewCompanyStaffRepository 创建公司员工仓库
func NewCompanyStaffRepository(orm *gorm.DB) *CompanyStaffRepository {
return &CompanyStaffRepository{
db.NewRepositoryWith[entities.CompanyStaff](orm, "id"),
}
db.NewRepositoryWith[entities.CompanyStaff](orm, "id"),
}
}

@ -13,7 +13,6 @@ type ConfigRepository struct {
// NewConfigRepository 创建配置仓库
func NewConfigRepository(orm *gorm.DB) *ConfigRepository {
return &ConfigRepository{
db.NewRepositoryWith[entities.Config](orm, "id"),
}
db.NewRepositoryWith[entities.Config](orm, "id"),
}
}

@ -13,7 +13,6 @@ type ConfigGroupRepository struct {
// NewConfigGroupRepository 创建配置组仓库
func NewConfigGroupRepository(orm *gorm.DB) *ConfigGroupRepository {
return &ConfigGroupRepository{
db.NewRepositoryWith[entities.ConfigGroup](orm, "id"),
}
db.NewRepositoryWith[entities.ConfigGroup](orm, "id"),
}
}

@ -13,7 +13,6 @@ type FeatureRepository struct {
// NewFeatureRepository 创建栏目仓库
func NewFeatureRepository(orm *gorm.DB) *FeatureRepository {
return &FeatureRepository{
db.NewRepositoryWith[entities.Feature](orm, "id"),
}
db.NewRepositoryWith[entities.Feature](orm, "id"),
}
}

@ -13,7 +13,6 @@ type FeatureCategoryRepository struct {
// NewFeatureCategoryRepository 创建栏目分类仓库
func NewFeatureCategoryRepository(orm *gorm.DB) *FeatureCategoryRepository {
return &FeatureCategoryRepository{
db.NewRepositoryWith[entities.FeatureCategory](orm, "id"),
}
db.NewRepositoryWith[entities.FeatureCategory](orm, "id"),
}
}

@ -13,7 +13,6 @@ type FeatureConfigRepository struct {
// NewFeatureConfigRepository 创建栏目配置仓库
func NewFeatureConfigRepository(orm *gorm.DB) *FeatureConfigRepository {
return &FeatureConfigRepository{
db.NewRepositoryWith[entities.FeatureConfig](orm, "id"),
}
db.NewRepositoryWith[entities.FeatureConfig](orm, "id"),
}
}

@ -13,7 +13,6 @@ type FeatureContentRepository struct {
// NewFeatureContentRepository 创建栏目内容仓库
func NewFeatureContentRepository(orm *gorm.DB) *FeatureContentRepository {
return &FeatureContentRepository{
db.NewRepositoryWith[entities.FeatureContent](orm, "id"),
}
db.NewRepositoryWith[entities.FeatureContent](orm, "id"),
}
}

@ -13,7 +13,6 @@ type FeatureContentChapterRepository struct {
// NewFeatureContentChapterRepository 创建栏目内容章回仓库
func NewFeatureContentChapterRepository(orm *gorm.DB) *FeatureContentChapterRepository {
return &FeatureContentChapterRepository{
db.NewRepositoryWith[entities.FeatureContentChapter](orm, "id"),
}
db.NewRepositoryWith[entities.FeatureContentChapter](orm, "id"),
}
}

@ -13,7 +13,6 @@ type FeatureContentDetailRepository struct {
// NewFeatureContentDetailRepository 创建栏目内容详情仓库
func NewFeatureContentDetailRepository(orm *gorm.DB) *FeatureContentDetailRepository {
return &FeatureContentDetailRepository{
db.NewRepositoryWith[entities.FeatureContentDetail](orm, "id"),
}
db.NewRepositoryWith[entities.FeatureContentDetail](orm, "id"),
}
}

@ -13,7 +13,6 @@ type ResourceRepository struct {
// NewResourceRepository 创建资源仓库
func NewResourceRepository(orm *gorm.DB) *ResourceRepository {
return &ResourceRepository{
db.NewRepositoryWith[entities.Resource](orm, "id"),
}
db.NewRepositoryWith[entities.Resource](orm, "id"),
}
}

@ -13,7 +13,6 @@ type ResourceCategoryRepository struct {
// NewResourceCategoryRepository 创建资源分类仓库
func NewResourceCategoryRepository(orm *gorm.DB) *ResourceCategoryRepository {
return &ResourceCategoryRepository{
db.NewRepositoryWith[entities.ResourceCategory](orm, "id"),
}
db.NewRepositoryWith[entities.ResourceCategory](orm, "id"),
}
}

@ -13,7 +13,6 @@ type SystemLogRepository struct {
// NewSystemLogRepository 创建系统日志仓库
func NewSystemLogRepository(orm *gorm.DB) *SystemLogRepository {
return &SystemLogRepository{
db.NewRepositoryWith[entities.SystemLog](orm, "id"),
}
db.NewRepositoryWith[entities.SystemLog](orm, "id"),
}
}

@ -13,7 +13,6 @@ type SystemMenuRepository struct {
// NewSystemMenuRepository 创建系统菜单仓库
func NewSystemMenuRepository(orm *gorm.DB) *SystemMenuRepository {
return &SystemMenuRepository{
db.NewRepositoryWith[entities.SystemMenu](orm, "id"),
}
db.NewRepositoryWith[entities.SystemMenu](orm, "id"),
}
}

@ -13,7 +13,6 @@ type SystemPermissionRepository struct {
// NewSystemPermissionRepository 创建系统权限仓库
func NewSystemPermissionRepository(orm *gorm.DB) *SystemPermissionRepository {
return &SystemPermissionRepository{
db.NewRepositoryWith[entities.SystemPermission](orm, "id"),
}
db.NewRepositoryWith[entities.SystemPermission](orm, "id"),
}
}

@ -13,7 +13,6 @@ type SystemRoleRepository struct {
// NewSystemRoleRepository 创建系统用户角色仓库
func NewSystemRoleRepository(orm *gorm.DB) *SystemRoleRepository {
return &SystemRoleRepository{
db.NewRepositoryWith[entities.SystemRole](orm, "id"),
}
db.NewRepositoryWith[entities.SystemRole](orm, "id"),
}
}

@ -13,7 +13,6 @@ type SystemRolePowerRepository struct {
// NewSystemRolePowerRepository 创建角色授权仓库
func NewSystemRolePowerRepository(orm *gorm.DB) *SystemRolePowerRepository {
return &SystemRolePowerRepository{
db.NewRepositoryWith[entities.SystemRolePower](orm, "id"),
}
db.NewRepositoryWith[entities.SystemRolePower](orm, "id"),
}
}

@ -13,7 +13,6 @@ type SystemUserRepository struct {
// NewSystemUserRepository 创建系统用户仓库
func NewSystemUserRepository(orm *gorm.DB) *SystemUserRepository {
return &SystemUserRepository{
db.NewRepositoryWith[entities.SystemUser](orm, "id"),
}
db.NewRepositoryWith[entities.SystemUser](orm, "id"),
}
}

@ -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
import (
"github.com/labstack/echo/v4"
"github.com/swaggo/echo-swagger"
"gorm.io/gorm"
"net/http"
_ "sorbet/docs" // 开发文档
"log"
"sorbet/internal"
"sorbet/internal/entities"
"sorbet/internal/middleware"
"sorbet/internal/repositories"
"sorbet/internal/util"
"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() {
if err := env.Init(); err != nil {
panic(err)
}
if err := internal.Init(); err != nil {
panic(err)
}
repositories.Init()
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)
if err := internal.Start(); err != nil {
log.Panicln(err)
}
}

@ -122,20 +122,20 @@ func (q *QueryBuilder[T]) Count() (int64, error) {
return count, err
}
func (q *QueryBuilder[T]) First(entity any) error {
func (q *QueryBuilder[T]) First(entity *T) error {
return q.db.Scopes(q.Scopes).First(entity).Error
}
func (q *QueryBuilder[T]) Take(entity any) error {
func (q *QueryBuilder[T]) Take(entity *T) error {
return q.db.Scopes(q.Scopes).Take(entity).Error
}
func (q *QueryBuilder[T]) Last(entity any) error {
func (q *QueryBuilder[T]) Last(entity *T) error {
return q.db.Scopes(q.Scopes).Last(entity).Error
}
func (q *QueryBuilder[T]) Find(entity any) error {
return q.db.Scopes(q.Scopes).Find(entity).Error
func (q *QueryBuilder[T]) Find(entities *[]*T) error {
return q.db.Scopes(q.Scopes).Find(entities).Error
}
func (q *QueryBuilder[T]) Paginate() (*Pager[T], error) {

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

@ -13,7 +13,7 @@ var (
// ErrInternal 客户端请求有效,但服务器处理时发生了意外。
// 对应 HTTP 响应状态码为 500。
ErrInternal = NewError(-100, "internal error")
ErrInternal = NewError(-100, "系统内部错误")
// ErrServiceUnavailable 服务器无法处理请求,一般用于网站维护状态。
// 对应 HTTP 响应状态码为 503。
@ -55,7 +55,7 @@ var (
// ErrBadParams 客户端提交的参数不符合要求
// 对应 HTTP 状态码为 400。
ErrBadParams = NewError(-110, "bad parameters")
ErrBadParams = NewError(-110, "参数错误")
// ErrRecordNotFound 访问的数据不存在
// 对应 HTTP 状态码为 404。

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

@ -33,6 +33,18 @@ func toText(m map[string]any) (string, error) {
}
}
type RespondValuer interface {
RespondValue() any
}
type RespondData[T any] struct {
data T
}
func (r *RespondData[T]) RespondValue() any {
return r.data
}
type response struct {
code int
headers map[string]string
@ -109,7 +121,11 @@ func respond(c echo.Context, o *response) error {
m["code"] = 0
m["success"] = true
if o.data != nil {
m["data"] = o.data
if v, ok := o.data.(RespondValuer); ok {
m["data"] = v.RespondValue()
} else {
m["data"] = o.data
}
}
}
if m["message"] == "" {
Loading…
Cancel
Save