diff --git a/.gitignore b/.gitignore
index b0d13f8..8f3f017 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
*.dylib
main
testpaper
+management
# Test binary, built with `go test -c`
*.test
diff --git a/cmd/erp.go b/cmd/erp.go
new file mode 100644
index 0000000..123869e
--- /dev/null
+++ b/cmd/erp.go
@@ -0,0 +1,80 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "management/internal/config"
+ db "management/internal/db/sqlc"
+ "management/internal/erpserver"
+ "management/internal/erpserver/biz"
+ "management/internal/erpserver/handler"
+ "management/internal/pkg/logger"
+ "management/internal/pkg/middleware"
+ "management/internal/pkg/redis"
+ "management/internal/pkg/session"
+ "management/internal/pkg/snowflake"
+ "management/internal/pkg/tpl"
+
+ "github.com/spf13/cobra"
+)
+
+var erpCmd = &cobra.Command{
+ Use: "erp",
+ Short: "Start erp management server",
+ Long: `A Service to erp management`,
+ Run: func(cmd *cobra.Command, args []string) {
+ err := runErp(cmd.Context())
+ if err != nil {
+ log.Fatalf("run erp failed: %v", err)
+ }
+ },
+}
+
+func init() {
+ erpCmd.Flags().StringVarP(&configPath, "config", "c", "", "Custom config file path")
+ rootCmd.AddCommand(erpCmd)
+}
+
+func runErp(ctx context.Context) error {
+ conf, err := config.New(configPath)
+ checkError(err)
+
+ logger.New(conf.App.Prod)
+
+ store, err := db.NewIStore(ctx, conf.DB)
+ checkError(err)
+
+ // 初始化数据
+ // dbinit.InitSeed()
+
+ // mustInit(redis.Init)
+
+ redis, err := redis.New(conf.Redis)
+ checkError(err)
+
+ session := session.New(store.Pool(), conf.App.Prod)
+
+ mustInit(snowflake.Init)
+
+ biz := biz.NewBiz(store, redis, session)
+
+ middleware := middleware.New(biz.SystemV1(), session)
+
+ rander, err := tpl.New(session, biz.SystemV1().MenuBiz())
+ checkError(err)
+
+ handler := handler.NewHandler(conf, rander, session, biz)
+
+ address := fmt.Sprintf("%s:%d", conf.App.Host, conf.App.Port)
+ log.Printf("Starting erp manage server on %s", address)
+ server := InitServer(address, erpserver.NewRouter(handler, middleware))
+ return server.ListenAndServe()
+}
+
+func checkError(err error) {
+ if err != nil {
+ log.Fatalf("init failed: %v", err)
+ }
+}
diff --git a/cmd/manage.go b/cmd/manage.go
index b6fd90c..6e15cd0 100644
--- a/cmd/manage.go
+++ b/cmd/manage.go
@@ -7,10 +7,7 @@ import (
"management/internal/config"
"management/internal/pkg/logger"
- "management/internal/pkg/redis"
- "management/internal/pkg/session"
"management/internal/pkg/snowflake"
- "management/internal/tpl"
dbinit "management/internal/db/init"
db "management/internal/db/sqlc"
@@ -43,12 +40,12 @@ func runManage(ctx context.Context) error {
mustInitAny(ctx, db.NewStore)
// 初始化数据
dbinit.InitSeed()
- mustInit(redis.Init)
- session.Init()
+ // mustInit(redis.Init)
+ // session.Init()
mustInit(snowflake.Init)
// mustInit(token.NewPasetoMaker)
// mustInit(tencentoss.Init)
- mustInit(tpl.Init)
+ // mustInit(tpl.Init)
address := fmt.Sprintf("%s:%d", config.File.App.Host, config.File.App.Port)
log.Printf("Starting manage server on %s", address)
diff --git a/internal/config/config.go b/internal/config/config.go
index d8fa685..1d4f981 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -25,6 +25,40 @@ type Config struct {
Smb Smb `mapstructure:"smb" json:"smb" yaml:"smb"`
}
+func New(path string) (*Config, error) {
+ fp := "."
+ fn := ConfigDefaultFile
+ if len(path) > 0 {
+ fp, fn = filepath.Split(path)
+ if len(fp) == 0 {
+ fp = "."
+ }
+ }
+
+ v := viper.New()
+ v.AddConfigPath(fp)
+ v.SetConfigName(fn)
+ v.SetConfigType("yaml")
+ if err := v.ReadInConfig(); err != nil {
+ return nil, err
+ }
+
+ v.WatchConfig()
+
+ var conf *Config
+ v.OnConfigChange(func(e fsnotify.Event) {
+ fmt.Println("config file changed:", e.Name)
+ if err := v.Unmarshal(&conf); err != nil {
+ fmt.Println(err)
+ }
+ })
+
+ if err := v.Unmarshal(&conf); err != nil {
+ return nil, err
+ }
+ return conf, nil
+}
+
func Init(path string) error {
fp := "."
fn := ConfigDefaultFile
diff --git a/internal/db/model/dto/search.go b/internal/db/model/dto/search.go
index c735a15..fb1efca 100644
--- a/internal/db/model/dto/search.go
+++ b/internal/db/model/dto/search.go
@@ -5,6 +5,7 @@ type SearchDto struct {
SearchTimeEnd string `json:"searchTimeEnd"`
SearchStatus int `json:"searchStatus"`
SearchName string `json:"searchName"`
+ SearchID int64 `json:"searchID"`
SearchKey string `json:"searchKey"`
SearchParentID int `json:"searchParentId"`
SearchDepartmentID int `json:"searchDepartmentId"`
diff --git a/internal/db/model/dto/tree.go b/internal/db/model/dto/tree.go
index d005967..45bfa98 100644
--- a/internal/db/model/dto/tree.go
+++ b/internal/db/model/dto/tree.go
@@ -12,6 +12,7 @@ type DTreeDto struct {
Title string `json:"title"`
Last bool `json:"last"`
ParentId string `json:"parentId"`
+ Spread bool `json:"spread"`
Children []*DTreeDto `json:"children"`
}
diff --git a/internal/db/model/dto/xm_select.go b/internal/db/model/dto/xm_select.go
index 02b7653..fe77bcc 100644
--- a/internal/db/model/dto/xm_select.go
+++ b/internal/db/model/dto/xm_select.go
@@ -19,3 +19,9 @@ type XmSelectStrDto struct {
Name string `json:"name"`
Value string `json:"value"`
}
+
+type XmSelectTreeDto struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Children []*XmSelectTreeDto `json:"children"`
+}
diff --git a/internal/db/sqlc/store.go b/internal/db/sqlc/store.go
index dd2ee2d..1e15602 100644
--- a/internal/db/sqlc/store.go
+++ b/internal/db/sqlc/store.go
@@ -14,13 +14,13 @@ import (
// ****************** conn ******************
-func newDsn() string {
+func newDsn(conf config.DB) string {
return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=disable",
- config.File.DB.Username,
- config.File.DB.Password,
- config.File.DB.Host,
- config.File.DB.Port,
- config.File.DB.DBName)
+ conf.Username,
+ conf.Password,
+ conf.Host,
+ conf.Port,
+ conf.DBName)
}
// ****************** errors ******************
@@ -69,9 +69,26 @@ type SQLStore struct {
*Queries
}
+func NewIStore(ctx context.Context, conf config.DB) (Store, error) {
+ pool, err := pgxpool.New(ctx, newDsn(conf))
+ if err != nil {
+ return nil, err
+ }
+
+ err = pool.Ping(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return &SQLStore{
+ connPool: pool,
+ Queries: New(pool),
+ }, nil
+}
+
// NewStore creates a new store
func NewStore(ctx context.Context) error {
- pool, err := pgxpool.New(ctx, newDsn())
+ pool, err := pgxpool.New(ctx, newDsn(config.File.DB))
if err != nil {
return err
}
diff --git a/internal/erpserver/biz/biz.go b/internal/erpserver/biz/biz.go
new file mode 100644
index 0000000..eb7aabb
--- /dev/null
+++ b/internal/erpserver/biz/biz.go
@@ -0,0 +1,46 @@
+package biz
+
+import (
+ db "management/internal/db/sqlc"
+ commonv1 "management/internal/erpserver/biz/v1/common"
+ systemv1 "management/internal/erpserver/biz/v1/system"
+ "management/internal/pkg/redis"
+ "management/internal/pkg/session"
+)
+
+// IBiz 定义了业务层需要实现的方法.
+type IBiz interface {
+ // 获取公共业务接口.
+ CommonV1() commonv1.CommonBiz
+ // 获取系统业务接口.
+ SystemV1() systemv1.SystemBiz
+}
+
+// biz 是 IBiz 的一个具体实现.
+type biz struct {
+ store db.Store
+ redis redis.IRedis
+ session session.ISession
+}
+
+// 确保 biz 实现了 IBiz 接口.
+var _ IBiz = (*biz)(nil)
+
+// NewBiz 创建一个 IBiz 类型的实例.
+func NewBiz(store db.Store, redis redis.IRedis, session session.ISession) *biz {
+ return &biz{
+ store: store,
+ redis: redis,
+ session: session,
+ }
+}
+
+// CommonV1 返回一个实现了 CommonBiz 接口的实例.
+func (b *biz) CommonV1() commonv1.CommonBiz {
+ return commonv1.New()
+}
+
+// SystemV1 返回一个实现了 SystemBiz 接口的实例.
+func (b *biz) SystemV1() systemv1.SystemBiz {
+ return systemv1.New(b.store, b.redis, b.session)
+}
diff --git a/internal/erpserver/biz/v1/common/captcha.go b/internal/erpserver/biz/v1/common/captcha.go
new file mode 100644
index 0000000..89079c1
--- /dev/null
+++ b/internal/erpserver/biz/v1/common/captcha.go
@@ -0,0 +1,36 @@
+package common
+
+import (
+ "github.com/mojocn/base64Captcha"
+)
+
+// CaptchaBiz 定义处理验证码请求所需的方法.
+type CaptchaBiz interface {
+ Generate(height int, width int, length int, maxSkew float64, dotCount int) (id, b64s, answer string, err error)
+ Verify(id, answer string, clear bool) bool
+}
+
+// captchaBiz 是 CaptchaBiz 接口的实现.
+type captchaBiz struct{}
+
+// 确保 captchaBiz 实现了 CaptchaBiz 接口.
+var _ CaptchaBiz = (*captchaBiz)(nil)
+
+func NewCaptcha() *captchaBiz {
+ return &captchaBiz{}
+}
+
+var captchaStore base64Captcha.Store = base64Captcha.DefaultMemStore
+
+func (b *captchaBiz) Generate(height int, width int, length int, maxSkew float64, dotCount int) (id, b64s, answer string, err error) {
+ driver := base64Captcha.NewDriverDigit(height, width, length, maxSkew, dotCount)
+ // driver := base64Captcha.NewDriverString(config.File.Captcha.ImgHeight,
+ // config.File.Captcha.ImgWidth,
+ // 6, 1, keyLong, source, nil, nil, nil)
+ cp := base64Captcha.NewCaptcha(driver, captchaStore)
+ return cp.Generate()
+}
+
+func (b *captchaBiz) Verify(id, answer string, clear bool) bool {
+ return captchaStore.Verify(id, answer, clear)
+}
diff --git a/internal/erpserver/biz/v1/common/common.go b/internal/erpserver/biz/v1/common/common.go
new file mode 100644
index 0000000..379283c
--- /dev/null
+++ b/internal/erpserver/biz/v1/common/common.go
@@ -0,0 +1,17 @@
+package common
+
+type CommonBiz interface {
+ CaptchaBiz() CaptchaBiz
+}
+
+type commonBiz struct{}
+
+var _ CommonBiz = (*commonBiz)(nil)
+
+func New() *commonBiz {
+ return &commonBiz{}
+}
+
+func (b *commonBiz) CaptchaBiz() CaptchaBiz {
+ return NewCaptcha()
+}
diff --git a/internal/erpserver/biz/v1/system/audit.go b/internal/erpserver/biz/v1/system/audit.go
new file mode 100644
index 0000000..df2b05f
--- /dev/null
+++ b/internal/erpserver/biz/v1/system/audit.go
@@ -0,0 +1,31 @@
+package system
+
+import (
+ "context"
+
+ db "management/internal/db/sqlc"
+)
+
+type AuditBiz interface {
+ Create(ctx context.Context, arg *db.CreateSysAuditLogParams) error
+
+ AuditExpansion
+}
+
+type AuditExpansion interface{}
+
+type auditBiz struct {
+ store db.Store
+}
+
+var _ AuditBiz = (*auditBiz)(nil)
+
+func NewAudit(store db.Store) *auditBiz {
+ return &auditBiz{
+ store: store,
+ }
+}
+
+func (b *auditBiz) Create(ctx context.Context, arg *db.CreateSysAuditLogParams) error {
+ return b.store.CreateSysAuditLog(ctx, arg)
+}
diff --git a/internal/erpserver/biz/v1/system/config.go b/internal/erpserver/biz/v1/system/config.go
new file mode 100644
index 0000000..f1a4a52
--- /dev/null
+++ b/internal/erpserver/biz/v1/system/config.go
@@ -0,0 +1,63 @@
+package system
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "management/internal/db/model/dto"
+ db "management/internal/db/sqlc"
+ "management/internal/global/keys"
+ "management/internal/global/pearadmin"
+ "management/internal/pkg/redis"
+ "management/internal/pkg/session"
+)
+
+type ConfigBiz interface {
+ ConfigExpansion
+}
+
+type ConfigExpansion interface {
+ Pear(ctx context.Context) (*dto.PearConfig, error)
+}
+
+type configBiz struct {
+ store db.Store
+ redis redis.IRedis
+ session session.ISession
+}
+
+var _ ConfigBiz = (*configBiz)(nil)
+
+func NewConfig(store db.Store, redis redis.IRedis, session session.ISession) *configBiz {
+ return &configBiz{
+ store: store,
+ redis: redis,
+ session: session,
+ }
+}
+
+func (b *configBiz) Pear(ctx context.Context) (*dto.PearConfig, error) {
+ // 判断redis是否存储
+ key := keys.GetManageKey(ctx, keys.PearAdmin)
+ bs, err := b.redis.GetBytes(ctx, key)
+ if err == nil {
+ var res *dto.PearConfig
+ if err := json.Unmarshal(bs, &res); err == nil {
+ return res, nil
+ }
+ }
+
+ conf, err := b.store.GetSysConfigByKey(ctx, pearadmin.PearKey)
+ if err != nil {
+ return nil, err
+ }
+
+ var pear dto.PearConfig
+ if err := json.Unmarshal(conf.Value, &pear); err != nil {
+ return nil, err
+ }
+
+ _ = b.redis.Set(ctx, key, conf.Value, time.Hour*6)
+ return &pear, nil
+}
diff --git a/internal/erpserver/biz/v1/system/department.go b/internal/erpserver/biz/v1/system/department.go
new file mode 100644
index 0000000..a052646
--- /dev/null
+++ b/internal/erpserver/biz/v1/system/department.go
@@ -0,0 +1,183 @@
+package system
+
+import (
+ "context"
+ "encoding/json"
+ "strconv"
+ "time"
+
+ "management/internal/db/model/dto"
+ db "management/internal/db/sqlc"
+ "management/internal/erpserver/model/view"
+ "management/internal/global/keys"
+ "management/internal/pkg/redis"
+ "management/internal/pkg/session"
+)
+
+type DepartmentBiz interface {
+ Create(ctx context.Context, arg *db.CreateSysDepartmentParams) (*db.SysDepartment, error)
+ Update(ctx context.Context, arg *db.UpdateSysDepartmentParams) (*db.SysDepartment, error)
+ All(ctx context.Context) ([]*db.SysDepartment, error)
+ List(ctx context.Context, q dto.SearchDto) ([]*db.SysDepartment, int64, error)
+ Get(ctx context.Context, id int32) (*db.SysDepartment, error)
+ Refresh(ctx context.Context) ([]*db.SysDepartment, error)
+ RebuildParentPath(ctx context.Context) error
+
+ Tree(ctx context.Context, id int32) ([]*view.LayuiTree, error)
+ XmSelect(ctx context.Context, id int32) ([]*view.XmSelectTree, error)
+
+ DepartmentExpansion
+}
+
+type DepartmentExpansion interface{}
+
+type departmentBiz struct {
+ store db.Store
+ redis redis.IRedis
+ session session.ISession
+}
+
+var _ DepartmentBiz = (*departmentBiz)(nil)
+
+func NewDepartment(store db.Store, redis redis.IRedis, session session.ISession) *departmentBiz {
+ return &departmentBiz{
+ store: store,
+ redis: redis,
+ session: session,
+ }
+}
+
+func (b *departmentBiz) All(ctx context.Context) ([]*db.SysDepartment, error) {
+ key := keys.GetManageKey(ctx, keys.AllDepartments)
+ bs, err := redis.GetBytes(ctx, key)
+ if err == nil {
+ var res []*db.SysDepartment
+ if err := json.Unmarshal(bs, &res); err == nil {
+ return res, nil
+ }
+ }
+
+ return b.Refresh(ctx)
+}
+
+func (b *departmentBiz) List(ctx context.Context, q dto.SearchDto) ([]*db.SysDepartment, int64, error) {
+ countArg := &db.CountSysDepartmentConditionParams{
+ IsStatus: q.SearchStatus != 9999,
+ Status: int32(q.SearchStatus),
+ IsID: q.SearchID != 0,
+ ID: int32(q.SearchID),
+ IsParentID: q.SearchParentID != 0,
+ ParentID: int32(q.SearchParentID),
+ Name: q.SearchName,
+ }
+
+ dataArg := &db.ListSysDepartmentConditionParams{
+ IsStatus: q.SearchStatus != 9999,
+ Status: int32(q.SearchStatus),
+ IsID: q.SearchID != 0,
+ ID: int32(q.SearchID),
+ IsParentID: q.SearchParentID != 0,
+ ParentID: int32(q.SearchParentID),
+ Name: q.SearchName,
+ Skip: (int32(q.Page) - 1) * int32(q.Rows),
+ Size: int32(q.Rows),
+ }
+ count, err := b.store.CountSysDepartmentCondition(ctx, countArg)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ departs, err := b.store.ListSysDepartmentCondition(ctx, dataArg)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return departs, count, nil
+}
+
+func (b *departmentBiz) Get(ctx context.Context, id int32) (*db.SysDepartment, error) {
+ return b.store.GetSysDepartment(ctx, id)
+}
+
+func (b *departmentBiz) Create(ctx context.Context, arg *db.CreateSysDepartmentParams) (*db.SysDepartment, error) {
+ return b.store.CreateSysDepartment(ctx, arg)
+}
+
+func (b *departmentBiz) Update(ctx context.Context, arg *db.UpdateSysDepartmentParams) (*db.SysDepartment, error) {
+ return b.store.UpdateSysDepartment(ctx, arg)
+}
+
+func (b *departmentBiz) Refresh(ctx context.Context) ([]*db.SysDepartment, error) {
+ all, err := b.store.AllSysDepartment(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ bs, err := json.Marshal(all)
+ if err != nil {
+ return nil, err
+ }
+
+ key := keys.GetManageKey(ctx, keys.AllDepartments)
+ err = redis.Set(ctx, key, bs, time.Hour*6)
+ if err != nil {
+ return nil, err
+ }
+ return all, nil
+}
+
+func (h *departmentBiz) RebuildParentPath(ctx context.Context) error {
+ return h.store.SysDepartmentRebuildPath(ctx)
+}
+
+func (h *departmentBiz) Tree(ctx context.Context, id int32) ([]*view.LayuiTree, error) {
+ all, err := h.All(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return toLayuiTree(id, all), nil
+}
+
+func (h *departmentBiz) XmSelect(ctx context.Context, id int32) ([]*view.XmSelectTree, error) {
+ all, err := h.All(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return toXmSelectTree(id, all), nil
+}
+
+func toXmSelectTree(parentId int32, data []*db.SysDepartment) []*view.XmSelectTree {
+ var res []*view.XmSelectTree
+ for _, v := range data {
+ if v.ParentID == parentId {
+ item := view.XmSelectTree{
+ Name: v.Name,
+ Value: strconv.FormatInt(int64(v.ID), 10),
+ Children: toXmSelectTree(v.ID, data),
+ }
+ res = append(res, &item)
+ }
+ }
+
+ return res
+}
+
+func toLayuiTree(parentId int32, data []*db.SysDepartment) []*view.LayuiTree {
+ var res []*view.LayuiTree
+ for _, v := range data {
+ if v.ParentID == parentId {
+ item := view.LayuiTree{}
+ item.ID = strconv.FormatInt(int64(v.ID), 10)
+ item.Title = v.Name
+ item.Children = toLayuiTree(v.ID, data)
+ if v.ParentID == 0 {
+ item.Spread = true
+ }
+ res = append(res, &item)
+ }
+ }
+
+ return res
+}
diff --git a/internal/erpserver/biz/v1/system/menu.go b/internal/erpserver/biz/v1/system/menu.go
new file mode 100644
index 0000000..9050801
--- /dev/null
+++ b/internal/erpserver/biz/v1/system/menu.go
@@ -0,0 +1,311 @@
+package system
+
+import (
+ "context"
+ "encoding/json"
+ "strconv"
+ "strings"
+ "time"
+
+ "management/internal/db/model/dto"
+ db "management/internal/db/sqlc"
+ "management/internal/global/keys"
+ "management/internal/pkg/redis"
+ "management/internal/pkg/session"
+)
+
+type MenuBiz interface {
+ MenuExpansion
+}
+
+type MenuExpansion interface {
+ GetSysMenuByUrl(ctx context.Context, url string) (*db.SysMenu, error)
+ ListOwnerMenuByRoleID(ctx context.Context, roleID int32) ([]*dto.OwnerMenuDto, error)
+ RecursiveSysMenus(ctx context.Context, roleID int32) ([]*dto.MenuUIDto, error)
+ SetRecursiveSysMenus(ctx context.Context, roleID int32) ([]*dto.MenuUIDto, error)
+ MapOwnerMenuByRoleID(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error)
+ SetOwnerMapMenuByRoleID(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error)
+}
+
+type menuBiz struct {
+ store db.Store
+ redis redis.IRedis
+ session session.ISession
+}
+
+var _ MenuBiz = (*menuBiz)(nil)
+
+func NewMenu(store db.Store, redis redis.IRedis, session session.ISession) *menuBiz {
+ return &menuBiz{
+ store: store,
+ redis: redis,
+ session: session,
+ }
+}
+
+func (b *menuBiz) GetSysMenuByUrl(ctx context.Context, url string) (*db.SysMenu, error) {
+ return b.store.GetSysMenuByUrl(ctx, url)
+}
+
+func (b *menuBiz) ListOwnerMenuByRoleID(ctx context.Context, roleID int32) ([]*dto.OwnerMenuDto, error) {
+ // 判断redis是否存储
+ key := keys.GetManageKey(ctx, keys.OwnerMenus, roleID)
+ bs, err := b.redis.GetBytes(ctx, key)
+ if err == nil {
+ var res []*dto.OwnerMenuDto
+ if err := json.Unmarshal(bs, &res); err == nil {
+ return res, nil
+ }
+ }
+
+ return b.SetOwnerListMenuByRoleID(ctx, roleID)
+}
+
+func (b *menuBiz) SetOwnerListMenuByRoleID(ctx context.Context, roleID int32) ([]*dto.OwnerMenuDto, error) {
+ menus, err := b.ownerMenusByRoleID(ctx, roleID)
+ if err != nil {
+ return nil, err
+ }
+
+ var res []*dto.OwnerMenuDto
+ for _, menu := range menus {
+ res = append(res, &dto.OwnerMenuDto{
+ ID: menu.ID,
+ DisplayName: menu.DisplayName,
+ Url: menu.Url,
+ ParentID: menu.ParentID,
+ Avatar: menu.Avatar,
+ Style: menu.Style,
+ IsList: menu.IsList,
+ })
+ }
+
+ bs, err := json.Marshal(res)
+ if err != nil {
+ return nil, err
+ }
+
+ key := keys.GetManageKey(ctx, keys.OwnerMenus, roleID)
+ _ = redis.Set(ctx, key, bs, time.Hour*6)
+ return res, nil
+}
+
+func (b *menuBiz) RecursiveSysMenus(ctx context.Context, roleID int32) ([]*dto.MenuUIDto, error) {
+ // 判断redis是否存储
+ key := keys.GetManageKey(ctx, keys.RecursiveMenus, roleID)
+ bs, err := b.redis.GetBytes(ctx, key)
+ if err == nil {
+ var res []*dto.MenuUIDto
+ if err := json.Unmarshal(bs, &res); err == nil {
+ return res, nil
+ }
+ }
+
+ return b.SetRecursiveSysMenus(ctx, roleID)
+}
+
+func (b *menuBiz) SetRecursiveSysMenus(ctx context.Context, roleID int32) ([]*dto.MenuUIDto, error) {
+ // 判断当前用户是否有vip角色
+ role, err := b.store.GetSysRole(ctx, roleID)
+ if err != nil {
+ return nil, err
+ }
+
+ var menus []*db.SysMenu
+ if role.Vip {
+ // vip 用户
+ all, err := b.store.RecursiveSysMenus(ctx)
+ if err != nil {
+ return nil, err
+ }
+ menus = convertToMenuUIDto(all)
+
+ } else {
+ // not vip
+ all, err := b.store.RecursiveSysMenusByRoleID(ctx, roleID)
+ if err != nil {
+ return nil, err
+ }
+ menus = convertToMenuUIDto2(all)
+ }
+ menuList := uniqueSysMenus(menus)
+ if len(menuList) == 0 {
+ return nil, nil
+ }
+
+ tree := convertToUITree(menuList, 0)
+ bs, err := json.Marshal(tree)
+ if err != nil {
+ return nil, err
+ }
+
+ key := keys.GetManageKey(ctx, keys.RecursiveMenus, roleID)
+ _ = redis.Set(ctx, key, bs, time.Hour*6)
+ return tree, nil
+}
+
+func (b *menuBiz) MapOwnerMenuByRoleID(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) {
+ // 判断redis是否存储
+ key := keys.GetManageKey(ctx, keys.OwnerMenus, roleID)
+ bs, err := b.redis.GetBytes(ctx, key)
+ if err == nil {
+ var res map[string]*dto.OwnerMenuDto
+ if err := json.Unmarshal(bs, &res); err == nil {
+ return res, nil
+ }
+ }
+
+ return b.SetOwnerMapMenuByRoleID(ctx, roleID)
+}
+
+func (b *menuBiz) SetOwnerMapMenuByRoleID(ctx context.Context, roleID int32) (map[string]*dto.OwnerMenuDto, error) {
+ result := make(map[string]*dto.OwnerMenuDto)
+ menus, err := b.ownerMenusByRoleID(ctx, roleID)
+ if err != nil {
+ return result, err
+ }
+
+ for _, menu := range menus {
+ result[menu.Url] = &dto.OwnerMenuDto{
+ ID: menu.ID,
+ DisplayName: menu.DisplayName,
+ Url: menu.Url,
+ ParentID: menu.ParentID,
+ Avatar: menu.Avatar,
+ Style: menu.Style,
+ IsList: menu.IsList,
+ }
+ }
+
+ bs, err := json.Marshal(result)
+ if err != nil {
+ return nil, err
+ }
+
+ key := keys.GetManageKey(ctx, keys.OwnerMenus, roleID)
+ _ = redis.Set(ctx, key, bs, time.Hour*6)
+ return result, nil
+}
+
+func (b *menuBiz) ownerMenusByRoleID(ctx context.Context, roleID int32) ([]*db.SysMenu, error) {
+ // 判断当前用户是否有vip角色
+ role, err := b.store.GetSysRole(ctx, roleID)
+ if err != nil {
+ return nil, err
+ }
+
+ var e error
+ var menus []*db.SysMenu
+ if role.Vip {
+ // vip 用户
+ menus, e = b.store.AllSysMenu(ctx)
+ if e != nil {
+ return nil, err
+ }
+
+ } else {
+ // not vip
+ menus, e = b.store.ListSysMenuByRoleID(ctx, roleID)
+ if e != nil {
+ return nil, err
+ }
+ }
+
+ return menus, nil
+}
+
+func convertToMenuUIDto(data []*db.RecursiveSysMenusRow) []*db.SysMenu {
+ var res []*db.SysMenu
+
+ for _, item := range data {
+ temp := &db.SysMenu{
+ ID: item.ID,
+ Name: item.Name,
+ DisplayName: item.DisplayName,
+ Url: item.Url,
+ Type: item.Type,
+ ParentID: item.ParentID,
+ ParentPath: item.ParentPath,
+ Avatar: item.Avatar,
+ Style: item.Style,
+ Visible: item.Visible,
+ IsList: item.IsList,
+ Status: item.Status,
+ Sort: item.Sort,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+ res = append(res, temp)
+ }
+
+ return res
+}
+
+func convertToMenuUIDto2(data []*db.RecursiveSysMenusByRoleIDRow) []*db.SysMenu {
+ var res []*db.SysMenu
+
+ for _, item := range data {
+ temp := &db.SysMenu{
+ ID: item.ID,
+ Name: item.Name,
+ DisplayName: item.DisplayName,
+ Url: item.Url,
+ Type: item.Type,
+ ParentID: item.ParentID,
+ ParentPath: item.ParentPath,
+ Avatar: item.Avatar,
+ Style: item.Style,
+ Visible: item.Visible,
+ IsList: item.IsList,
+ Status: item.Status,
+ Sort: item.Sort,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+ res = append(res, temp)
+ }
+
+ return res
+}
+
+func convertToUITree(data []*db.SysMenu, parentID int32) []*dto.MenuUIDto {
+ var root []*dto.MenuUIDto
+ for _, item := range data {
+ if item.ParentID == parentID {
+ if item.IsList {
+ temp := &dto.MenuUIDto{
+ ID: strings.ToLower(item.Url),
+ Title: item.DisplayName,
+ Icon: item.Avatar,
+ Type: 1,
+ OpenType: "_iframe",
+ // OpenType: "_component",
+ Href: item.Url,
+ }
+ root = append(root, temp)
+ } else {
+ temp := &dto.MenuUIDto{
+ ID: strconv.Itoa(int(item.ID)),
+ Title: item.DisplayName,
+ Icon: item.Avatar,
+ Type: 0,
+ }
+ temp.Children = convertToUITree(data, item.ID)
+ root = append(root, temp)
+ }
+ }
+ }
+ return root
+}
+
+func uniqueSysMenus(sm []*db.SysMenu) []*db.SysMenu {
+ res := make([]*db.SysMenu, 0) // 返回的新切片
+ m1 := make(map[int32]byte) // 用来去重的临时map
+ for _, v := range sm {
+ if _, ok := m1[v.ID]; !ok {
+ m1[v.ID] = 1
+ res = append(res, v)
+ }
+ }
+ return res
+}
diff --git a/internal/erpserver/biz/v1/system/system.go b/internal/erpserver/biz/v1/system/system.go
new file mode 100644
index 0000000..e1f089a
--- /dev/null
+++ b/internal/erpserver/biz/v1/system/system.go
@@ -0,0 +1,51 @@
+package system
+
+import (
+ db "management/internal/db/sqlc"
+ "management/internal/pkg/redis"
+ "management/internal/pkg/session"
+)
+
+type SystemBiz interface {
+ UserBiz() UserBiz
+ MenuBiz() MenuBiz
+ DepartmentBiz() DepartmentBiz
+ AuditBiz() AuditBiz
+ ConfigBiz() ConfigBiz
+}
+
+type systemBiz struct {
+ store db.Store
+ redis redis.IRedis
+ session session.ISession
+}
+
+var _ SystemBiz = (*systemBiz)(nil)
+
+func New(store db.Store, redis redis.IRedis, session session.ISession) *systemBiz {
+ return &systemBiz{
+ store: store,
+ redis: redis,
+ session: session,
+ }
+}
+
+func (b *systemBiz) UserBiz() UserBiz {
+ return NewUser(b.store, b.session)
+}
+
+func (b *systemBiz) MenuBiz() MenuBiz {
+ return NewMenu(b.store, b.redis, b.session)
+}
+
+func (b *systemBiz) DepartmentBiz() DepartmentBiz {
+ return NewDepartment(b.store, b.redis, b.session)
+}
+
+func (b *systemBiz) AuditBiz() AuditBiz {
+ return NewAudit(b.store)
+}
+
+func (b *systemBiz) ConfigBiz() ConfigBiz {
+ return NewConfig(b.store, b.redis, b.session)
+}
diff --git a/internal/erpserver/biz/v1/system/user.go b/internal/erpserver/biz/v1/system/user.go
new file mode 100644
index 0000000..6ffac81
--- /dev/null
+++ b/internal/erpserver/biz/v1/system/user.go
@@ -0,0 +1,117 @@
+package system
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "time"
+
+ "management/internal/db/model/dto"
+ db "management/internal/db/sqlc"
+ "management/internal/erpserver/model/req"
+ "management/internal/global/know"
+ "management/internal/pkg/crypto"
+ "management/internal/pkg/session"
+)
+
+// UserBiz 定义处理用户请求所需的方法.
+type UserBiz interface {
+ Create(ctx context.Context, req *db.CreateSysUserParams) (*db.SysUser, error)
+
+ UserExpansion
+}
+
+// UserExpansion 定义用户操作的扩展方法.
+type UserExpansion interface {
+ Login(ctx context.Context, req *req.Login) error
+}
+
+// userBiz 是 UserBiz 接口的实现.
+type userBiz struct {
+ store db.Store
+ session session.ISession
+}
+
+// 确保 userBiz 实现了 UserBiz 接口.
+var _ UserBiz = (*userBiz)(nil)
+
+func NewUser(store db.Store, session session.ISession) *userBiz {
+ return &userBiz{
+ store: store,
+ session: session,
+ }
+}
+
+func (b *userBiz) Create(ctx context.Context, req *db.CreateSysUserParams) (*db.SysUser, error) {
+ return b.store.CreateSysUser(ctx, req)
+}
+
+func (b *userBiz) Login(ctx context.Context, req *req.Login) error {
+ log := &db.CreateSysUserLoginLogParams{
+ CreatedAt: time.Now(),
+ Email: req.Email,
+ IsSuccess: false,
+ RefererUrl: req.Referrer,
+ Url: req.Url,
+ Os: req.Os,
+ Ip: req.Ip,
+ Browser: req.Browser,
+ }
+
+ user, err := b.store.GetSysUserByEmail(ctx, req.Email)
+ if err != nil {
+ log.Message = err.Error()
+ _ = b.store.CreateSysUserLoginLog(ctx, log)
+ return err
+ }
+ log.UserUuid = user.Uuid
+ log.Username = user.Username
+
+ err = crypto.BcryptComparePassword(user.HashedPassword, req.Password+user.Salt)
+ if err != nil {
+ log.Message = "compare password failed"
+ _ = b.store.CreateSysUserLoginLog(ctx, log)
+ return errors.New(log.Message)
+ }
+
+ // 登陆成功
+
+ if user.RoleID == 0 {
+ log.Message = "账号没有配置角色, 请联系管理员"
+ _ = b.store.CreateSysUserLoginLog(ctx, log)
+ return errors.New(log.Message)
+ }
+
+ sysRole, err := b.store.GetSysRole(ctx, user.RoleID)
+ if err != nil {
+ log.Message = "账号配置的角色错误, 请联系管理员"
+ _ = b.store.CreateSysUserLoginLog(ctx, log)
+ return errors.New(log.Message)
+ }
+
+ auth := dto.AuthorizeUser{
+ ID: user.ID,
+ Uuid: user.Uuid,
+ Email: user.Email,
+ Username: user.Username,
+ RoleID: sysRole.ID,
+ RoleName: sysRole.Name,
+ OS: log.Os,
+ IP: log.Ip,
+ Browser: log.Browser,
+ }
+
+ gob, err := json.Marshal(auth)
+ if err != nil {
+ log.Message = err.Error()
+ _ = b.store.CreateSysUserLoginLog(ctx, log)
+ return err
+ }
+
+ b.session.Put(ctx, know.StoreName, gob)
+
+ log.IsSuccess = true
+ log.Message = "登陆成功"
+ _ = b.store.CreateSysUserLoginLog(ctx, log)
+ return nil
+}
diff --git a/internal/erpserver/handler/common/captcha.go b/internal/erpserver/handler/common/captcha.go
new file mode 100644
index 0000000..2e60d26
--- /dev/null
+++ b/internal/erpserver/handler/common/captcha.go
@@ -0,0 +1,56 @@
+package common
+
+import (
+ "net/http"
+
+ "management/internal/config"
+ commonv1 "management/internal/erpserver/biz/v1/common"
+ "management/internal/pkg/tpl"
+)
+
+type CaptchaHandler interface {
+ Captcha(w http.ResponseWriter, r *http.Request)
+}
+
+// captchaHandler 是 CaptchaHandler 接口的实现.
+type captchaHandler struct {
+ conf *config.Captcha
+ render tpl.Renderer
+ biz commonv1.CaptchaBiz
+}
+
+// 确保 captchaHandler 实现了 CaptchaHandler 接口.
+var _ CaptchaHandler = (*captchaHandler)(nil)
+
+func NewCaptchaHandler(conf *config.Captcha, render tpl.Renderer, biz commonv1.CaptchaBiz) *captchaHandler {
+ return &captchaHandler{
+ conf: conf,
+ render: render,
+ biz: biz,
+ }
+}
+
+type CaptchaResponse struct {
+ CaptchaID string `json:"captcha_id"`
+ PicPath string `json:"pic_path"`
+ CaptchaLength int `json:"captcha_length"`
+ OpenCaptcha int `json:"open_captcha"`
+}
+
+func (h *captchaHandler) Captcha(w http.ResponseWriter, r *http.Request) {
+ keyLong := h.conf.KeyLong
+ oc := h.conf.OpenCaptcha
+ id, b64s, _, err := h.biz.Generate(h.conf.ImgHeight, h.conf.ImgWidth, keyLong, 0.7, 80)
+ if err != nil {
+ h.render.JSON(w, tpl.Response{Success: false, Message: "获取验证码失败"})
+ return
+ }
+
+ rsp := CaptchaResponse{
+ CaptchaID: id,
+ PicPath: b64s,
+ CaptchaLength: keyLong,
+ OpenCaptcha: oc,
+ }
+ h.render.JSON(w, tpl.Response{Success: true, Message: "ok", Data: rsp})
+}
diff --git a/internal/erpserver/handler/common/common.go b/internal/erpserver/handler/common/common.go
new file mode 100644
index 0000000..3314bd1
--- /dev/null
+++ b/internal/erpserver/handler/common/common.go
@@ -0,0 +1,31 @@
+package common
+
+import (
+ "management/internal/config"
+ commonv1 "management/internal/erpserver/biz/v1/common"
+ "management/internal/pkg/tpl"
+)
+
+type CommonHandler interface {
+ CaptchaHandler() CaptchaHandler
+}
+
+type commonHandler struct {
+ conf *config.Config
+ render tpl.Renderer
+ biz commonv1.CommonBiz
+}
+
+var _ CommonHandler = (*commonHandler)(nil)
+
+func NewCommonHandler(conf *config.Config, render tpl.Renderer, biz commonv1.CommonBiz) *commonHandler {
+ return &commonHandler{
+ conf: conf,
+ render: render,
+ biz: biz,
+ }
+}
+
+func (h *commonHandler) CaptchaHandler() CaptchaHandler {
+ return NewCaptchaHandler(&h.conf.Captcha, h.render, h.biz.CaptchaBiz())
+}
diff --git a/internal/erpserver/handler/handler.go b/internal/erpserver/handler/handler.go
new file mode 100644
index 0000000..b4e2084
--- /dev/null
+++ b/internal/erpserver/handler/handler.go
@@ -0,0 +1,55 @@
+package handler
+
+import (
+ "net/http"
+
+ "management/internal/config"
+ "management/internal/erpserver/biz"
+ "management/internal/erpserver/handler/common"
+ "management/internal/erpserver/handler/system"
+ "management/internal/pkg/session"
+ "management/internal/pkg/tpl"
+)
+
+// IHandler 定义了Handler需要实现的方法.
+type IHandler interface {
+ // 获取 Common Handler 接口.
+ CommonHandler() common.CommonHandler
+
+ // 获取首页
+ Home(w http.ResponseWriter, req *http.Request)
+
+ // 获取 System Handler 接口.
+ SystemHandler() system.SystemHandler
+}
+
+// handler 是 IHandler 的一个具体实现.
+type handler struct {
+ conf *config.Config
+ render tpl.Renderer
+ session session.ISession
+ biz biz.IBiz
+}
+
+// 确保 handler 实现了 IHandler 接口.
+var _ IHandler = (*handler)(nil)
+
+// NewHandler 创建一个 IHandler 类型的实例.
+func NewHandler(conf *config.Config, render tpl.Renderer, session session.ISession, biz biz.IBiz) *handler {
+ return &handler{
+ conf: conf,
+ render: render,
+ session: session,
+ biz: biz,
+ }
+}
+
+// CommonHandler 返回一个实现了 CommonHandler 接口的实例.
+func (h *handler) CommonHandler() common.CommonHandler {
+ return common.NewCommonHandler(h.conf, h.render, h.biz.CommonV1())
+}
+
+// SystemHandler 返回一个实现了 SystemHandler 接口的实例.
+func (h *handler) SystemHandler() system.SystemHandler {
+ return system.NewSystemHandler(h.render, h.session, h.biz)
+}
diff --git a/internal/erpserver/handler/home.go b/internal/erpserver/handler/home.go
new file mode 100644
index 0000000..24b8b90
--- /dev/null
+++ b/internal/erpserver/handler/home.go
@@ -0,0 +1,7 @@
+package handler
+
+import "net/http"
+
+func (h *handler) Home(w http.ResponseWriter, r *http.Request) {
+ h.render.HTML(w, r, "home/home.tmpl", nil)
+}
diff --git a/internal/erpserver/handler/system/config.go b/internal/erpserver/handler/system/config.go
new file mode 100644
index 0000000..6c440a4
--- /dev/null
+++ b/internal/erpserver/handler/system/config.go
@@ -0,0 +1,50 @@
+package system
+
+import (
+ "net/http"
+
+ "management/internal/erpserver/biz"
+ "management/internal/pkg/session"
+ "management/internal/pkg/tpl"
+)
+
+type ConfigHandler interface {
+ // Add(w http.ResponseWriter, r *http.Request)
+ // Edit(w http.ResponseWriter, r *http.Request)
+ // Save(w http.ResponseWriter, r *http.Request)
+ // List(w http.ResponseWriter, r *http.Request)
+
+ ConfigExpansion
+}
+
+type ConfigExpansion interface {
+ Pear(w http.ResponseWriter, r *http.Request)
+}
+
+// configHandler 是 ConfigHandler 接口的实现.
+type configHandler struct {
+ render tpl.Renderer
+ session session.ISession
+ biz biz.IBiz
+}
+
+// 确保 userHandler 实现了 ConfigHandler 接口.
+var _ ConfigHandler = (*configHandler)(nil)
+
+func NewConfigHandler(render tpl.Renderer, session session.ISession, biz biz.IBiz) *configHandler {
+ return &configHandler{
+ render: render,
+ session: session,
+ biz: biz,
+ }
+}
+
+func (h *configHandler) Pear(w http.ResponseWriter, r *http.Request) {
+ pear, err := h.biz.SystemV1().ConfigBiz().Pear(r.Context())
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ h.render.JSON(w, pear)
+}
diff --git a/internal/erpserver/handler/system/department.go b/internal/erpserver/handler/system/department.go
new file mode 100644
index 0000000..87b1866
--- /dev/null
+++ b/internal/erpserver/handler/system/department.go
@@ -0,0 +1,206 @@
+package system
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "management/internal/db/model/dto"
+ db "management/internal/db/sqlc"
+ "management/internal/erpserver/biz"
+ "management/internal/pkg/convertor"
+ "management/internal/pkg/tpl"
+)
+
+type DepartmentHandler interface {
+ List(w http.ResponseWriter, r *http.Request)
+ Add(w http.ResponseWriter, r *http.Request)
+ AddChildren(w http.ResponseWriter, r *http.Request)
+ Edit(w http.ResponseWriter, r *http.Request)
+ Save(w http.ResponseWriter, r *http.Request)
+ Tree(w http.ResponseWriter, r *http.Request)
+ Refresh(w http.ResponseWriter, r *http.Request)
+ RebuildParentPath(w http.ResponseWriter, r *http.Request)
+}
+
+type departmentHandler struct {
+ render tpl.Renderer
+ biz biz.IBiz
+}
+
+var _ DepartmentHandler = (*departmentHandler)(nil)
+
+func NewDepartmentHandler(render tpl.Renderer, biz biz.IBiz) *departmentHandler {
+ return &departmentHandler{
+ render: render,
+ biz: biz,
+ }
+}
+
+func (h *departmentHandler) List(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodGet {
+ h.render.HTML(w, r, "department/list.tmpl", nil)
+ return
+ } else if r.Method == http.MethodPost {
+ var q dto.SearchDto
+ q.SearchStatus = convertor.ConvertInt(r.PostFormValue("status"), 9999)
+ q.SearchParentID = convertor.ConvertInt(r.PostFormValue("parentId"), 0)
+ q.SearchName = r.PostFormValue("name")
+ q.SearchID = convertor.ConvertInt[int64](r.PostFormValue("id"), 0)
+ q.Page = convertor.ConvertInt(r.PostFormValue("page"), 1)
+ q.Rows = convertor.ConvertInt(r.PostFormValue("rows"), 10)
+ res, count, err := h.biz.SystemV1().DepartmentBiz().List(r.Context(), q)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ data := tpl.ResponseList{
+ Code: 0,
+ Message: "ok",
+ Count: count,
+ Data: res,
+ }
+ h.render.JSON(w, data)
+ return
+ }
+
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+}
+
+func (h *departmentHandler) Add(w http.ResponseWriter, r *http.Request) {
+ h.render.HTML(w, r, "department/edit.tmpl", map[string]any{
+ "Item": &db.SysDepartment{Sort: 6666},
+ })
+}
+
+func (h *departmentHandler) AddChildren(w http.ResponseWriter, r *http.Request) {
+ vars := r.URL.Query()
+ parentID := convertor.QueryInt(vars, "parentID", 0)
+ vm := &db.SysDepartment{ParentID: int32(parentID), Sort: 6666}
+ h.render.HTML(w, r, "department/edit.tmpl", map[string]any{
+ "Item": vm,
+ })
+}
+
+func (h *departmentHandler) Edit(w http.ResponseWriter, r *http.Request) {
+ vars := r.URL.Query()
+ id := convertor.QueryInt[int32](vars, "id", 0)
+ vm := &db.SysDepartment{Sort: 6666}
+ if id > 0 {
+ vm, _ = h.biz.SystemV1().DepartmentBiz().Get(r.Context(), id)
+ }
+ h.render.HTML(w, r, "department/edit.tmpl", map[string]any{
+ "Item": vm,
+ })
+}
+
+func (h *departmentHandler) Save(w http.ResponseWriter, r *http.Request) {
+ id := convertor.ConvertInt[int32](r.PostFormValue("ID"), 0)
+ ParentID := convertor.ConvertInt[int32](r.PostFormValue("ParentID"), 0)
+ name := r.PostFormValue("Name")
+ sort := convertor.ConvertInt[int32](r.PostFormValue("Sort"), 6666)
+ status := convertor.ConvertInt[int32](r.PostFormValue("Status"), 9999)
+
+ ctx := r.Context()
+ var parent *db.SysDepartment
+ if ParentID > 0 {
+ var err error
+ parent, err = h.biz.SystemV1().DepartmentBiz().Get(ctx, ParentID)
+ if err != nil {
+ h.render.JSONERR(w, "父级节点错误")
+ return
+ }
+ }
+
+ if id == 0 {
+ arg := db.CreateSysDepartmentParams{
+ Name: name,
+ ParentID: ParentID,
+ ParentPath: fmt.Sprintf("%s,%d,", parent.ParentPath, parent.ID),
+ Status: status,
+ Sort: sort,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+ _, err := h.biz.SystemV1().DepartmentBiz().Create(ctx, &arg)
+ if err != nil {
+ if db.IsUniqueViolation(err) {
+ h.render.JSONERR(w, "部门名称已存在")
+ return
+ }
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSONOK(w, "添加成功")
+ } else {
+ res, err := h.biz.SystemV1().DepartmentBiz().Get(ctx, id)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ arg := &db.UpdateSysDepartmentParams{
+ ID: res.ID,
+ Name: name,
+ ParentID: ParentID,
+ ParentPath: fmt.Sprintf("%s,%d,", parent.ParentPath, parent.ID),
+ Status: status,
+ Sort: sort,
+ UpdatedAt: time.Now(),
+ }
+ _, err = h.biz.SystemV1().DepartmentBiz().Update(ctx, arg)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSONOK(w, "更新成功")
+ }
+}
+
+func (h *departmentHandler) Tree(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ vars := r.URL.Query()
+ if vars.Get("type") == "xmselect" {
+ res, err := h.biz.SystemV1().DepartmentBiz().XmSelect(ctx, 0)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSON(w, res)
+ return
+ } else {
+ res, err := h.biz.SystemV1().DepartmentBiz().Tree(ctx, 0)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSON(w, res)
+ return
+ }
+}
+
+func (h *departmentHandler) Refresh(w http.ResponseWriter, r *http.Request) {
+ _, err := h.biz.SystemV1().DepartmentBiz().Refresh(r.Context())
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSONOK(w, "刷新成功")
+}
+
+func (h *departmentHandler) RebuildParentPath(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ err := h.biz.SystemV1().DepartmentBiz().RebuildParentPath(ctx)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSONOK(w, "重建成功")
+}
diff --git a/internal/erpserver/handler/system/menu.go b/internal/erpserver/handler/system/menu.go
new file mode 100644
index 0000000..4cb8ae5
--- /dev/null
+++ b/internal/erpserver/handler/system/menu.go
@@ -0,0 +1,53 @@
+package system
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "management/internal/db/model/dto"
+ "management/internal/erpserver/biz"
+ "management/internal/global/know"
+ "management/internal/pkg/session"
+ "management/internal/pkg/tpl"
+)
+
+type MenuHandler interface {
+ MenuExpansion
+}
+
+type MenuExpansion interface {
+ Menus(w http.ResponseWriter, r *http.Request)
+}
+
+type menuHandler struct {
+ render tpl.Renderer
+ session session.ISession
+ biz biz.IBiz
+}
+
+var _ MenuHandler = (*menuHandler)(nil)
+
+func NewMenuHandler(render tpl.Renderer, session session.ISession, biz biz.IBiz) *menuHandler {
+ return &menuHandler{
+ render: render,
+ session: session,
+ biz: biz,
+ }
+}
+
+func (h *menuHandler) Menus(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ b := h.session.GetBytes(ctx, know.StoreName)
+ var u dto.AuthorizeUser
+ if err := json.Unmarshal(b, &u); err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+ menus, err := h.biz.SystemV1().MenuBiz().RecursiveSysMenus(ctx, u.RoleID)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSON(w, menus)
+}
diff --git a/internal/erpserver/handler/system/system.go b/internal/erpserver/handler/system/system.go
new file mode 100644
index 0000000..608fba1
--- /dev/null
+++ b/internal/erpserver/handler/system/system.go
@@ -0,0 +1,46 @@
+package system
+
+import (
+ "management/internal/erpserver/biz"
+ "management/internal/pkg/session"
+ "management/internal/pkg/tpl"
+)
+
+type SystemHandler interface {
+ UserHandler() UserHandler
+ MenuHandler() MenuHandler
+ DepartmentHandler() DepartmentHandler
+ ConfigHandler() ConfigHandler
+}
+
+type systemHandler struct {
+ render tpl.Renderer
+ session session.ISession
+ biz biz.IBiz
+}
+
+var _ SystemHandler = (*systemHandler)(nil)
+
+func NewSystemHandler(render tpl.Renderer, session session.ISession, biz biz.IBiz) *systemHandler {
+ return &systemHandler{
+ render: render,
+ session: session,
+ biz: biz,
+ }
+}
+
+func (h *systemHandler) UserHandler() UserHandler {
+ return NewUserHandler(h.render, h.session, h.biz)
+}
+
+func (h *systemHandler) MenuHandler() MenuHandler {
+ return NewMenuHandler(h.render, h.session, h.biz)
+}
+
+func (h *systemHandler) DepartmentHandler() DepartmentHandler {
+ return NewDepartmentHandler(h.render, h.biz)
+}
+
+func (h *systemHandler) ConfigHandler() ConfigHandler {
+ return NewConfigHandler(h.render, h.session, h.biz)
+}
diff --git a/internal/erpserver/handler/system/user.go b/internal/erpserver/handler/system/user.go
new file mode 100644
index 0000000..5878b10
--- /dev/null
+++ b/internal/erpserver/handler/system/user.go
@@ -0,0 +1,127 @@
+package system
+
+import (
+ "encoding/json"
+ "net/http"
+ "strings"
+
+ "management/internal/db/model/dto"
+ "management/internal/erpserver/biz"
+ "management/internal/erpserver/model/req"
+ "management/internal/global/know"
+ "management/internal/pkg/session"
+ "management/internal/pkg/tpl"
+
+ "github.com/zhang2092/browser"
+)
+
+type UserHandler interface {
+ Add(w http.ResponseWriter, r *http.Request)
+ Edit(w http.ResponseWriter, r *http.Request)
+ Save(w http.ResponseWriter, r *http.Request)
+ List(w http.ResponseWriter, r *http.Request)
+
+ UserExpansion
+}
+
+type UserExpansion interface {
+ Login(w http.ResponseWriter, r *http.Request)
+ Logout(w http.ResponseWriter, r *http.Request)
+}
+
+// userHandler 是 UserHandler 接口的实现.
+type userHandler struct {
+ render tpl.Renderer
+ session session.ISession
+ biz biz.IBiz
+}
+
+// 确保 userHandler 实现了 UserHandler 接口.
+var _ UserHandler = (*userHandler)(nil)
+
+func NewUserHandler(render tpl.Renderer, session session.ISession, biz biz.IBiz) *userHandler {
+ return &userHandler{
+ render: render,
+ session: session,
+ biz: biz,
+ }
+}
+
+func (h *userHandler) Add(w http.ResponseWriter, r *http.Request) {}
+
+func (h *userHandler) Edit(w http.ResponseWriter, r *http.Request) {}
+
+func (h *userHandler) Save(w http.ResponseWriter, r *http.Request) {}
+
+func (h *userHandler) List(w http.ResponseWriter, r *http.Request) {}
+
+func (h *userHandler) Login(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ if r.Method == http.MethodGet {
+ var user dto.AuthorizeUser
+ u := h.session.GetBytes(ctx, know.StoreName)
+ if err := json.Unmarshal(u, &user); err == nil {
+ // 判断租户是否一致, 一致则刷新令牌,跳转到首页
+ if err := h.session.RenewToken(ctx); err == nil {
+ h.session.Put(ctx, know.StoreName, u)
+ http.Redirect(w, r, "/home.html", http.StatusFound)
+ return
+ }
+ }
+
+ h.session.Destroy(ctx)
+ h.render.HTML(w, r, "oauth/login.tmpl", nil)
+ return
+ } else if r.Method == http.MethodPost {
+ req := &req.Login{
+ Email: strings.TrimSpace(r.PostFormValue("email")),
+ Password: strings.TrimSpace(r.PostFormValue("password")),
+ CaptchaID: strings.TrimSpace(r.PostFormValue("captcha_id")),
+ Captcha: strings.TrimSpace(r.PostFormValue("captcha")),
+ Ip: r.RemoteAddr,
+ Referrer: r.Header.Get("Referer"),
+ Url: r.URL.RequestURI(),
+ }
+
+ if len(req.Email) == 0 {
+ h.render.JSON(w, tpl.Response{Success: false, Message: "请填写邮箱"})
+ return
+ }
+ if len(req.Password) == 0 {
+ h.render.JSON(w, tpl.Response{Success: false, Message: "请填写密码"})
+ return
+ }
+ if len(req.Captcha) == 0 {
+ h.render.JSON(w, tpl.Response{Success: false, Message: "请填写验证码"})
+ return
+ }
+ if !h.biz.CommonV1().CaptchaBiz().Verify(req.CaptchaID, req.Captcha, true) {
+ h.render.JSON(w, tpl.Response{Success: false, Message: "验证码错误"})
+ return
+ }
+
+ br, err := browser.NewBrowser(r.Header.Get("User-Agent"))
+ if err != nil {
+ h.render.JSONERR(w, "平台信息获取错误")
+ return
+ }
+
+ req.Os = br.Platform().Name()
+ req.Browser = br.Name()
+ err = h.biz.SystemV1().UserBiz().Login(ctx, req)
+ if err != nil {
+ h.render.JSONERR(w, err.Error())
+ return
+ }
+
+ h.render.JSONOK(w, "login successful")
+ return
+ }
+
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+}
+
+func (h *userHandler) Logout(w http.ResponseWriter, r *http.Request) {
+ h.session.Destroy(r.Context())
+ http.Redirect(w, r, "/", http.StatusFound)
+}
diff --git a/internal/erpserver/http.go b/internal/erpserver/http.go
new file mode 100644
index 0000000..96429b5
--- /dev/null
+++ b/internal/erpserver/http.go
@@ -0,0 +1,147 @@
+package erpserver
+
+import (
+ "net/http"
+
+ "management/internal/erpserver/handler"
+ mw "management/internal/pkg/middleware"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+)
+
+func NewRouter(handler handler.IHandler, mw mw.IMiddleware) *chi.Mux {
+ r := chi.NewRouter()
+
+ r.Use(middleware.RequestID)
+ r.Use(middleware.RealIP)
+ // r.Use(middleware.Logger)
+ r.Use(middleware.Recoverer)
+
+ staticServer := http.FileServer(http.Dir("./web/statics/"))
+ r.Handle("/statics/*", http.StripPrefix("/statics", staticServer))
+
+ uploadServer := http.FileServer(http.Dir("./upload/"))
+ r.Handle("/upload/*", http.StripPrefix("/upload", uploadServer))
+
+ r.Group(func(r chi.Router) {
+ r.Use(mw.NoSurf) // CSRF
+ r.Use(mw.LoadSession) // Session
+
+ r.Get("/captcha", handler.CommonHandler().CaptchaHandler().Captcha)
+
+ r.Get("/", handler.SystemHandler().UserHandler().Login)
+ r.Post("/login", handler.SystemHandler().UserHandler().Login)
+ r.Get("/logout", handler.SystemHandler().UserHandler().Logout)
+
+ // r.With(auth.Authorize, mw.Audit).Post("/upload/img", commonhandler.UploadImg)
+ // r.With(auth.Authorize, mw.Audit).Post("/upload/file", commonhandler.UploadFile)
+ // r.With(auth.Authorize, mw.Audit).Post("/upload/mutilfile", commonhandler.UploadMutilFiles)
+
+ r.With(mw.Authorize, mw.Audit).Get("/home.html", handler.Home)
+ r.With(mw.Authorize).Get("/pear.json", handler.SystemHandler().ConfigHandler().Pear)
+
+ r.Route("/system", func(r chi.Router) {
+ r.Use(mw.Authorize)
+
+ // r.Route("/config", func(r chi.Router) {
+ // r.Use(mw.Audit)
+ // r.Get("/list", configHandler.List)
+ // r.Post("/list", configHandler.PostList)
+ // r.Get("/add", configHandler.Add)
+ // r.Get("/edit", configHandler.Edit)
+ // r.Post("/save", configHandler.Save)
+ // r.Post("/reset_pear", configHandler.ResetPear)
+ // r.Post("/refresh", configHandler.Refresh)
+ // })
+
+ r.Route("/department", func(r chi.Router) {
+ r.Use(mw.Audit)
+ r.Get("/list", handler.SystemHandler().DepartmentHandler().List)
+ r.Post("/list", handler.SystemHandler().DepartmentHandler().List)
+ r.Get("/add", handler.SystemHandler().DepartmentHandler().Add)
+ r.Get("/add_children", handler.SystemHandler().DepartmentHandler().AddChildren)
+ r.Get("/edit", handler.SystemHandler().DepartmentHandler().Edit)
+ r.Post("/save", handler.SystemHandler().DepartmentHandler().Save)
+ r.Post("/tree", handler.SystemHandler().DepartmentHandler().Tree)
+ r.Post("/refresh", handler.SystemHandler().DepartmentHandler().Refresh)
+ r.Post("/rebuild_parent_path", handler.SystemHandler().DepartmentHandler().RebuildParentPath)
+ })
+
+ // r.Route("/user", func(r chi.Router) {
+ // r.Use(mw.Audit)
+ // userHandler := systemhandler.NewSysUserHandler()
+ // r.Get("/list", userHandler.List)
+ // r.Post("/list", userHandler.PostList)
+ // r.Get("/add", userHandler.Add)
+ // r.Get("/edit", userHandler.Edit)
+ // r.Post("/save", userHandler.Save)
+ // r.Get("/profile", userHandler.Profile)
+ // r.Post("/xmselect", userHandler.XmSelect)
+ // })
+
+ // r.Route("/login_log", func(r chi.Router) {
+ // // r.Use(mw.Audit)
+ // userLoginLogHandler := systemhandler.NewSysUserLoginLogHandler()
+ // r.Get("/list", userLoginLogHandler.List)
+ // r.Post("/list", userLoginLogHandler.PostList)
+ // })
+
+ // r.Route("/audit_log", func(r chi.Router) {
+ // // r.Use(mw.Audit)
+ // userAuditLogHandler := systemhandler.NewSysAuditLogHandler()
+ // r.Get("/list", userAuditLogHandler.List)
+ // r.Post("/list", userAuditLogHandler.PostList)
+ // })
+
+ // r.Route("/role", func(r chi.Router) {
+ // r.Use(mw.Audit)
+ // roleHandler := systemhandler.NewSysRoleHandler()
+ // r.Get("/list", roleHandler.List)
+ // r.Post("/list", roleHandler.PostList)
+ // r.Get("/add", roleHandler.Add)
+ // r.Get("/edit", roleHandler.Edit)
+ // r.Post("/save", roleHandler.Save)
+ // r.Post("/dtree", roleHandler.DTree)
+ // r.Post("/refresh", roleHandler.Refresh)
+ // r.Post("/rebuild_parent_path", roleHandler.RebuildParentPath)
+ // r.Post("/refresh_role_menus", roleHandler.RefreshRoleMenus)
+ // r.Post("/xm_select", roleHandler.XmSelect)
+ // r.Get("/set_menu", roleHandler.SetMenu)
+ // r.Post("/set_menu", roleHandler.PostSetMenu)
+ // })
+
+ r.Get("/menus", handler.SystemHandler().MenuHandler().Menus)
+ // r.Route("/menu", func(r chi.Router) {
+ // r.Use(mw.Audit)
+ // r.Get("/tree", menuHandler.Tree)
+ // r.Get("/list", menuHandler.List)
+ // r.Post("/list", menuHandler.PostList)
+ // r.Get("/add", menuHandler.Add)
+ // r.Get("/add_children", menuHandler.AddChildren)
+ // r.Get("/edit", menuHandler.Edit)
+ // r.Post("/save", menuHandler.Save)
+ // r.Post("/xm_select_tree", menuHandler.XmSelectTree)
+ // r.Post("/refresh_cache", menuHandler.Refresh)
+ // })
+
+ // // 类别
+ // r.Route("/category", func(r chi.Router) {
+ // r.Use(mw.Audit)
+ // categoryHandler := categoryhandler.NewCategoryHandler()
+ // r.Get("/list", categoryHandler.List)
+ // r.Post("/list", categoryHandler.PostList)
+ // r.Get("/add", categoryHandler.Add)
+ // r.Get("/add_children", categoryHandler.AddChildren)
+ // r.Get("/edit", categoryHandler.Edit)
+ // r.Post("/save", categoryHandler.Save)
+ // r.Post("/dtree", categoryHandler.DTree)
+ // r.Post("/xmselect", categoryHandler.XmSelect)
+ // r.Post("/refresh", categoryHandler.Refresh)
+ // r.Post("/rebuild_parent_path", categoryHandler.RebuildParentPath)
+ // })
+ })
+ })
+
+ return r
+}
diff --git a/internal/erpserver/model/req/user.go b/internal/erpserver/model/req/user.go
new file mode 100644
index 0000000..d098ef5
--- /dev/null
+++ b/internal/erpserver/model/req/user.go
@@ -0,0 +1,15 @@
+package req
+
+type Login struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+ Captcha string `json:"captcha"`
+ CaptchaID string `json:"captcha_id"`
+
+ // 平台信息
+ Os string `json:"os"`
+ Ip string `json:"ip"`
+ Browser string `json:"browser"`
+ Referrer string `json:"referrer"`
+ Url string `json:"url"`
+}
diff --git a/internal/erpserver/model/view/system.go b/internal/erpserver/model/view/system.go
new file mode 100644
index 0000000..8a45de4
--- /dev/null
+++ b/internal/erpserver/model/view/system.go
@@ -0,0 +1,14 @@
+package view
+
+type LayuiTree struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Spread bool `json:"spread"`
+ Children []*LayuiTree `json:"children"`
+}
+
+type XmSelectTree struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Children []*XmSelectTree `json:"children"`
+}
diff --git a/internal/global/know/know.go b/internal/global/know/know.go
index 6584e97..1d30ebd 100644
--- a/internal/global/know/know.go
+++ b/internal/global/know/know.go
@@ -10,4 +10,36 @@ const (
IncomeCategory = "income_category"
ExpenseCategory = "expense_category"
+
+ CookieName = "authorize"
+ StoreName = "authorize_user"
+)
+
+var (
+ // pear admin 配置
+ PearAdmin = "m:pearjson"
+
+ // 所有类别
+ AllCategories = "m:category:all"
+ // 所有类别 简单信息
+ AllCategorySimple = "m:categorysimple:all"
+ // 类别列表 根据 父id 获取
+ ListCategoriesByParentID = "m:category:parent_id:%d"
+
+ // 所有部门
+ AllDepartments = "m:department:all"
+
+ // 所有菜单
+ AllMenus = "m:menus:all"
+ // 递归菜单
+ RecursiveMenus = "m:rec_menus:%d"
+ // 根据用户ID获取菜单
+ AdminMenus = "m:admin_menus:%d"
+ // 登陆用户的菜单
+ OwnerMenus = "m:owner_menus:%d"
+ // 登陆用户的菜单
+ OwnerMenusMap = "m:owner_menus_map:%d"
+
+ // 所有角色
+ AllRoles = "m:role:all"
)
diff --git a/internal/middleware/manage/auth/authorize.go b/internal/middleware/manage/auth/authorize.go
index 944dddb..964a90c 100644
--- a/internal/middleware/manage/auth/authorize.go
+++ b/internal/middleware/manage/auth/authorize.go
@@ -2,12 +2,9 @@ package auth
import (
"context"
- "encoding/json"
"net/http"
"management/internal/db/model/dto"
- "management/internal/global/auth"
- "management/internal/pkg/session"
systemservice "management/internal/service/system"
)
@@ -61,23 +58,23 @@ func Authorize(next http.Handler) http.Handler {
}
func isLogin(ctx context.Context) (*dto.AuthorizeUser, bool) {
- if exists := session.Exists(ctx, auth.StoreName); exists {
- b := session.GetBytes(ctx, auth.StoreName)
- var user dto.AuthorizeUser
- if err := json.Unmarshal(b, &user); err != nil {
- return nil, false
- }
- return &user, true
- }
+ // if exists := session.Exists(ctx, auth.StoreName); exists {
+ // b := session.GetBytes(ctx, auth.StoreName)
+ // var user dto.AuthorizeUser
+ // if err := json.Unmarshal(b, &user); err != nil {
+ // return nil, false
+ // }
+ // return &user, true
+ // }
return nil, false
}
func AuthUser(ctx context.Context) dto.AuthorizeUser {
var user dto.AuthorizeUser
- if exists := session.Exists(ctx, auth.StoreName); exists {
- b := session.GetBytes(ctx, auth.StoreName)
- _ = json.Unmarshal(b, &user)
- }
+ // if exists := session.Exists(ctx, auth.StoreName); exists {
+ // b := session.GetBytes(ctx, auth.StoreName)
+ // _ = json.Unmarshal(b, &user)
+ // }
return user
}
diff --git a/internal/middleware/manage/session/session.go b/internal/middleware/manage/session/session.go
index cbc6d99..2af6705 100644
--- a/internal/middleware/manage/session/session.go
+++ b/internal/middleware/manage/session/session.go
@@ -6,6 +6,6 @@ import (
"management/internal/pkg/session"
)
-func LoadSession(next http.Handler) http.Handler {
+func LoadSession(session session.ISession, next http.Handler) http.Handler {
return session.LoadAndSave(next)
}
diff --git a/internal/pkg/convertor/http_form.go b/internal/pkg/convertor/http_form.go
index da99ba2..ba5658a 100644
--- a/internal/pkg/convertor/http_form.go
+++ b/internal/pkg/convertor/http_form.go
@@ -1,6 +1,9 @@
package convertor
-import "strconv"
+import (
+ "net/url"
+ "strconv"
+)
func ConvertInt[T int | int16 | int32 | int64](value string, defaultValue T) T {
i, err := strconv.Atoi(value)
@@ -9,3 +12,17 @@ func ConvertInt[T int | int16 | int32 | int64](value string, defaultValue T) T {
}
return T(i)
}
+
+func QueryInt[T int | int16 | int32 | int64](vars url.Values, key string, defaultValue T) T {
+ v := vars.Get(key)
+ if len(v) == 0 {
+ return defaultValue
+ }
+
+ i, err := strconv.Atoi(v)
+ if err != nil {
+ return defaultValue
+ }
+
+ return T(i)
+}
diff --git a/internal/pkg/logger/log.go b/internal/pkg/logger/log.go
index 756871b..bf5add1 100644
--- a/internal/pkg/logger/log.go
+++ b/internal/pkg/logger/log.go
@@ -11,6 +11,27 @@ import (
"github.com/rs/zerolog/log"
)
+func New(prod bool) {
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+ logRotate := &lumberjack.Logger{
+ Filename: "./log/run.log", // 日志文件的位置
+ MaxSize: 10, // 在进行切割之前,日志文件的最大大小(以MB为单位)
+ MaxBackups: 100, // 保留旧文件的最大个数
+ MaxAge: 30, // 保留旧文件的最大天数
+ Compress: true,
+ }
+ zerolog.TimeFieldFormat = time.DateTime
+ log.Logger = log.With().Caller().Logger()
+
+ if prod {
+ log.Logger = log.Output(logRotate)
+ } else {
+ consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.DateTime}
+ multi := zerolog.MultiLevelWriter(consoleWriter, logRotate)
+ log.Logger = log.Output(multi)
+ }
+}
+
func Init() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
logRotate := &lumberjack.Logger{
diff --git a/internal/pkg/middleware/audit.go b/internal/pkg/middleware/audit.go
new file mode 100644
index 0000000..ac31824
--- /dev/null
+++ b/internal/pkg/middleware/audit.go
@@ -0,0 +1,73 @@
+package middleware
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ db "management/internal/db/sqlc"
+
+ "github.com/zhang2092/browser"
+)
+
+func (m *middleware) Audit(next http.Handler) http.Handler {
+ fn := func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ defer func(res http.ResponseWriter, req *http.Request) {
+ // 记录审计日志
+ go m.writeLog(req, start)
+ }(w, r)
+ next.ServeHTTP(w, r)
+ }
+
+ return http.HandlerFunc(fn)
+}
+
+func (m *middleware) writeLog(req *http.Request, start time.Time) {
+ end := time.Now()
+ duration := end.Sub(start)
+ var params string
+ method := req.Method
+ if method == "GET" {
+ params = req.URL.Query().Encode()
+ } else if method == "POST" {
+ contentType := req.Header.Get("Content-Type")
+ if strings.Contains(contentType, "application/json") {
+ body := make([]byte, req.ContentLength)
+ req.Body.Read(body)
+ params = string(body)
+ } else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
+ params = req.Form.Encode()
+ }
+ }
+
+ ctx := req.Context()
+ au := m.AuthUser(ctx)
+ arg := &db.CreateSysAuditLogParams{
+ CreatedAt: time.Now(),
+ Email: au.Email,
+ Username: au.Username,
+ UserUuid: au.Uuid,
+ StartAt: start,
+ EndAt: end,
+ Duration: strconv.FormatInt(duration.Milliseconds(), 10),
+ Url: req.URL.RequestURI(),
+ Method: method,
+ Parameters: params,
+ RefererUrl: req.Header.Get("Referer"),
+ Ip: req.RemoteAddr,
+ Remark: "",
+ }
+ br, err := browser.NewBrowser(req.Header.Get("User-Agent"))
+ if err == nil {
+ arg.Os = br.Platform().Name()
+ arg.Browser = br.Name()
+ }
+
+ c, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+
+ _ = m.biz.AuditBiz().Create(c, arg)
+}
diff --git a/internal/pkg/middleware/authorize.go b/internal/pkg/middleware/authorize.go
new file mode 100644
index 0000000..6fd1a5e
--- /dev/null
+++ b/internal/pkg/middleware/authorize.go
@@ -0,0 +1,81 @@
+package middleware
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+
+ "management/internal/db/model/dto"
+ "management/internal/global/auth"
+)
+
+var defaultMenus = map[string]bool{
+ "/home.html": true,
+ "/system/menus": true,
+ "/upload/img": true,
+ "/upload/file": true,
+ "/upload/mutilfile": true,
+ "/pear.json": true,
+}
+
+func (m *middleware) Authorize(next http.Handler) http.Handler {
+ fn := func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ user, ok := m.isLogin(ctx)
+ if !ok {
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+ }
+
+ if user == nil {
+ http.Error(w, "user not found", http.StatusUnauthorized)
+ return
+ }
+
+ // 登陆成功 判断权限
+
+ // 默认权限判断
+ path := r.URL.Path
+ if b, ok := defaultMenus[path]; ok && b {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ menus, err := m.biz.MenuBiz().MapOwnerMenuByRoleID(ctx, user.RoleID)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if _, ok := menus[path]; ok {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ }
+
+ return http.HandlerFunc(fn)
+}
+
+func (m *middleware) isLogin(ctx context.Context) (*dto.AuthorizeUser, bool) {
+ if exists := m.session.Exists(ctx, auth.StoreName); exists {
+ b := m.session.GetBytes(ctx, auth.StoreName)
+ var user dto.AuthorizeUser
+ if err := json.Unmarshal(b, &user); err != nil {
+ return nil, false
+ }
+ return &user, true
+ }
+
+ return nil, false
+}
+
+func (m *middleware) AuthUser(ctx context.Context) dto.AuthorizeUser {
+ var user dto.AuthorizeUser
+ if exists := m.session.Exists(ctx, auth.StoreName); exists {
+ b := m.session.GetBytes(ctx, auth.StoreName)
+ _ = json.Unmarshal(b, &user)
+ }
+ return user
+}
diff --git a/internal/pkg/middleware/middleware.go b/internal/pkg/middleware/middleware.go
new file mode 100644
index 0000000..bc51e37
--- /dev/null
+++ b/internal/pkg/middleware/middleware.go
@@ -0,0 +1,29 @@
+package middleware
+
+import (
+ "net/http"
+
+ systemv1 "management/internal/erpserver/biz/v1/system"
+ "management/internal/pkg/session"
+)
+
+type IMiddleware interface {
+ Audit(next http.Handler) http.Handler
+ NoSurf(next http.Handler) http.Handler
+ LoadSession(next http.Handler) http.Handler
+ Authorize(next http.Handler) http.Handler
+}
+
+type middleware struct {
+ biz systemv1.SystemBiz
+ session session.ISession
+}
+
+var _ IMiddleware = (*middleware)(nil)
+
+func New(biz systemv1.SystemBiz, session session.ISession) IMiddleware {
+ return &middleware{
+ biz: biz,
+ session: session,
+ }
+}
diff --git a/internal/pkg/middleware/nocsrf.go b/internal/pkg/middleware/nocsrf.go
new file mode 100644
index 0000000..2b098a8
--- /dev/null
+++ b/internal/pkg/middleware/nocsrf.go
@@ -0,0 +1,11 @@
+package middleware
+
+import (
+ "net/http"
+
+ "github.com/justinas/nosurf"
+)
+
+func (m *middleware) NoSurf(next http.Handler) http.Handler {
+ return nosurf.New(next)
+}
diff --git a/internal/pkg/middleware/session.go b/internal/pkg/middleware/session.go
new file mode 100644
index 0000000..07cf596
--- /dev/null
+++ b/internal/pkg/middleware/session.go
@@ -0,0 +1,9 @@
+package middleware
+
+import (
+ "net/http"
+)
+
+func (m *middleware) LoadSession(next http.Handler) http.Handler {
+ return m.session.LoadAndSave(next)
+}
diff --git a/internal/pkg/redis/redis.go b/internal/pkg/redis/redis.go
index 9fbbcfc..57b02d7 100644
--- a/internal/pkg/redis/redis.go
+++ b/internal/pkg/redis/redis.go
@@ -13,31 +13,42 @@ import (
"github.com/redis/go-redis/v9"
)
-var (
- engine *redis.Client
- ErrRedisKeyNotFound = errors.New("redis key not found")
-)
+var ErrRedisKeyNotFound = errors.New("redis key not found")
-// func GetRedis() *redis.Client {
-// return rd
-// }
+type IRedis interface {
+ Encode(a any) ([]byte, error)
+ Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
+ Del(ctx context.Context, keys ...string) error
+ Get(ctx context.Context, key string) (string, error)
+ GetBytes(ctx context.Context, key string) ([]byte, error)
+ Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd
+ Keys(ctx context.Context, pattern string) ([]string, error)
+ ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]string, int, error)
+}
-func Init() error {
+type redisCache struct {
+ engine *redis.Client
+}
+
+var _ IRedis = (*redisCache)(nil)
+
+func New(conf config.Redis) (*redisCache, error) {
rdb := redis.NewClient(&redis.Options{
- Addr: fmt.Sprintf("%s:%d", config.File.Redis.Host, config.File.Redis.Port),
- Password: config.File.Redis.Password,
- DB: config.File.Redis.DB,
+ Addr: fmt.Sprintf("%s:%d", conf.Host, conf.Port),
+ Password: conf.Password,
+ DB: conf.DB,
})
_, err := rdb.Ping(context.Background()).Result()
if err != nil {
- return err
+ return nil, err
}
- engine = rdb
- return nil
+ return &redisCache{
+ engine: rdb,
+ }, nil
}
-func Encode(a any) ([]byte, error) {
+func (r *redisCache) Encode(a any) ([]byte, error) {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(a); err != nil {
return nil, err
@@ -47,18 +58,18 @@ func Encode(a any) ([]byte, error) {
}
// Set 设置值
-func Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
- return engine.Set(ctx, key, value, expiration).Err()
+func (r *redisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
+ return r.engine.Set(ctx, key, value, expiration).Err()
}
// Del 删除键值
-func Del(ctx context.Context, keys ...string) error {
- return engine.Del(ctx, keys...).Err()
+func (r *redisCache) Del(ctx context.Context, keys ...string) error {
+ return r.engine.Del(ctx, keys...).Err()
}
// Get 获取值
-func Get(ctx context.Context, key string) (string, error) {
- val, err := engine.Get(ctx, key).Result()
+func (r *redisCache) Get(ctx context.Context, key string) (string, error) {
+ val, err := r.engine.Get(ctx, key).Result()
if err == redis.Nil {
return "", ErrRedisKeyNotFound
} else if err != nil {
@@ -69,8 +80,8 @@ func Get(ctx context.Context, key string) (string, error) {
}
// GetBytes 获取值
-func GetBytes(ctx context.Context, key string) ([]byte, error) {
- val, err := engine.Get(ctx, key).Bytes()
+func (r *redisCache) GetBytes(ctx context.Context, key string) ([]byte, error) {
+ val, err := r.engine.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, ErrRedisKeyNotFound
} else if err != nil {
@@ -80,16 +91,16 @@ func GetBytes(ctx context.Context, key string) ([]byte, error) {
}
}
-func Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd {
- return engine.Scan(ctx, cursor, match, count)
+func (r *redisCache) Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd {
+ return r.engine.Scan(ctx, cursor, match, count)
}
-func Keys(ctx context.Context, pattern string) ([]string, error) {
- return engine.Keys(ctx, pattern).Result()
+func (r *redisCache) Keys(ctx context.Context, pattern string) ([]string, error) {
+ return r.engine.Keys(ctx, pattern).Result()
}
-func ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]string, int, error) {
- all, err := engine.Keys(ctx, pattern).Result()
+func (r *redisCache) ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]string, int, error) {
+ all, err := r.engine.Keys(ctx, pattern).Result()
if err != nil {
return nil, 0, err
}
@@ -104,7 +115,7 @@ func ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]
for {
var scanResult []string
var err error
- scanResult, cursor, err = engine.Scan(ctx, cursor, pattern, int64(pageSize)).Result()
+ scanResult, cursor, err = r.engine.Scan(ctx, cursor, pattern, int64(pageSize)).Result()
if err != nil {
return nil, count, err
}
@@ -124,3 +135,36 @@ func ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]
}
return keys[startIndex:endIndex], count, nil
}
+
+// ==========================
+func Encode(a any) ([]byte, error) {
+ return nil, nil
+}
+
+func Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
+ return nil
+}
+
+func Del(ctx context.Context, keys ...string) error {
+ return nil
+}
+
+func Get(ctx context.Context, key string) (string, error) {
+ return "", nil
+}
+
+func GetBytes(ctx context.Context, key string) ([]byte, error) {
+ return nil, nil
+}
+
+func Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd {
+ return nil
+}
+
+func Keys(ctx context.Context, pattern string) ([]string, error) {
+ return nil, nil
+}
+
+func ListKeys(ctx context.Context, pattern string, pageID int, pageSize int) ([]string, int, error) {
+ return nil, 0, nil
+}
diff --git a/internal/pkg/session/redis.go b/internal/pkg/session/redis.go
index 6c9eb0f..3ffb127 100644
--- a/internal/pkg/session/redis.go
+++ b/internal/pkg/session/redis.go
@@ -1,83 +1,83 @@
package session
-import (
- "context"
- "time"
+// import (
+// "context"
+// "time"
- "management/internal/pkg/redis"
-)
+// "management/internal/pkg/redis"
+// )
-var (
- storePrefix = "scs:session:"
- ctx = context.Background()
- DefaultRedisStore = newRedisStore()
-)
+// var (
+// storePrefix = "scs:session:"
+// ctx = context.Background()
+// DefaultRedisStore = newRedisStore()
+// )
-type redisStore struct{}
+// type redisStore struct{}
-func newRedisStore() *redisStore {
- return &redisStore{}
-}
+// func newRedisStore() *redisStore {
+// return &redisStore{}
+// }
-// Delete should remove the session token and corresponding data from the
-// session store. If the token does not exist then Delete should be a no-op
-// and return nil (not an error).
-func (s *redisStore) Delete(token string) error {
- return redis.Del(ctx, storePrefix+token)
-}
+// // Delete should remove the session token and corresponding data from the
+// // session store. If the token does not exist then Delete should be a no-op
+// // and return nil (not an error).
+// func (s *redisStore) Delete(token string) error {
+// return redis.Del(ctx, storePrefix+token)
+// }
-// Find should return the data for a session token from the store. If the
-// session token is not found or is expired, the found return value should
-// be false (and the err return value should be nil). Similarly, tampered
-// or malformed tokens should result in a found return value of false and a
-// nil err value. The err return value should be used for system errors only.
-func (s *redisStore) Find(token string) (b []byte, found bool, err error) {
- val, err := redis.GetBytes(ctx, storePrefix+token)
- if err != nil {
- return nil, false, err
- } else {
- return val, true, nil
- }
-}
+// // Find should return the data for a session token from the store. If the
+// // session token is not found or is expired, the found return value should
+// // be false (and the err return value should be nil). Similarly, tampered
+// // or malformed tokens should result in a found return value of false and a
+// // nil err value. The err return value should be used for system errors only.
+// func (s *redisStore) Find(token string) (b []byte, found bool, err error) {
+// val, err := redis.GetBytes(ctx, storePrefix+token)
+// if err != nil {
+// return nil, false, err
+// } else {
+// return val, true, nil
+// }
+// }
-// Commit should add the session token and data to the store, with the given
-// expiry time. If the session token already exists, then the data and
-// expiry time should be overwritten.
-func (s *redisStore) Commit(token string, b []byte, expiry time.Time) error {
- // TODO: 这边可以调整时间
- exp, err := time.ParseInLocation(time.DateTime, time.Now().Format("2006-01-02")+" 23:59:59", time.Local)
- if err != nil {
- return err
- }
+// // Commit should add the session token and data to the store, with the given
+// // expiry time. If the session token already exists, then the data and
+// // expiry time should be overwritten.
+// func (s *redisStore) Commit(token string, b []byte, expiry time.Time) error {
+// // TODO: 这边可以调整时间
+// exp, err := time.ParseInLocation(time.DateTime, time.Now().Format("2006-01-02")+" 23:59:59", time.Local)
+// if err != nil {
+// return err
+// }
- t := time.Now()
- expired := exp.Sub(t)
- return redis.Set(ctx, storePrefix+token, b, expired)
-}
+// t := time.Now()
+// expired := exp.Sub(t)
+// return redis.Set(ctx, storePrefix+token, b, expired)
+// }
-// All should return a map containing data for all active sessions (i.e.
-// sessions which have not expired). The map key should be the session
-// token and the map value should be the session data. If no active
-// sessions exist this should return an empty (not nil) map.
-func (s *redisStore) All() (map[string][]byte, error) {
- sessions := make(map[string][]byte)
+// // All should return a map containing data for all active sessions (i.e.
+// // sessions which have not expired). The map key should be the session
+// // token and the map value should be the session data. If no active
+// // sessions exist this should return an empty (not nil) map.
+// func (s *redisStore) All() (map[string][]byte, error) {
+// sessions := make(map[string][]byte)
- iter := redis.Scan(ctx, 0, storePrefix+"*", 0).Iterator()
- for iter.Next(ctx) {
- key := iter.Val()
- token := key[len(storePrefix):]
- data, exists, err := s.Find(token)
- if err != nil {
- return nil, err
- }
+// iter := redis.Scan(ctx, 0, storePrefix+"*", 0).Iterator()
+// for iter.Next(ctx) {
+// key := iter.Val()
+// token := key[len(storePrefix):]
+// data, exists, err := s.Find(token)
+// if err != nil {
+// return nil, err
+// }
- if exists {
- sessions[token] = data
- }
- }
- if err := iter.Err(); err != nil {
- return nil, err
- }
+// if exists {
+// sessions[token] = data
+// }
+// }
+// if err := iter.Err(); err != nil {
+// return nil, err
+// }
- return sessions, nil
-}
+// return sessions, nil
+// }
diff --git a/internal/pkg/session/session.go b/internal/pkg/session/session.go
index 0808429..f54e11d 100644
--- a/internal/pkg/session/session.go
+++ b/internal/pkg/session/session.go
@@ -5,55 +5,68 @@ import (
"net/http"
"time"
- "management/internal/config"
- db "management/internal/db/sqlc"
-
"github.com/alexedwards/scs/pgxstore"
"github.com/alexedwards/scs/v2"
+ "github.com/jackc/pgx/v5/pgxpool"
)
-var sessionManager *scs.SessionManager
+type ISession interface {
+ Destroy(ctx context.Context) error
+ LoadAndSave(next http.Handler) http.Handler
+ Put(ctx context.Context, key string, val any)
+ GetBytes(ctx context.Context, key string) []byte
+ Exists(ctx context.Context, key string) bool
+ RenewToken(ctx context.Context) error
+}
-func Init() {
- sessionManager = scs.New()
+type session struct {
+ sessionManager *scs.SessionManager
+}
+
+func New(pool *pgxpool.Pool, prod bool) ISession {
+ sessionManager := scs.New()
sessionManager.Lifetime = 24 * time.Hour
sessionManager.IdleTimeout = 2 * time.Hour
sessionManager.Cookie.Name = "token"
sessionManager.Cookie.HttpOnly = true
sessionManager.Cookie.Persist = true
sessionManager.Cookie.SameSite = http.SameSiteStrictMode
- sessionManager.Cookie.Secure = config.File.App.Prod
+ sessionManager.Cookie.Secure = prod
// postgres
// github.com/alexedwards/scs/postgresstore
// sessionManager.Store = postgresstore.New(db)
// pgx
// github.com/alexedwards/scs/pgxstore
- sessionManager.Store = pgxstore.New(db.Engine.Pool())
+ sessionManager.Store = pgxstore.New(pool)
// redis
// sessionManager.Store = newRedisStore()
+
+ return &session{
+ sessionManager: sessionManager,
+ }
}
-func Destroy(ctx context.Context) error {
- return sessionManager.Destroy(ctx)
+func (s *session) Destroy(ctx context.Context) error {
+ return s.sessionManager.Destroy(ctx)
}
-func LoadAndSave(next http.Handler) http.Handler {
- return sessionManager.LoadAndSave(next)
+func (s *session) LoadAndSave(next http.Handler) http.Handler {
+ return s.sessionManager.LoadAndSave(next)
}
-func Put(ctx context.Context, key string, val interface{}) {
- sessionManager.Put(ctx, key, val)
+func (s *session) Put(ctx context.Context, key string, val any) {
+ s.sessionManager.Put(ctx, key, val)
}
-func GetBytes(ctx context.Context, key string) []byte {
- return sessionManager.GetBytes(ctx, key)
+func (s *session) GetBytes(ctx context.Context, key string) []byte {
+ return s.sessionManager.GetBytes(ctx, key)
}
-func Exists(ctx context.Context, key string) bool {
- return sessionManager.Exists(ctx, key)
+func (s *session) Exists(ctx context.Context, key string) bool {
+ return s.sessionManager.Exists(ctx, key)
}
-func RenewToken(ctx context.Context) error {
- return sessionManager.RenewToken(ctx)
+func (s *session) RenewToken(ctx context.Context) error {
+ return s.sessionManager.RenewToken(ctx)
}
diff --git a/internal/pkg/tpl/html.go b/internal/pkg/tpl/html.go
new file mode 100644
index 0000000..8522dbb
--- /dev/null
+++ b/internal/pkg/tpl/html.go
@@ -0,0 +1,48 @@
+package tpl
+
+import (
+ "bytes"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "management/internal/db/model/dto"
+)
+
+type TemplateConfig struct {
+ Root string
+ Extension string
+ Layout string
+ Partial string
+}
+
+type HtmlData struct {
+ IsAuthenticated bool
+ AuthorizeUser dto.AuthorizeUser
+ AuthorizeMenus []*dto.OwnerMenuDto
+ Data any
+}
+
+func (r *render) HTML(w http.ResponseWriter, req *http.Request, tpl string, data map[string]any) {
+ name := strings.ReplaceAll(tpl, "/", "_")
+ t, ok := r.templates[name]
+ if !ok {
+ http.Error(w, "template is empty", http.StatusInternalServerError)
+ return
+ }
+
+ hd := r.setDefaultData(req, data)
+
+ buf := new(bytes.Buffer)
+ err := t.ExecuteTemplate(buf, filepath.Base(tpl), hd)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ _, err = buf.WriteTo(w)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/internal/pkg/tpl/html_btn.go b/internal/pkg/tpl/html_btn.go
new file mode 100644
index 0000000..9ee3b97
--- /dev/null
+++ b/internal/pkg/tpl/html_btn.go
@@ -0,0 +1,91 @@
+package tpl
+
+import (
+ "html/template"
+ "path/filepath"
+ "strings"
+
+ "management/internal/db/model/dto"
+)
+
+func (r *render) btnFuncs() map[string]any {
+ res := make(map[string]any, 3)
+
+ res["genBtn"] = func(btns []*dto.OwnerMenuDto, actionNames ...string) template.HTML {
+ if len(btns) == 0 {
+ return template.HTML("")
+ }
+
+ var res string
+ for _, action := range actionNames {
+ for _, btn := range btns {
+ btn.Style = strings.ReplaceAll(btn.Style, "pear", "layui")
+ base := filepath.Base(btn.Url)
+ if base == action {
+ res += ``
+ }
+ }
+ }
+
+ return template.HTML(res)
+ }
+
+ res["genLink"] = func(btns []*dto.OwnerMenuDto, actionNames ...string) template.HTML {
+ if len(btns) == 0 {
+ return template.HTML("")
+ }
+
+ var res string
+ for _, action := range actionNames {
+ for _, btn := range btns {
+ btn.Style = strings.ReplaceAll(btn.Style, "pear", "layui")
+ base := filepath.Base(btn.Url)
+ if base == action {
+ res += ``
+ }
+ }
+ }
+
+ return template.HTML(res)
+ }
+
+ res["previewPicture"] = func(name string) template.HTML {
+ var res string
+ res += ``
+
+ return template.HTML(res)
+ }
+
+ res["submitBtn"] = func(btns []*dto.OwnerMenuDto, actionNames ...string) template.HTML {
+ if len(btns) == 0 {
+ return template.HTML("")
+ }
+
+ var res string
+ for _, action := range actionNames {
+ for _, btn := range btns {
+ btn.Style = strings.ReplaceAll(btn.Style, "pear", "layui")
+ base := filepath.Base(btn.Url)
+ if base == action {
+ res += ``
+ }
+ }
+ }
+
+ return template.HTML(res)
+ }
+
+ return res
+}
diff --git a/internal/pkg/tpl/html_method.go b/internal/pkg/tpl/html_method.go
new file mode 100644
index 0000000..36accad
--- /dev/null
+++ b/internal/pkg/tpl/html_method.go
@@ -0,0 +1,57 @@
+package tpl
+
+import (
+ "html/template"
+ "strings"
+ "time"
+)
+
+func (r *render) Methods() map[string]any {
+ res := make(map[string]any, 1)
+
+ res["dateFormat"] = func(dt time.Time) template.HTML {
+ return template.HTML(dt.Format(time.DateTime))
+ }
+
+ res["today"] = func() template.HTML {
+ return template.HTML(time.Now().Format("2006-01-02"))
+ }
+
+ res["threeMonth"] = func() template.HTML {
+ return template.HTML(time.Now().AddDate(0, 3, 0).Format("2006-01-02"))
+ }
+
+ res["yearBegin"] = func() template.HTML {
+ dt := time.Now()
+ t := dt.AddDate(0, -int(dt.Month())+1, -dt.Day()+1)
+ return template.HTML(t.Format("2006-01-02") + " 00:00:00")
+ }
+
+ res["monthBegin"] = func() template.HTML {
+ dt := time.Now()
+ t := dt.AddDate(0, 0, -dt.Day()+1)
+ return template.HTML(t.Format("2006-01-02") + " 00:00:00")
+ }
+
+ res["monthEnd"] = func() template.HTML {
+ dt := time.Now()
+ t := dt.AddDate(0, 0, -dt.Day()+1).AddDate(0, 1, -1)
+ return template.HTML(t.Format("2006-01-02") + " 23:59:59")
+ }
+
+ res["trimSpace"] = func(s string) template.HTML {
+ return template.HTML(strings.TrimSpace(s))
+ }
+
+ res["expandTags"] = func(s []string) template.HTML {
+ if len(s) == 0 {
+ return template.HTML("")
+ }
+ if len(s) == 1 && s[0] == "all" {
+ return template.HTML("")
+ }
+ return template.HTML(strings.Join(s, ","))
+ }
+
+ return res
+}
diff --git a/internal/pkg/tpl/json.go b/internal/pkg/tpl/json.go
new file mode 100644
index 0000000..8d8ed4e
--- /dev/null
+++ b/internal/pkg/tpl/json.go
@@ -0,0 +1,57 @@
+package tpl
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+type Response struct {
+ Success bool `json:"success"`
+ Message string `json:"msg"`
+ Data any `json:"data"`
+}
+
+type ResponseDtree struct {
+ Status ResponseDtreeStatus `json:"status"`
+ Data any `json:"data"`
+}
+
+type ResponseDtreeStatus struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+}
+
+type ResponseList struct {
+ Code int `json:"code"`
+ Message string `json:"msg"`
+ Count int64 `json:"count"`
+ Data any `json:"data"`
+}
+
+func (r *render) JSONF(w http.ResponseWriter, success bool, message string) {
+ r.JSON(w, Response{Success: success, Message: message})
+}
+
+func (r *render) JSONOK(w http.ResponseWriter, message string) {
+ r.JSON(w, Response{Success: true, Message: message})
+}
+
+func (r *render) JSONERR(w http.ResponseWriter, message string) {
+ r.JSON(w, Response{Success: false, Message: message})
+}
+
+func (r *render) JSON(w http.ResponseWriter, data any) {
+ v, err := json.Marshal(data)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, err = w.Write(v)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/internal/pkg/tpl/render.go b/internal/pkg/tpl/render.go
new file mode 100644
index 0000000..3ba1a09
--- /dev/null
+++ b/internal/pkg/tpl/render.go
@@ -0,0 +1,46 @@
+package tpl
+
+import (
+ "html/template"
+ "net/http"
+
+ systemv1 "management/internal/erpserver/biz/v1/system"
+ "management/internal/pkg/session"
+)
+
+type Renderer interface {
+ HTML(w http.ResponseWriter, req *http.Request, name string, data map[string]any)
+ JSON(w http.ResponseWriter, data any)
+ JSONF(w http.ResponseWriter, success bool, message string)
+ JSONOK(w http.ResponseWriter, message string)
+ JSONERR(w http.ResponseWriter, message string)
+}
+
+type render struct {
+ session session.ISession
+ config *TemplateConfig
+ templates map[string]*template.Template
+
+ menuBiz systemv1.MenuBiz
+}
+
+func New(session session.ISession, menuBiz systemv1.MenuBiz) (Renderer, error) {
+ render := &render{
+ session: session,
+ menuBiz: menuBiz,
+ config: &TemplateConfig{
+ Root: ".",
+ Extension: ".tmpl",
+ Layout: "base",
+ Partial: "partial",
+ },
+ }
+
+ templates, err := render.createTemplateCache()
+ if err != nil {
+ return nil, err
+ }
+
+ render.templates = templates
+ return render, nil
+}
diff --git a/internal/pkg/tpl/util.go b/internal/pkg/tpl/util.go
new file mode 100644
index 0000000..1c7be4c
--- /dev/null
+++ b/internal/pkg/tpl/util.go
@@ -0,0 +1,181 @@
+package tpl
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "io/fs"
+ "net/http"
+ "os"
+ "path/filepath"
+ "slices"
+ "strings"
+
+ "management/internal/db/model/dto"
+ "management/internal/global/auth"
+ templates "management/web/templates/manage"
+
+ "github.com/justinas/nosurf"
+)
+
+func (r *render) setDefaultData(req *http.Request, data map[string]any) map[string]any {
+ if data == nil {
+ data = make(map[string]any)
+ }
+
+ ctx := req.Context()
+ isAuth := r.session.Exists(ctx, auth.StoreName)
+ data["IsAuthenticated"] = isAuth
+ if isAuth {
+ var authUser dto.AuthorizeUser
+ u := r.session.GetBytes(ctx, auth.StoreName)
+ _ = json.Unmarshal(u, &authUser)
+
+ data["AuthorizeMenus"] = r.getCurrentPathBtns(ctx, authUser.RoleID, req.URL.Path)
+ }
+ token := nosurf.Token(req)
+ data["CsrfToken"] = token
+ data["CsrfTokenField"] = template.HTML(fmt.Sprintf(``, token))
+
+ return data
+}
+
+func (r *render) getCurrentPathBtns(ctx context.Context, roleID int32, path string) []*dto.OwnerMenuDto {
+ var res []*dto.OwnerMenuDto
+
+ // 获取当前path的菜单
+ menu, err := r.menuBiz.GetSysMenuByUrl(ctx, path)
+ if err != nil {
+ return res
+ }
+
+ // 获取权限
+ menus, err := r.menuBiz.ListOwnerMenuByRoleID(ctx, roleID)
+ if err != nil {
+ return res
+ }
+
+ for _, item := range menus {
+ if menu.IsList {
+ if item.ParentID == menu.ID || item.ID == menu.ID {
+ res = append(res, item)
+ }
+ } else {
+ if item.ParentID == menu.ParentID {
+ res = append(res, item)
+ }
+ }
+ }
+
+ return res
+}
+
+func (r *render) createTemplateCache() (map[string]*template.Template, error) {
+ cache := make(map[string]*template.Template)
+ pages, err := getFiles(r.config.Root, r.config.Extension)
+ if err != nil {
+ return nil, err
+ }
+
+ layoutAndPartial, err := r.getLayoutAndPartials()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, page := range pages {
+ if strings.HasPrefix(page, "base") || strings.HasSuffix(page, "partial") {
+ continue
+ }
+
+ name := filepath.Base(page)
+ pathArr := strings.Split(page, "/")
+ dir := pathArr[len(pathArr)-2 : len(pathArr)-1]
+ templateName := fmt.Sprintf("%s_%s", dir[0], name)
+ ts := template.Must(template.New(templateName).Funcs(r.btnFuncs()).Funcs(r.Methods()).ParseFS(templates.TemplateFS, page))
+ if err != nil {
+ return nil, err
+ }
+
+ ts, err = ts.ParseFS(templates.TemplateFS, layoutAndPartial...)
+ if err != nil {
+ return nil, err
+ }
+
+ cache[templateName] = ts
+ }
+
+ return cache, nil
+}
+
+func (r *render) getLayoutAndPartials() ([]string, error) {
+ layouts, err := getFiles(r.config.Layout, r.config.Extension)
+ if err != nil {
+ return nil, err
+ }
+
+ partials, err := getFiles(r.config.Partial, r.config.Extension)
+ if err != nil {
+ return nil, err
+ }
+
+ return slices.Concat(layouts, partials), nil
+}
+
+func getFiles(path string, stuffix string) ([]string, error) {
+ files := make([]string, 0)
+ b, err := pathExists(templates.TemplateFS, path)
+ if err != nil {
+ return nil, err
+ }
+
+ if !b {
+ return files, nil
+ }
+
+ err = fs.WalkDir(templates.TemplateFS, path, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ if strings.HasSuffix(path, stuffix) {
+ files = append(files, path)
+ }
+ return nil
+ })
+ // err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
+ // if info == nil {
+ // return err
+ // }
+ // if info.IsDir() {
+ // return nil
+ // }
+ // // 将模板后缀的文件放到列表
+ // if strings.HasSuffix(path, stuffix) {
+ // files = append(files, path)
+ // }
+ // return nil
+ // })
+ return files, err
+}
+
+func pathExists(fs fs.FS, path string) (bool, error) {
+ _, err := fs.Open(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+ }
+ return true, err
+}
+
+func firstLower(s string) string {
+ if len(s) == 0 {
+ return s
+ }
+
+ return strings.ToLower(s[:1]) + s[1:]
+}
diff --git a/internal/router/manage/category/category.go b/internal/router/manage/category/category.go
index 0c0e8e1..9c5576b 100644
--- a/internal/router/manage/category/category.go
+++ b/internal/router/manage/category/category.go
@@ -8,6 +8,7 @@ import (
"management/internal/db/model/dto"
db "management/internal/db/sqlc"
+ "management/internal/pkg/convertor"
"management/internal/router/manage/util"
categoryservice "management/internal/service/category"
"management/internal/tpl"
@@ -28,12 +29,12 @@ func (h *CategoryHandler) List(w http.ResponseWriter, r *http.Request) {
func (h *CategoryHandler) PostList(w http.ResponseWriter, r *http.Request) {
var q dto.SearchDto
- q.SearchStatus = util.ConvertInt(r.PostFormValue("SearchStatus"), 9999)
- q.SearchParentID = util.ConvertInt(r.PostFormValue("SearchParentID"), 0)
- q.SearchName = r.PostFormValue("SearchName")
- q.SearchKey = r.PostFormValue("SearchKey")
- q.Page = util.ConvertInt(r.PostFormValue("page"), 1)
- q.Rows = util.ConvertInt(r.PostFormValue("rows"), 10)
+ q.SearchStatus = convertor.ConvertInt(r.PostFormValue("status"), 9999)
+ q.SearchParentID = convertor.ConvertInt(r.PostFormValue("parentId"), 0)
+ q.SearchName = r.PostFormValue("name")
+ q.SearchID = convertor.ConvertInt[int64](r.PostFormValue("id"), 0)
+ q.Page = convertor.ConvertInt(r.PostFormValue("page"), 1)
+ q.Rows = convertor.ConvertInt(r.PostFormValue("rows"), 10)
res, count, err := categoryservice.ListCategoriesCondition(r.Context(), q)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -194,19 +195,32 @@ func (h *CategoryHandler) DTree(w http.ResponseWriter, r *http.Request) {
}
func (h *CategoryHandler) XmSelect(w http.ResponseWriter, r *http.Request) {
- all, err := categoryservice.ListByLetter(r.Context(), r.URL.Query().Get("letter"))
+ letter := r.URL.Query().Get("letter")
+ ctx := r.Context()
+ if len(letter) > 0 {
+ all, err := categoryservice.ListByLetter(ctx, letter)
+ if err != nil {
+ tpl.JSONERR(w, err.Error())
+ return
+ }
+
+ var res []*dto.XmSelectStrDto
+ for _, v := range all {
+ res = append(res, &dto.XmSelectStrDto{
+ Name: v.Name,
+ Value: strconv.FormatInt(int64(v.ID), 10),
+ })
+ }
+ tpl.JSON(w, res)
+ return
+ }
+
+ res, err := categoryservice.XmSelectCategory(ctx, 0)
if err != nil {
tpl.JSONERR(w, err.Error())
return
}
- var res []*dto.XmSelectStrDto
- for _, v := range all {
- res = append(res, &dto.XmSelectStrDto{
- Name: v.Name,
- Value: strconv.FormatInt(int64(v.ID), 10),
- })
- }
tpl.JSON(w, res)
}
diff --git a/internal/router/manage/oauth/oauth.go b/internal/router/manage/oauth/oauth.go
index cb1b998..e95d948 100644
--- a/internal/router/manage/oauth/oauth.go
+++ b/internal/router/manage/oauth/oauth.go
@@ -8,9 +8,7 @@ import (
"management/internal/db/model/dto"
db "management/internal/db/sqlc"
- authglobal "management/internal/global/auth"
"management/internal/pkg/crypto"
- "management/internal/pkg/session"
captchaservice "management/internal/service/captcha"
systemservice "management/internal/service/system"
"management/internal/tpl"
@@ -19,19 +17,19 @@ import (
)
func Login(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- var user dto.AuthorizeUser
- u := session.GetBytes(ctx, authglobal.StoreName)
- if err := json.Unmarshal(u, &user); err == nil {
- // 判断租户是否一致, 一致则刷新令牌,跳转到首页
- if err := session.RenewToken(ctx); err == nil {
- session.Put(ctx, authglobal.StoreName, u)
- http.Redirect(w, r, "/home.html", http.StatusFound)
- return
- }
- }
+ // ctx := r.Context()
+ // var user dto.AuthorizeUser
+ // u := session.GetBytes(ctx, authglobal.StoreName)
+ // if err := json.Unmarshal(u, &user); err == nil {
+ // // 判断租户是否一致, 一致则刷新令牌,跳转到首页
+ // if err := session.RenewToken(ctx); err == nil {
+ // session.Put(ctx, authglobal.StoreName, u)
+ // http.Redirect(w, r, "/home.html", http.StatusFound)
+ // return
+ // }
+ // }
- session.Destroy(ctx)
+ // session.Destroy(ctx)
tpl.HTML(w, r, "oauth/login.tmpl", nil)
}
@@ -122,7 +120,7 @@ func PostLogin(w http.ResponseWriter, r *http.Request) {
Browser: log.Browser,
}
- b, err := json.Marshal(auth)
+ _, err = json.Marshal(auth)
if err != nil {
log.Message = err.Error()
_ = systemservice.CreateSysUserLoginLog(ctx, log)
@@ -130,15 +128,15 @@ func PostLogin(w http.ResponseWriter, r *http.Request) {
return
}
- session.Put(ctx, authglobal.StoreName, b)
+ // session.Put(ctx, authglobal.StoreName, b)
log.IsSuccess = true
log.Message = "登陆成功"
_ = systemservice.CreateSysUserLoginLog(ctx, log)
- tpl.JSON(w, tpl.Response{Success: true, Message: "login successful"})
+ tpl.JSONOK(w, "login successful")
}
func Logout(w http.ResponseWriter, r *http.Request) {
- session.Destroy(r.Context())
+ // session.Destroy(r.Context())
http.Redirect(w, r, "/", http.StatusFound)
}
diff --git a/internal/router/manage/router.go b/internal/router/manage/router.go
index 3c03e4f..1c6fcbf 100644
--- a/internal/router/manage/router.go
+++ b/internal/router/manage/router.go
@@ -6,7 +6,6 @@ import (
"management/internal/middleware/manage/audit"
"management/internal/middleware/manage/auth"
"management/internal/middleware/manage/nosurf"
- "management/internal/middleware/manage/session"
budgethandler "management/internal/router/manage/budget"
cachehandler "management/internal/router/manage/cache"
@@ -38,8 +37,8 @@ func NewRouter() *chi.Mux {
r.Handle("/upload/*", http.StripPrefix("/upload", uploadServer))
r.Group(func(r chi.Router) {
- r.Use(nosurf.NoSurf) // CSRF
- r.Use(session.LoadSession) // Session
+ r.Use(nosurf.NoSurf) // CSRF
+ // r.Use(session.LoadSession) // Session
r.Get("/captcha", commonhandler.Captcha)
diff --git a/internal/router/manage/system/sys_menu.go b/internal/router/manage/system/sys_menu.go
index 03a78c0..bc07ccd 100644
--- a/internal/router/manage/system/sys_menu.go
+++ b/internal/router/manage/system/sys_menu.go
@@ -1,16 +1,12 @@
package system
import (
- "encoding/json"
"net/http"
"strconv"
"strings"
"time"
- "management/internal/db/model/dto"
db "management/internal/db/sqlc"
- "management/internal/global/auth"
- "management/internal/pkg/session"
"management/internal/router/manage/util"
systemservice "management/internal/service/system"
"management/internal/tpl"
@@ -189,20 +185,20 @@ func (h *SysMenuHandler) Save(w http.ResponseWriter, r *http.Request) {
}
func (h *SysMenuHandler) UserMenus(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- b := session.GetBytes(ctx, auth.StoreName)
- var u dto.AuthorizeUser
- if err := json.Unmarshal(b, &u); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- menus, err := systemservice.RecursiveSysMenus(ctx, u.RoleID)
- if err != nil {
- tpl.JSON(w, tpl.Response{Success: false, Message: err.Error()})
- return
- }
+ // ctx := r.Context()
+ // b := session.GetBytes(ctx, auth.StoreName)
+ // var u dto.AuthorizeUser
+ // if err := json.Unmarshal(b, &u); err != nil {
+ // http.Error(w, err.Error(), http.StatusInternalServerError)
+ // return
+ // }
+ // menus, err := systemservice.RecursiveSysMenus(ctx, u.RoleID)
+ // if err != nil {
+ // tpl.JSON(w, tpl.Response{Success: false, Message: err.Error()})
+ // return
+ // }
- tpl.JSON(w, menus)
+ tpl.JSON(w, nil)
}
func (h *SysMenuHandler) XmSelectTree(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/router/manage/system/sys_role.go b/internal/router/manage/system/sys_role.go
index fee9e4c..ad99603 100644
--- a/internal/router/manage/system/sys_role.go
+++ b/internal/router/manage/system/sys_role.go
@@ -8,6 +8,7 @@ import (
"management/internal/db/model/dto"
db "management/internal/db/sqlc"
+ "management/internal/pkg/convertor"
"management/internal/router/manage/util"
systemservice "management/internal/service/system"
"management/internal/tpl"
@@ -25,14 +26,13 @@ func (h *SysRoleHandler) List(w http.ResponseWriter, r *http.Request) {
func (h *SysRoleHandler) PostList(w http.ResponseWriter, r *http.Request) {
var q dto.SearchDto
- q.SearchStatus = util.ConvertInt(r.PostFormValue("SearchStatus"), 9999)
- q.SearchParentID = util.ConvertInt(r.PostFormValue("SearchParentID"), 0)
- q.SearchName = r.PostFormValue("SearchName")
- q.SearchKey = r.PostFormValue("SearchKey")
- q.Page = util.ConvertInt(r.PostFormValue("page"), 1)
- q.Rows = util.ConvertInt(r.PostFormValue("rows"), 10)
- ctx := r.Context()
- res, count, err := systemservice.ListSysRoleCondition(ctx, q)
+ q.SearchStatus = convertor.ConvertInt(r.PostFormValue("status"), 9999)
+ q.SearchParentID = convertor.ConvertInt(r.PostFormValue("parentId"), 0)
+ q.SearchName = r.PostFormValue("name")
+ q.SearchID = convertor.ConvertInt[int64](r.PostFormValue("id"), 0)
+ q.Page = convertor.ConvertInt(r.PostFormValue("page"), 1)
+ q.Rows = convertor.ConvertInt(r.PostFormValue("rows"), 10)
+ res, count, err := systemservice.ListSysRoleCondition(r.Context(), q)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -141,7 +141,7 @@ func (h *SysRoleHandler) Save(w http.ResponseWriter, r *http.Request) {
func (h *SysRoleHandler) XmSelect(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
- res, err := systemservice.XmSelectSysRole(ctx)
+ res, err := systemservice.XmSelectSysRole(ctx, 0)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
diff --git a/internal/service/category/category.go b/internal/service/category/category.go
index ec64363..82bd69e 100644
--- a/internal/service/category/category.go
+++ b/internal/service/category/category.go
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"strconv"
- "strings"
"time"
"management/internal/db/model/dto"
@@ -19,35 +18,25 @@ func ListCategoriesCondition(ctx context.Context, q dto.SearchDto) ([]*db.Catego
countArg := &db.CountCategoriesConditionParams{
IsStatus: q.SearchStatus != 9999,
Status: int16(q.SearchStatus),
- IsParentID: q.SearchParentID != 0,
+ IsID: q.SearchID != 0,
+ ID: int32(q.SearchID),
+ IsParentID: q.SearchParentID != 0 && q.SearchParentID != 1,
ParentID: int32(q.SearchParentID),
+ Name: q.SearchName,
}
dataArg := &db.ListCategoriesConditionParams{
IsStatus: q.SearchStatus != 9999,
Status: int16(q.SearchStatus),
- IsParentID: q.SearchParentID != 0,
+ IsID: q.SearchID != 0,
+ ID: int32(q.SearchID),
+ IsParentID: q.SearchParentID != 0 && q.SearchParentID != 1,
ParentID: int32(q.SearchParentID),
+ Name: q.SearchName,
Skip: (int32(q.Page) - 1) * int32(q.Rows),
Size: int32(q.Rows),
}
- if len(q.SearchKey) > 0 {
- switch strings.ToLower(q.SearchName) {
- case "id":
- id, err := strconv.Atoi(q.SearchKey)
- if err == nil {
- countArg.IsID = true
- countArg.ID = int32(id)
-
- dataArg.IsID = true
- dataArg.ID = int32(id)
- }
- case "name":
- countArg.Name = q.SearchKey
- dataArg.Name = q.SearchKey
- }
- }
count, err := db.Engine.CountCategoriesCondition(ctx, countArg)
if err != nil {
return nil, 0, err
@@ -70,6 +59,15 @@ func DTreeCategory(ctx context.Context, id int32) ([]*dto.DTreeDto, error) {
return toDtree(id, all), nil
}
+func XmSelectCategory(ctx context.Context, id int32) ([]*dto.XmSelectTreeDto, error) {
+ all, err := db.Engine.AllCategories(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return toXmSelectTree(id, all), nil
+}
+
func GetParentCategorySelectLetter(ctx context.Context, letter string) ([]*global.DataDict, error) {
all := AllCategories(ctx)
if len(all) == 0 {
@@ -178,6 +176,25 @@ func toDtree(parentId int32, data []*db.Category) []*dto.DTreeDto {
item.Last = !hasChildren(v.ID, data)
item.ParentId = strconv.FormatInt(int64(v.ParentID), 10)
item.Children = toDtree(v.ID, data)
+ if v.ParentID == 0 {
+ item.Spread = true
+ }
+ res = append(res, &item)
+ }
+ }
+
+ return res
+}
+
+func toXmSelectTree(parentId int32, data []*db.Category) []*dto.XmSelectTreeDto {
+ var res []*dto.XmSelectTreeDto
+ for _, v := range data {
+ if v.ParentID == parentId {
+ item := dto.XmSelectTreeDto{
+ Name: v.Name,
+ Value: strconv.FormatInt(int64(v.ID), 10),
+ Children: toXmSelectTree(v.ID, data),
+ }
res = append(res, &item)
}
}
diff --git a/internal/service/system/sys_role.go b/internal/service/system/sys_role.go
index 8a91772..4f99ea1 100644
--- a/internal/service/system/sys_role.go
+++ b/internal/service/system/sys_role.go
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"strconv"
- "strings"
"time"
"management/internal/db/model/dto"
@@ -27,36 +26,25 @@ func GetSysRole(ctx context.Context, id int32) (*db.SysRole, error) {
func ListSysRoleCondition(ctx context.Context, q dto.SearchDto) ([]*db.SysRole, int64, error) {
countArg := &db.CountSysRoleConditionParams{
- IsStatus: q.SearchStatus != 9999,
- Status: int32(q.SearchStatus),
- IsParentID: q.SearchParentID != 0,
- ParentID: int32(q.SearchParentID),
+ IsStatus: q.SearchStatus != 9999,
+ Status: int32(q.SearchStatus),
+ IsID: q.SearchID != 0,
+ ID: int32(q.SearchID),
+ IsParentID: q.SearchParentID != 0,
+ ParentID: int32(q.SearchParentID),
+ DisplayName: q.SearchName,
}
dataArg := &db.ListSysRoleConditionParams{
- IsStatus: q.SearchStatus != 9999,
- Status: int32(q.SearchStatus),
- IsParentID: q.SearchParentID != 0,
- ParentID: int32(q.SearchParentID),
- Skip: (int32(q.Page) - 1) * int32(q.Rows),
- Size: int32(q.Rows),
- }
-
- if len(q.SearchKey) > 0 {
- switch strings.ToLower(q.SearchName) {
- case "id":
- id, err := strconv.Atoi(q.SearchKey)
- if err == nil {
- countArg.IsID = true
- countArg.ID = int32(id)
-
- dataArg.IsID = true
- dataArg.ID = int32(id)
- }
- case "name":
- countArg.DisplayName = q.SearchKey
- dataArg.DisplayName = q.SearchKey
- }
+ IsStatus: q.SearchStatus != 9999,
+ Status: int32(q.SearchStatus),
+ IsID: q.SearchID != 0,
+ ID: int32(q.SearchID),
+ IsParentID: q.SearchParentID != 0,
+ ParentID: int32(q.SearchParentID),
+ DisplayName: q.SearchName,
+ Skip: (int32(q.Page) - 1) * int32(q.Rows),
+ Size: int32(q.Rows),
}
count, err := db.Engine.CountSysRoleCondition(ctx, countArg)
if err != nil {
@@ -71,18 +59,29 @@ func ListSysRoleCondition(ctx context.Context, q dto.SearchDto) ([]*db.SysRole,
return roles, count, nil
}
-func XmSelectSysRole(ctx context.Context) ([]*dto.XmSelectDto, error) {
+func XmSelectSysRole(ctx context.Context, id int32) ([]*dto.XmSelectTreeDto, error) {
all, err := db.Engine.AllSysRole(ctx)
if err != nil {
return nil, err
}
- var res []*dto.XmSelectDto
- for _, item := range all {
- res = append(res, &dto.XmSelectDto{Name: item.DisplayName, Value: int(item.ID)})
+ return toXmSelectTree(id, all), nil
+}
+
+func toXmSelectTree(parentId int32, data []*db.SysRole) []*dto.XmSelectTreeDto {
+ var res []*dto.XmSelectTreeDto
+ for _, v := range data {
+ if v.ParentID == parentId {
+ item := dto.XmSelectTreeDto{
+ Name: v.Name,
+ Value: strconv.FormatInt(int64(v.ID), 10),
+ Children: toXmSelectTree(v.ID, data),
+ }
+ res = append(res, &item)
+ }
}
- return res, nil
+ return res
}
func SetMenu(ctx context.Context, roleID int32, menus []*db.SysMenu) error {
diff --git a/internal/tpl/html.go b/internal/tpl/html.go
index b07d7d4..7a5dcf9 100644
--- a/internal/tpl/html.go
+++ b/internal/tpl/html.go
@@ -23,11 +23,11 @@ type HtmlData struct {
Data any
}
-func HTML(w http.ResponseWriter, req *http.Request, tpl string, data map[string]any) {
- rndr.html(w, req, tpl, data)
+func HTML(w http.ResponseWriter, r *http.Request, tpl string, data map[string]any) {
+ rndr.HTML(w, r, tpl, data)
}
-func (r *render) html(w http.ResponseWriter, req *http.Request, tpl string, data map[string]any) {
+func (r *render) HTML(w http.ResponseWriter, req *http.Request, tpl string, data map[string]any) {
name := strings.ReplaceAll(tpl, "/", "_")
t, ok := r.templates[name]
if !ok {
diff --git a/internal/tpl/json.go b/internal/tpl/json.go
index ff31ecf..1570cff 100644
--- a/internal/tpl/json.go
+++ b/internal/tpl/json.go
@@ -29,22 +29,34 @@ type ResponseList struct {
}
func JSON(w http.ResponseWriter, data any) {
- rndr.json(w, data)
+ rndr.JSON(w, data)
}
func JSONF(w http.ResponseWriter, success bool, message string) {
- rndr.json(w, Response{Success: success, Message: message})
+ rndr.JSONF(w, success, message)
}
func JSONOK(w http.ResponseWriter, message string) {
- rndr.json(w, Response{Success: true, Message: message})
+ rndr.JSONOK(w, message)
}
func JSONERR(w http.ResponseWriter, message string) {
- rndr.json(w, Response{Success: false, Message: message})
+ rndr.JSONERR(w, message)
}
-func (r *render) json(w http.ResponseWriter, data any) {
+func (r *render) JSONF(w http.ResponseWriter, success bool, message string) {
+ r.JSON(w, Response{Success: success, Message: message})
+}
+
+func (r *render) JSONOK(w http.ResponseWriter, message string) {
+ r.JSON(w, Response{Success: true, Message: message})
+}
+
+func (r *render) JSONERR(w http.ResponseWriter, message string) {
+ r.JSON(w, Response{Success: false, Message: message})
+}
+
+func (r *render) JSON(w http.ResponseWriter, data any) {
v, err := json.Marshal(data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/internal/tpl/render.go b/internal/tpl/render.go
index c74ac7e..c7fbb6a 100644
--- a/internal/tpl/render.go
+++ b/internal/tpl/render.go
@@ -3,26 +3,29 @@ package tpl
import (
"html/template"
"net/http"
+
+ "management/internal/pkg/session"
)
var rndr Renderer
-func Render() Renderer {
- return rndr
-}
-
type Renderer interface {
- html(w http.ResponseWriter, req *http.Request, name string, data map[string]any)
- json(w http.ResponseWriter, data any)
+ HTML(w http.ResponseWriter, req *http.Request, name string, data map[string]any)
+ JSON(w http.ResponseWriter, data any)
+ JSONF(w http.ResponseWriter, success bool, message string)
+ JSONOK(w http.ResponseWriter, message string)
+ JSONERR(w http.ResponseWriter, message string)
}
type render struct {
+ session session.ISession
config *TemplateConfig
templates map[string]*template.Template
}
-func Init() error {
+func New(session session.ISession) (Renderer, error) {
render := &render{
+ session: session,
config: &TemplateConfig{
Root: ".",
Extension: ".tmpl",
@@ -33,15 +36,9 @@ func Init() error {
templates, err := render.createTemplateCache()
if err != nil {
- return err
+ return nil, err
}
render.templates = templates
- rndr = render
- return nil
-}
-
-func InitJson() error {
- rndr = &render{}
- return nil
+ return render, nil
}
diff --git a/internal/tpl/util.go b/internal/tpl/util.go
index 79aa5aa..068c88e 100644
--- a/internal/tpl/util.go
+++ b/internal/tpl/util.go
@@ -14,7 +14,6 @@ import (
"management/internal/db/model/dto"
"management/internal/global/auth"
- "management/internal/pkg/session"
systemservice "management/internal/service/system"
templates "management/web/templates/manage"
@@ -27,11 +26,11 @@ func (r *render) setDefaultData(req *http.Request, data map[string]any) map[stri
}
ctx := req.Context()
- isAuth := session.Exists(ctx, auth.StoreName)
+ isAuth := r.session.Exists(ctx, auth.StoreName)
data["IsAuthenticated"] = isAuth
if isAuth {
var authUser dto.AuthorizeUser
- u := session.GetBytes(ctx, auth.StoreName)
+ u := r.session.GetBytes(ctx, auth.StoreName)
_ = json.Unmarshal(u, &authUser)
data["AuthorizeMenus"] = r.getCurrentPathBtns(ctx, authUser.RoleID, req.URL.Path)
diff --git a/management b/management
index 34fa123..0e9b593 100755
Binary files a/management and b/management differ
diff --git a/web/statics/admin/css/style.css b/web/statics/admin/css/style.css
index 7695344..e637c14 100644
--- a/web/statics/admin/css/style.css
+++ b/web/statics/admin/css/style.css
@@ -1,5 +1,9 @@
@charset "UTF-8";
+.h-all {
+ height: 100%;
+}
+
xm-select { border-color: var(--global-primary-color) !important; }
xm-select .xm-label .label-content .xm-label-block { background-color: var(--global-primary-color) !important; }
xm-select .xm-body .xm-option .xm-option-icon { border-color: var(--global-primary-color) !important; }
@@ -175,4 +179,30 @@ xm-select .xm-body .xm-option.selected .xm-option-icon { color: var(--global-pri
/* 设置滚动条轨道样式 */
/*::-webkit-scrollbar-track {
/*background-color: #f1f1f1; /* 轨道颜色 */
-/*}*/
\ No newline at end of file
+/*}*/
+
+.own-pannel {
+ position: relative;
+ border: 1px solid #eee;
+ border-radius: 2px;
+ /*box-shadow: 1px 1px 4px rgb(0 0 0 / 8%);*/
+ background-color: #fff;
+ color: #5f5f5f;
+ height: calc(100% - 3px);
+}
+
+.own-left-pannel {
+ position: relative;
+ border: 1px solid #eee;
+ border-radius: 2px;
+ /*box-shadow: 1px 1px 4px rgb(0 0 0 / 8%);*/
+ background-color: #fff;
+ color: #5f5f5f;
+ margin-right: 10px;
+ height: calc(100% - 3px);
+}
+
+.own-tree {
+ height: 100%;
+ overflow-y: auto;
+}
\ No newline at end of file
diff --git a/web/statics/component/pear/pear.js b/web/statics/component/pear/pear.js
index b399f79..c582d7b 100644
--- a/web/statics/component/pear/pear.js
+++ b/web/statics/component/pear/pear.js
@@ -45,9 +45,9 @@ layui.config({
notice: "notice", // 消息提示组件
step: "step", // 分布表单组件
tag: "tag", // 多标签页组件
- treetable: "treetable", // 树状表格
+ treetable: "treetable", // 树状表格
dtree: "dtree", // 树结构
- tinymce: "tinymce/tinymce", // 编辑器
+ tinymce: "tinymce/tinymce", // 编辑器
area: "area", // 省市级联
topBar: "topBar", // 置顶组件
design: "design", // 表单设计
diff --git a/web/templates/manage/home/home.tmpl b/web/templates/manage/home/home.tmpl
index b3edaac..8d466af 100644
--- a/web/templates/manage/home/home.tmpl
+++ b/web/templates/manage/home/home.tmpl
@@ -4,7 +4,7 @@