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 @@ - component + 管理后台 diff --git a/web/templates/manage/system/category/edit.tmpl b/web/templates/manage/system/category/edit.tmpl index 687e8fb..3904c1b 100644 --- a/web/templates/manage/system/category/edit.tmpl +++ b/web/templates/manage/system/category/edit.tmpl @@ -5,7 +5,6 @@
{{.CsrfTokenField}} -