├── src ├── application │ ├── modules │ │ └── README.md │ ├── models │ │ ├── tables │ │ │ ├── admin_role_priv.go │ │ │ ├── search_m_a_e.go │ │ │ ├── category_priv.go │ │ │ ├── advert_space.go │ │ │ ├── attachments.go │ │ │ ├── admin_role.go │ │ │ ├── position.go │ │ │ ├── level.go │ │ │ ├── wechat_material.go │ │ │ ├── wechat_qrcode.go │ │ │ ├── district.go │ │ │ ├── page.go │ │ │ ├── tags.go │ │ │ ├── department.go │ │ │ ├── wechat_account.go │ │ │ ├── request_log.go │ │ │ ├── log.go │ │ │ ├── attachment_type.go │ │ │ ├── wechat_log.go │ │ │ ├── advert.go │ │ │ ├── wechat_msg_reply_rule.go │ │ │ ├── wechat_user.go │ │ │ ├── link.go │ │ │ ├── document_model.go │ │ │ ├── setting.go │ │ │ ├── admin.go │ │ │ ├── document_model_field.go │ │ │ ├── content.go │ │ │ ├── dict_category.go │ │ │ ├── wechat_template.go │ │ │ ├── menu.go │ │ │ ├── category.go │ │ │ ├── plugin.go │ │ │ ├── todo.go │ │ │ ├── member.go │ │ │ └── define.go │ │ ├── base_model.go │ │ ├── wechat_member_model.go │ │ ├── document_field_model.go │ │ ├── document_dsl_model.go │ │ ├── page_model.go │ │ ├── attachments_model.go │ │ ├── log_model.go │ │ ├── member_model.go │ │ ├── ad_model.go │ │ ├── link_model.go │ │ ├── adspace_model.go │ │ ├── admin_model.go │ │ ├── admin_role_model.go │ │ └── menu_model.go │ ├── controllers │ │ ├── backend │ │ │ ├── webssh │ │ │ │ ├── common │ │ │ │ │ ├── core │ │ │ │ │ │ ├── server.go │ │ │ │ │ │ └── helper.go │ │ │ │ │ ├── jwt.go │ │ │ │ │ └── aes.go │ │ │ │ ├── Apiform │ │ │ │ │ ├── Response.go │ │ │ │ │ └── Api.go │ │ │ │ ├── tables │ │ │ │ │ ├── user.go │ │ │ │ │ └── server.go │ │ │ │ ├── errcode │ │ │ │ │ └── Err.go │ │ │ │ ├── define.go │ │ │ │ ├── route.go │ │ │ │ ├── ssh_controller.go │ │ │ │ └── middleware │ │ │ │ │ └── Auth.go │ │ │ ├── wechat │ │ │ │ ├── dto │ │ │ │ │ └── dto.go │ │ │ │ ├── README.md │ │ │ │ ├── response.go │ │ │ │ ├── cacher.go │ │ │ │ ├── route.go │ │ │ │ ├── rule_controller.go │ │ │ │ ├── menu_controller.go │ │ │ │ ├── common.go │ │ │ │ ├── account_controller.go │ │ │ │ ├── qrcode_ controller.go │ │ │ │ └── define.go │ │ │ ├── filemanager │ │ │ │ ├── route.go │ │ │ │ └── tables │ │ │ │ │ └── file_manager_account.go │ │ │ ├── link_controller.go │ │ │ ├── tags_controller.go │ │ │ ├── position_controller.go │ │ │ ├── im_session_controller.go │ │ │ ├── attachment_type_controller.go │ │ │ ├── level_controller.go │ │ │ ├── errlog_controller.go │ │ │ ├── ad_controller.go │ │ │ ├── log_controller.go │ │ │ ├── menu_controller.go │ │ │ ├── member_controller.go │ │ │ ├── ad_space_controller.go │ │ │ ├── member_group_controller.go │ │ │ ├── request_params.go │ │ │ ├── helper.go │ │ │ ├── department_controller.go │ │ │ ├── attachment_controller.go │ │ │ ├── dict_category_controller.go │ │ │ ├── database_backup_controller.go │ │ │ ├── login_controller.go │ │ │ ├── const.go │ │ │ └── dict_controller.go │ │ ├── middleware │ │ │ ├── apidoc │ │ │ │ ├── export │ │ │ │ │ ├── intf.go │ │ │ │ │ └── swag.go │ │ │ │ ├── entity.go │ │ │ │ └── parse.go │ │ │ ├── rbac_models.conf │ │ │ ├── pprof.go │ │ │ ├── limiter.go │ │ │ ├── statsviz.go │ │ │ ├── data.go │ │ │ ├── cors.go │ │ │ └── verify_token.go │ │ ├── taglibs │ │ │ ├── hotwords.go │ │ │ ├── tags.go │ │ │ ├── TopType.go │ │ │ ├── myad.go │ │ │ ├── type.go │ │ │ ├── flink.go │ │ │ ├── query.go │ │ │ ├── pagelist.go │ │ │ ├── position.go │ │ │ ├── adlist.go │ │ │ ├── likearticle.go │ │ │ ├── helper.go │ │ │ ├── channel.go │ │ │ ├── channelartlist.go │ │ │ └── list.go │ │ ├── helper.go │ │ ├── frontend │ │ │ ├── fesc_controller.go │ │ │ ├── index.go │ │ │ ├── click.go │ │ │ ├── page.go │ │ │ └── list.go │ │ ├── service_key.go │ │ ├── cache_key.go │ │ └── tplfun │ │ │ └── func.go │ └── plugins │ │ └── task │ │ ├── cmd │ │ ├── make_task.go │ │ └── task.go │ │ ├── manager │ │ └── logger.go │ │ ├── config.json │ │ └── table │ │ └── table.go ├── common │ ├── search │ │ ├── search.go │ │ ├── nullsearch.go │ │ └── zincsearch.go │ ├── enum │ │ ├── enum_test.go │ │ └── enum.go │ ├── message │ │ ├── message_intf.go │ │ ├── null.go │ │ ├── email.go │ │ └── sms.go │ ├── oncer │ │ └── oncer.go │ ├── storage │ │ ├── storage_intf.go │ │ └── storage_ftp_test.go │ ├── helper │ │ └── service.go │ ├── template │ │ └── gktemplate.go │ └── logger │ │ └── pinecms_logger.go ├── router │ ├── web.go │ └── module.go └── server │ └── server.go ├── resources ├── themes │ └── default │ │ ├── thumb.png │ │ ├── footer.jet │ │ ├── page.jet │ │ ├── error.jet │ │ ├── right.jet │ │ ├── head2.jet │ │ └── head.jet └── configs │ ├── database.yml.dist │ └── application.yml ├── .gitattributes ├── cmd ├── plugin │ ├── plugin.go │ └── build_plugin.go ├── server │ ├── run.go │ ├── remove.go │ ├── stop.go │ ├── install.go │ ├── start.go │ ├── status.go │ ├── serve.go │ └── service.go ├── version │ └── version.go ├── annotation.go ├── root.go ├── crud │ ├── crud_format_stub.go │ └── crud_stub.go ├── example.go ├── dede │ └── import.go └── util │ └── util.go ├── tasks └── echo.sh ├── main.go ├── .vscode └── launch.json ├── .gitignore └── tests └── ast_test.go /src/application/modules/README.md: -------------------------------------------------------------------------------- 1 | # 模块目录 # -------------------------------------------------------------------------------- /resources/themes/default/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiusin/pinecms/HEAD/resources/themes/default/thumb.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-language=go 2 | *.js linguist-language=go 3 | *.sql linguist-language=go 4 | *.html linguist-language=go -------------------------------------------------------------------------------- /src/application/models/tables/admin_role_priv.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type AdminRolePriv struct { 4 | RoleId int64 5 | MenuId int64 6 | } 7 | -------------------------------------------------------------------------------- /src/application/models/base_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "xorm.io/builder" 5 | ) 6 | 7 | type BaseModel struct { 8 | builder.Between 9 | } 10 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/common/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Server struct { 4 | Ip string 5 | Port int 6 | User string 7 | Passwd string 8 | } -------------------------------------------------------------------------------- /src/application/models/tables/search_m_a_e.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type SearchMAE struct { 4 | Mid int64 `json:"mid"` 5 | Aid int64 `json:"aid"` 6 | Eid string `json:"eid"` 7 | } 8 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/apidoc/export/intf.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import "github.com/xiusin/pine" 4 | 5 | type ExportIntf interface { 6 | SetContext(*pine.Context) 7 | Export() 8 | } 9 | -------------------------------------------------------------------------------- /src/application/models/tables/category_priv.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | // CategoryPriv 分类权限 4 | type CategoryPriv struct { 5 | Catid int64 `xorm:"pk"` 6 | Roleid int64 7 | IsAdmin int64 8 | Action string 9 | } 10 | -------------------------------------------------------------------------------- /src/application/models/tables/advert_space.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type AdvertSpace struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name"` 6 | Key string `json:"key"` 7 | Remark string `json:"remark"` 8 | } 9 | -------------------------------------------------------------------------------- /src/application/models/tables/attachments.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type AttachmentType struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name"` 6 | CreatedAt LocalTime `json:"created_at" xorm:"created"` 7 | } 8 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/Apiform/Response.go: -------------------------------------------------------------------------------- 1 | package Apiform 2 | 3 | type Resp struct { 4 | Code int `json:"code"` 5 | Data any `json:"data"` 6 | Msg string `json:"msg"` 7 | Token string `json:"token"` 8 | } 9 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/dto/dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type RuleSearch struct { 4 | Page int `json:"page"` 5 | PageSize int `json:"page_size"` 6 | Param struct { 7 | Appid string `json:"appid"` 8 | } `json:"param"` 9 | } 10 | -------------------------------------------------------------------------------- /cmd/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var Cmd = &cobra.Command{ 8 | Use: "plugin", 9 | Short: "插件模块管理", 10 | } 11 | 12 | func init() { 13 | Cmd.AddCommand(makePluginCmd, buildPluginCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/server/run.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var runCmd = &cobra.Command{ 6 | Use: "run", 7 | Short: "启动后端服务", 8 | Run: func(cmd *cobra.Command, args []string) { 9 | serv.Manage([]string{"run"}, cmd.UsageString()) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/application/plugins/task/cmd/make_task.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var makeTaskCmd = &cobra.Command{ 8 | Use: "make", 9 | Short: "创建任务源文件", 10 | Run: func(cmd *cobra.Command, args []string) { 11 | 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/tables/user.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type SSHUser struct { 4 | Id int64 `xorm:"pk autoincr"` 5 | Phone int64 `xorm:"unique"` 6 | Email *string `xorm:"unique"` 7 | Password string 8 | Servers []SSHServer 9 | } 10 | -------------------------------------------------------------------------------- /cmd/server/remove.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var stopCmd = &cobra.Command{ 6 | Use: "stop", 7 | Short: "停止守护进程", 8 | Run: func(cmd *cobra.Command, args []string) { 9 | serv.Manage([]string{"stop"}, cmd.UsageString()) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /cmd/server/stop.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var removeCmd = &cobra.Command{ 6 | Use: "remove", 7 | Short: "移除服务", 8 | Run: func(cmd *cobra.Command, args []string) { 9 | serv.Manage([]string{"remove"}, cmd.UsageString()) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /cmd/server/install.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var installCmd = &cobra.Command{ 6 | Use: "install", 7 | Short: "安装服务", 8 | Run: func(cmd *cobra.Command, args []string) { 9 | serv.Manage([]string{"install"}, cmd.UsageString()) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /cmd/server/start.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var startCmd = &cobra.Command{ 6 | Use: "start", 7 | Short: "守护进程方式打开服务", 8 | Run: func(cmd *cobra.Command, args []string) { 9 | serv.Manage([]string{"start"}, cmd.UsageString()) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /cmd/server/status.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var statusCmd = &cobra.Command{ 6 | Use: "status", 7 | Short: "查看守护进程状态", 8 | Run: func(cmd *cobra.Command, args []string) { 9 | serv.Manage([]string{"status"}, cmd.UsageString()) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/common/search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | 4 | type ISearch interface { 5 | Search(index string, query any) (any, error) 6 | Index(index string, doc map[string]any) (string, error) 7 | Update(index, id string, doc map[string]any) error 8 | Delete(index string, id string) error 9 | } 10 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/README.md: -------------------------------------------------------------------------------- 1 | # 微信模块 2 | 3 | 开发期间本地可以使用 `FRP` 或 `花生壳` 软件做内网穿透代理 4 | 5 | ```ini 6 | [common] 7 | server_addr = 100.100.100.100 # 服务端IP 8 | server_port = 7000 # 服务端端口 9 | 10 | [web] 11 | type = http 12 | local_port = 2019 13 | custom_domains = wechat.xxx.com 14 | ``` 15 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/apidoc/export/swag.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import "github.com/xiusin/pine" 4 | 5 | type Swag struct { 6 | template string 7 | } 8 | 9 | func (s *Swag) SetContext(ctx *pine.Context) { 10 | s.template = "" 11 | } 12 | 13 | func (s *Swag) Export() { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tasks/echo.sh: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "encoding/json" 5 | "pinecms" 6 | ) 7 | 8 | // 脚本样例 9 | func Run(orm *pinecms.DB) (string, error) { 10 | var data = map[string]interface{}{} 11 | orm.Table("pinecms_task_info").Get(&data) 12 | d, _ := json.Marshal(&data) 13 | return string(d), nil 14 | } 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:generate go run main.go src 2 | package main 3 | 4 | import ( 5 | "github.com/bytedance/sonic" 6 | "github.com/xiusin/pine/cache" 7 | "github.com/xiusin/pinecms/cmd" 8 | ) 9 | 10 | func init() { 11 | cache.SetTranscoderFunc(sonic.Marshal, sonic.Unmarshal) 12 | } 13 | 14 | func main() { 15 | cmd.Execute() 16 | } 17 | -------------------------------------------------------------------------------- /cmd/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | const Version = "dev 0.2.0" 10 | 11 | var Cmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "显示版本号", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Println("version: " + Version) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/rbac_models.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "1" 15 | -------------------------------------------------------------------------------- /src/application/plugins/task/cmd/task.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | const taskDir = "tasks" 8 | 9 | var TaskCmd = &cobra.Command{ 10 | Use: "task", 11 | Short: "任务管理模块", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | }, 14 | } 15 | 16 | func init() { 17 | TaskCmd.AddCommand(makeTaskCmd) 18 | } 19 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/tables/server.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type SSHServer struct { 8 | Id int64 `xorm:"pk autoincr"` 9 | Nickname string 10 | Ip string 11 | Port int 12 | Username string 13 | Password string `json:"-"` 14 | BindUser int64 `json:"-"` 15 | BeforeTime time.Time 16 | } 17 | -------------------------------------------------------------------------------- /cmd/annotation.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | annotation "github.com/xiusin/go-annotation/pkg" 7 | _ "github.com/xiusin/pinecms/cmd/util/annotations/rest" 8 | ) 9 | 10 | var annotationsCmd = &cobra.Command{ 11 | Use: "src", 12 | Short: "注解路由生成", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | annotation.Process() 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/application/models/tables/admin_role.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type AdminRole struct { 4 | Id int64 `xorm:"pk autoincr" json:"id"` 5 | Rolename string `json:"rolename"` 6 | Description string `json:"description"` 7 | Listorder int64 `json:"listorder"` 8 | Disabled int64 `json:"disabled"` 9 | MenuIdList []int64 `json:"menuIdList" xorm:"json menu_ids"` 10 | } 11 | -------------------------------------------------------------------------------- /src/application/models/tables/position.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | //Position 职位管理 4 | type Position struct { 5 | Id int64 `json:"id"` 6 | Name string `json:"name" schema:"name" xorm:"varchar(25)"` 7 | Code string `json:"code" xorm:"varchar(25)"` 8 | Listorder uint `json:"listorder"` 9 | Status bool `json:"status"` 10 | Remark string `json:"remark" xorm:"text"` 11 | } 12 | -------------------------------------------------------------------------------- /src/application/models/tables/level.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | // Level 职级 4 | type Level struct { 5 | Id int64 `json:"id"` 6 | Name string `json:"name" schema:"name" xorm:"varchar(25)"` 7 | Listorder uint `json:"listorder"` 8 | Status bool `json:"status"` 9 | CreatedAt LocalTime `json:"created" xorm:"created"` 10 | UpdatedAt *LocalTime `json:"updated" xorm:"updated"` 11 | } 12 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/errcode/Err.go: -------------------------------------------------------------------------------- 1 | package errcode 2 | 3 | const ( 4 | C_nil_err = 200 //成功 5 | C_from_err = 100 //表单错误 6 | C_phone_err = 101 //手机号错误 7 | C_code_err = 102 //验证码错误 8 | S_send_err = 300 //发送验证码错误 9 | S_auth_err = 301 //Token权限校验失败 10 | S_auth_fmt_err = 302 //Header Token格式错误 11 | S_Verify_err = 303 //验证码校验失败 12 | S_Db_err = 304 13 | ) 14 | -------------------------------------------------------------------------------- /src/common/enum/enum_test.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type Ttt struct { 9 | CustomValue Type[string] `enum:"name_custom_value"` 10 | KeyAsValue Type[string] 11 | } 12 | 13 | var vv = New[Ttt](nil) 14 | 15 | func TestEnum(_ *testing.T) { 16 | EnumAsParam(vv.CustomValue) 17 | } 18 | 19 | func EnumAsParam(dist TypeContract[string]) { 20 | fmt.Println(dist.Value()) 21 | } 22 | -------------------------------------------------------------------------------- /src/application/plugins/task/manager/logger.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "github.com/xiusin/pine/contracts" 5 | ) 6 | 7 | type logger struct { 8 | contracts.Logger 9 | } 10 | 11 | func (l logger) Info(msg string, keysAndValues ...any) { 12 | l.Logger.Warn(msg, keysAndValues) 13 | } 14 | 15 | func (l logger) Error(err error, msg string, keysAndValues ...any) { 16 | l.Logger.Error("%s: 错误: %s, 参数: %s", msg, err, keysAndValues) 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "cwd": "${workspaceFolder}", 13 | "program": "main.go" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/application/controllers/backend/filemanager/route.go: -------------------------------------------------------------------------------- 1 | package filemanager 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | ) 6 | 7 | func InitRouter(app *pine.Application, router *pine.Router) { 8 | InitInstall(app, "/uploads/", "./resources/assets/uploads/") 9 | //app.StaticFS("/fm/ui", assets, "dist", "index.html") 10 | app.Static("/fm/ui", "src/application/controllers/backend/filemanager/dist", 2) 11 | app.Handle(new(FileManagerController), "/filemanager") 12 | } 13 | -------------------------------------------------------------------------------- /src/application/plugins/task/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "任务管理", 3 | "author": "xiusin", 4 | "contact": "18818818818", 5 | "description": "实现任务管理功能", 6 | "version": "dev 0.0.1", 7 | "page": "# 任务管理模块教程\n\n## 安装 \n1. 下载编译完成的**task.so**放置到程序同级目录**plugins**下\n2. 程序每隔10秒扫描一次目录,自动注册程序信息到插件系统\n3. 本模块需要在开发环境时导入, 需要自动导出**pages.zip** 放到前端开发目录下使用\n\n## 修改路由前缀\n1. 默认路由前缀和插件名称一致,如果修改需要重启程序以载入.so实现. \n2. 需要重新导入router.ts注入修改后的路由地址 (不建议修改)\n\n## 结束语\n如有问题望请各位PR或提交到个人邮箱或电话" 8 | } 9 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_material.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type WechatMaterial struct { 4 | Id int64 `json:"id" xorm:"pk autoincr"` 5 | Appid string `json:"appid" xorm:"char(20) not null comment('appid')"` 6 | Type string `json:"type" xorm:"varchar(5) comment('媒体素材类型')"` 7 | MediaId string `json:"media_id" xorm:"varchar(40) comment('媒体ID')"` 8 | Url string `json:"url"` 9 | UpdateTime LocalTime `json:"update_time"` 10 | } 11 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/pprof.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/valyala/fasthttp/pprofhandler" 5 | "github.com/xiusin/pine" 6 | ) 7 | 8 | func Pprof(shouldUsePprofHandlerFn func(ctx *pine.Context) bool) pine.Handler { 9 | return func(ctx *pine.Context) { 10 | if shouldUsePprofHandlerFn != nil && shouldUsePprofHandlerFn(ctx) { 11 | pprofhandler.PprofHandler(ctx.RequestCtx) 12 | ctx.Stop() 13 | return 14 | } 15 | ctx.Next() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/search/nullsearch.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | type NullSearch struct { 4 | } 5 | 6 | func (n *NullSearch) Search(index string, query any) (any, error) { 7 | return nil, nil 8 | } 9 | 10 | func (n *NullSearch) Index(index string, doc map[string]any) (string, error) { 11 | return "", nil 12 | } 13 | 14 | func (n *NullSearch) Update(index string, id string, doc map[string]any) error { 15 | return nil 16 | } 17 | 18 | func (n *NullSearch) Delete(index string, id string) error { 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /src/application/controllers/backend/link_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import "github.com/xiusin/pinecms/src/application/models/tables" 4 | 5 | type LinkController struct { 6 | BaseController 7 | } 8 | 9 | func (c *LinkController) Construct() { 10 | c.KeywordsSearch = []SearchFieldDsl{ 11 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 12 | } 13 | c.Table = &tables.Link{} 14 | c.Entries = &[]tables.Link{} 15 | c.ApiEntityName = "友链" 16 | c.Group = "友情链接管理" 17 | c.BaseController.Construct() 18 | } 19 | -------------------------------------------------------------------------------- /src/application/controllers/backend/tags_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import "github.com/xiusin/pinecms/src/application/models/tables" 4 | 5 | type TagsController struct { 6 | BaseController 7 | } 8 | 9 | func (c *TagsController) Construct() { 10 | c.KeywordsSearch = []SearchFieldDsl{ 11 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 12 | } 13 | c.Table = &tables.Tags{} 14 | c.Entries = &[]*tables.Tags{} 15 | c.ApiEntityName = "标签" 16 | c.Group = "标签管理" 17 | c.BaseController.Construct() 18 | } 19 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/limiter.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/xiusin/pine" 8 | "golang.org/x/time/rate" 9 | ) 10 | 11 | func Limiter(maxBurstSize int) pine.Handler { 12 | limiter := rate.NewLimiter(rate.Every(time.Second), maxBurstSize) 13 | return func(ctx *pine.Context) { 14 | if limiter.Allow() { 15 | ctx.Next() 16 | } else { 17 | ctx.Abort(http.StatusTooManyRequests, "The service is temporarily unavailable") 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/router/web.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/xiusin/pinecms/src/application/controllers/frontend" 7 | 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pinecms/src/config" 10 | ) 11 | 12 | func InitStatics(app *pine.Application) { 13 | for _, static := range config.App().Statics { 14 | app.Static(static.Route, filepath.FromSlash(static.Path), 1) 15 | } 16 | } 17 | 18 | func InitRouter(app *pine.Application) { 19 | app.Handle(new(frontend.IndexController)) 20 | } 21 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_qrcode.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type WechatQrcode struct { 4 | Id int64 `json:"id" xorm:"pk autoincr"` 5 | AppId string `json:"appid" xorm:"char(20) appid"` 6 | IsTemp bool `json:"is_temp" xorm:"comment('是否为临时二维码')"` 7 | SceneStr string `json:"scene_str"` 8 | Ticket string `json:"ticket"` 9 | Url string `json:"url"` 10 | ExpireTime LocalTime `json:"expire_time"` 11 | CreatedAt LocalTime `json:"created_at" xorm:"created"` 12 | } 13 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/hotwords.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "github.com/CloudyKit/jet" 5 | "github.com/xiusin/pine" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func Tags(args jet.Arguments) reflect.Value { 11 | if !checkArgType(&args) { 12 | return defaultArrReturnVal 13 | } 14 | defer func() { 15 | if err := recover(); err != nil { 16 | pine.Logger().Error("HotWords Failed", err) 17 | } 18 | }() 19 | tags := strings.Split(args.Get(0).String(), ",") 20 | return reflect.ValueOf(tags) 21 | } 22 | -------------------------------------------------------------------------------- /src/application/models/tables/district.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type District struct { 4 | Id int64 `xorm:"pk autoincr" json:"id"` 5 | Name string `json:"name"` 6 | ParentId int64 `json:"parent_id"` 7 | Initial string `json:"initial"` 8 | Initials string `json:"initials"` 9 | PinYin string `json:"pinyin" xorm:"pinyin"` 10 | Extra string `json:"extra"` 11 | Suffix string `json:"suffix"` 12 | Code string `json:"code"` 13 | AreaCode string `json:"area_code" xorm:"area_code"` 14 | OrderNum uint `json:"order" xorm:"order"` 15 | } 16 | -------------------------------------------------------------------------------- /resources/configs/database.yml.dist: -------------------------------------------------------------------------------- 1 | db: 2 | dsn: "root:+3ak9,XhB&oD@tcp(127.0.0.1:3306)/pinecms?charset=utf8mb4" 3 | prefix: "pinecms_" 4 | driver: "mysql" # 目前只支持Mysql 5 | 6 | orm: 7 | show_sql: true 8 | show_exec_time: true 9 | max_open_conns: 10 10 | max_idle_conns: 10 11 | 12 | redis: 13 | host: 127.0.0.1 14 | port: 6379 15 | password: 16 | index: 1 17 | max_active: 100 18 | max_idle: 5 19 | idle_timeout: 60 20 | 21 | elastic: 22 | url: "http://127.0.0.1:9200" 23 | username: "elastic" 24 | password: "elastic" -------------------------------------------------------------------------------- /src/application/controllers/helper.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gbrlsnchs/jwt/v3" 5 | "github.com/xiusin/pine/di" 6 | ) 7 | 8 | // LoginAdminPayload 登录JWT载体 9 | type LoginAdminPayload struct { 10 | jwt.Payload 11 | Id int64 `json:"id"` 12 | AdminId int64 `json:"admin_id"` 13 | AdminName string `json:"admin_name"` 14 | RoleID []int64 `json:"role_id"` 15 | } 16 | 17 | // GetTableName 获取表名 18 | func GetTableName(name string) string { 19 | tablePrefix := di.MustGet(ServiceTablePrefix).(string) 20 | return tablePrefix + name 21 | } 22 | -------------------------------------------------------------------------------- /src/application/controllers/backend/position_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/models/tables" 5 | ) 6 | 7 | type PositionController struct { 8 | BaseController 9 | } 10 | 11 | func (c *PositionController) Construct() { 12 | c.Table = &tables.Position{} 13 | c.Entries = &[]tables.Position{} 14 | c.Group = "系统管理" 15 | c.SubGroup = "岗位管理" 16 | c.ApiEntityName = "岗位" 17 | c.BaseController.Construct() 18 | c.KeywordsSearch = []SearchFieldDsl{ 19 | {Field: "name", Op: "="}, 20 | {Field: "code", Op: "="}, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/application/models/tables/page.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Page struct { 4 | Id int64 `xorm:"pk comment('栏目ID')" json:"id"` 5 | Title string `json:"title" xorm:"comment('页面标题') varchar(100)"` 6 | Keywords string `json:"keywords" xorm:"comment('页面关键字') varchar(255)"` 7 | Description string `json:"description" xorm:"comment('页面描述') varchar(255)"` 8 | Content string `json:"content" xorm:"comment('页面内容') text"` 9 | CreatedAt LocalTime `json:"created_at" xorm:"created"` 10 | UpdatedAt LocalTime `json:"updated_at" xorm:"updated"` 11 | } 12 | -------------------------------------------------------------------------------- /src/application/models/tables/tags.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Tags struct { 4 | Id int64 `xorm:"pk autoincr id" json:"id"` 5 | Name string `json:"name"` 6 | RefNum uint `json:"ref_num"` 7 | Clicks uint `json:"clicks"` 8 | SeoTitle string `json:"seo_title"` 9 | SeoKeywords string `json:"seo_keywords"` 10 | SeoDescription string `json:"seo_description"` 11 | Listorder uint `json:"listorder"` 12 | Status uint `json:"status"` 13 | CreatedAt LocalTime `json:"created_at"` 14 | } 15 | -------------------------------------------------------------------------------- /resources/themes/default/footer.jet: -------------------------------------------------------------------------------- 1 |
2 |
3 | 关于我们| 4 | 联系我们| 5 | 加入我们| 6 | 投稿反馈 7 |
8 |
9 |

{{global["site_copyright"]}}

10 |

{{global["site_icp"]}}

11 |
12 |
13 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/response.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | /** 10 | 实现Response的响应对象 11 | */ 12 | type wechatResponseWrapper struct { 13 | *fasthttp.RequestCtx 14 | } 15 | 16 | func (w wechatResponseWrapper) Header() http.Header { 17 | return map[string][]string{} 18 | } 19 | 20 | func (w wechatResponseWrapper) Write(bytes []byte) (int, error) { 21 | return w.RequestCtx.Write(bytes) 22 | } 23 | 24 | func (w wechatResponseWrapper) WriteHeader(statusCode int) { 25 | w.Response.SetStatusCode(statusCode) 26 | } 27 | -------------------------------------------------------------------------------- /src/application/models/tables/department.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Department struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name" schema:"name" xorm:"varchar(25)"` 6 | LeaderName string `json:"leader_name" xorm:"varchar(25)"` 7 | LeaderPhone string `json:"leader_phone" xorm:"varchar(35)"` 8 | Email string `json:"email" xorm:"varchar(100)"` 9 | Status bool `json:"status" schema:"status"` // 状态 10 | Listorder uint `json:"listorder"` 11 | ParentId uint `json:"parent_id"` 12 | CreatedAt LocalTime `json:"created_at"` 13 | } 14 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_account.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type WechatAccount struct { 4 | Id int64 `json:"id" xorm:"pk autoincr"` 5 | AppId string `json:"appid" xorm:"char(20) not null comment('appid')"` 6 | Name string `json:"name" xorm:"varchar(50) not null comment('公众号名称')"` 7 | Type uint `json:"type" xorm:"tinyint(1) comment('账号类型')"` 8 | Verified bool `json:"verified" xorm:"tinyint(1) comment('认证状态')"` 9 | Secret string `json:"secret" xorm:"char(32)"` 10 | Token string `json:"token" xorm:"varchar(32)"` 11 | AesKey string `json:"aesKey" xorm:"varchar(43)"` 12 | } 13 | -------------------------------------------------------------------------------- /src/router/module.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/controllers/backend/filemanager" 6 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh" 7 | "github.com/xiusin/pinecms/src/application/controllers/backend/wechat" 8 | ) 9 | 10 | func InitModuleRouter(backendRouter *pine.Router, app *pine.Application) { 11 | filemanager.InitRouter(app, backendRouter) 12 | webssh.InitRouter(app, backendRouter) 13 | } 14 | 15 | func InitSubModuleRouter(app *pine.Application, admin *pine.Router) { 16 | wechat.InitRouter(app, admin) 17 | } 18 | -------------------------------------------------------------------------------- /src/application/models/tables/request_log.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type RequestLog struct { 4 | Id int64 `json:"id"` 5 | Params string `json:"params" xorm:"comment('参数') varchar(255)"` 6 | Uri string `json:"uri" xorm:"comment('请求uri') varchar(255)"` 7 | Userid int64 `json:"userid" xorm:"comment('操作用户ID') int(6)"` 8 | Username string `json:"username" xorm:"comment('操作用户名') varchar(100)"` 9 | Ip string `json:"ip" xorm:"comment('操作IP') varchar(15)"` 10 | Time LocalTime `json:"time" xorm:"comment('操作时间')"` 11 | Method string `json:"method" xorm:"comment('请求方法') varchar(10)"` 12 | } 13 | -------------------------------------------------------------------------------- /src/application/controllers/backend/im_session_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import "github.com/xiusin/pine" 4 | 5 | type ImSessionController struct { 6 | pine.Controller 7 | } 8 | 9 | func (c *ImSessionController) RegisterRoute(b pine.IRouterWrapper) { 10 | b.POST("/im/session/page", "SessionPage") 11 | b.POST("/im/session/unreadCount", "SessionUnreadCount") 12 | b.POST("/im/message/page", "MessagePage") 13 | } 14 | 15 | func (c *ImSessionController) SessionPage() { 16 | 17 | } 18 | 19 | func (c *ImSessionController) SessionUnreadCount() { 20 | 21 | } 22 | 23 | func (c *ImSessionController) MessagePage() { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/common/message/message_intf.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | const ( 4 | TypeDefault = iota 5 | TypeRegister 6 | TypeLogin 7 | TypeModifyProfile 8 | TypeFindPwd 9 | TypeNotice 10 | ) 11 | 12 | // AbstractMessage 发送接口 13 | type AbstractMessage interface { 14 | Init() error 15 | Send(receiver []string, subject string, body string) error 16 | // receiver 接收人数组 params 模板内数据 17 | Notice(receiver []string, params []any, templateId int) error 18 | // 更新单例配置 19 | UpdateCfg() error 20 | } 21 | 22 | var MessageServiceDict = map[string]string{ 23 | ServiceNullMessage: "空短信", 24 | //ServiceSmsMessage: "阿里信息", 25 | ServiceEmailMessage: "邮箱", 26 | } 27 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/tags.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "github.com/CloudyKit/jet" 5 | "github.com/xiusin/pine" 6 | "github.com/xiusin/pinecms/src/config" 7 | "reflect" 8 | "runtime/debug" 9 | "strings" 10 | ) 11 | 12 | func HotWords(args jet.Arguments) reflect.Value { 13 | if !checkArgType(&args) { 14 | return defaultArrReturnVal 15 | } 16 | defer func() { 17 | if err := recover(); err != nil { 18 | pine.Logger().Error("HotWords Failed", err, string(debug.Stack())) 19 | } 20 | }() 21 | conf, _ := config.SiteConfig() 22 | words := strings.Split(conf["SITE_HOTWORDS"], ",") 23 | return reflect.ValueOf(words) 24 | } 25 | -------------------------------------------------------------------------------- /src/application/models/tables/log.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Log struct { 4 | Id int64 `json:"id"` 5 | Level uint8 `json:"level" xorm:"comment('日志类型') tinyint(3)"` 6 | //Uri string `json:"uri" xorm:"comment('请求uri') varchar(255)"` 7 | //Method string `json:"method" xorm:"comment('请求方法') varchar(10)"` 8 | //Params any `json:"params" xorm:"comment('请求参数') json"` 9 | Message string `json:"message" xorm:"comment('操作用户名') text"` 10 | //Stack string `json:"stack" xorm:"comment('调用堆栈') text"` 11 | //Ip string `json:"ip" xorm:"comment('操作IP') varchar(15)"` 12 | Time LocalTime `json:"time" xorm:"comment('操作时间')"` 13 | } 14 | -------------------------------------------------------------------------------- /src/common/message/null.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/xiusin/pine/di" 4 | 5 | type NullMessage struct{} 6 | 7 | var ServiceNullMessage = "pinecms.message.service.null" 8 | 9 | func (n NullMessage) Init() error { return nil } 10 | 11 | func (n NullMessage) Notice(receiver []string, params []any, templateId int) error { 12 | return nil 13 | } 14 | 15 | func (n NullMessage) Send(receiver []string, msg string, typo int) error { return nil } 16 | 17 | func (n NullMessage) UpdateCfg() error { return nil } 18 | 19 | func init() { 20 | di.Set(ServiceSmsMessage, func(builder di.AbstractBuilder) (any, error) { 21 | return &NullMessage{}, nil 22 | }, true) 23 | } 24 | -------------------------------------------------------------------------------- /src/application/models/tables/attachment_type.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Attachments struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name" xorm:"comment('附件名称')"` 6 | Url string `json:"url" xorm:"comment('完整的链接地址')"` 7 | OriginName string `json:"original" xorm:"comment('原始名称')"` 8 | Size int64 `json:"size" xorm:"comment('附件大小')"` 9 | CreatedAt LocalTime `json:"upload_time" xorm:"created"` 10 | Type string `json:"type" xorm:"comment('类型') varchar(30)"` 11 | ClassifyId int64 `json:"classifyId" xorm:"comment('归属分类ID') int(5)"` 12 | Md5 string `json:"md5" xorm:"comment('附件的md5值') unique varchar(32)"` 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | runtime 3 | .idea 4 | *.js___jb_tmp___ 5 | go.sum 6 | vendor 7 | node_modules 8 | uploads 9 | resources/configs/database.yml 10 | resources/assets/example/ 11 | resources/themes/example/ 12 | resources/html/ 13 | *.exe 14 | /apidoc.html 15 | /apidoc.html.json 16 | /apidoc/ 17 | /admin/dist 18 | *.so 19 | /admin/public/Ueditor/ 20 | /apidoc-ui/public/UEditor/ 21 | /admin/wx-manage/ 22 | /main 23 | tests/*.png 24 | tests/*.jpg 25 | tests/*.h 26 | .lh 27 | /plugins/ 28 | ./pinecms 29 | pinecms 30 | admin/.yarnclean 31 | publish 32 | publish.zip 33 | pinecms-web 34 | dist 35 | pinecms-web-ui 36 | example 37 | __debug* 38 | *.gen.go 39 | data/_metadata.bolt 40 | .history/ 41 | *.DS_Store 42 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/statsviz.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/arl/statsviz" 5 | "github.com/valyala/fasthttp/fasthttpadaptor" 6 | "github.com/xiusin/pine" 7 | ) 8 | 9 | func StatesViz(app *pine.Application) pine.Handler { 10 | sev, _ := statsviz.NewServer() 11 | indexHandler := fasthttpadaptor.NewFastHTTPHandler(sev.Index()) 12 | wsHandler := fasthttpadaptor.NewFastHTTPHandler(sev.Ws()) 13 | 14 | app.GET("/debug/statsviz/*filepath", func(ctx *pine.Context) { 15 | if ctx.Params().Get("filepath") == "ws" { 16 | wsHandler(ctx.RequestCtx) 17 | } else { 18 | indexHandler(ctx.RequestCtx) 19 | } 20 | }) 21 | return func(ctx *pine.Context) { 22 | ctx.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/define.go: -------------------------------------------------------------------------------- 1 | package webssh 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/tables" 5 | "sync" 6 | 7 | "github.com/xiusin/pine" 8 | "github.com/xiusin/pinecms/src/common/helper" 9 | ) 10 | 11 | var once sync.Once 12 | 13 | func InitInstall(app *pine.Application, urlPrefix, dir string) { 14 | once.Do(func() { 15 | app.Static(urlPrefix, dir, 2) 16 | orm := helper.GetORM() 17 | defer func() { 18 | if err := recover(); err != nil { 19 | pine.Logger().Warn("初始化安装失败", err) 20 | } 21 | }() 22 | 23 | if err := orm.Sync2(&tables.SSHServer{}, &tables.SSHUser{}); err != nil { 24 | pine.Logger().Warn(err.Error()) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/common/oncer/oncer.go: -------------------------------------------------------------------------------- 1 | package oncer 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "sync" 7 | ) 8 | 9 | var caller = &sync.Map{} 10 | 11 | func Do(fn func(), withline ...bool) { 12 | var ( 13 | file string 14 | line int 15 | ok bool 16 | once *sync.Once 17 | ) 18 | 19 | if len(withline) > 0 && withline[0] { 20 | _, file, line, ok = runtime.Caller(1) 21 | if !ok { 22 | return 23 | } 24 | file = fmt.Sprintf("%s:%d", file, line) 25 | } else { 26 | _, file, _, ok = runtime.Caller(1) 27 | if !ok { 28 | return 29 | } 30 | } 31 | if v, ok := caller.Load(file); ok { 32 | once = v.(*sync.Once) 33 | } else { 34 | once = new(sync.Once) 35 | caller.Store(file, once) 36 | } 37 | once.Do(fn) 38 | } 39 | -------------------------------------------------------------------------------- /src/application/controllers/backend/attachment_type_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/models/tables" 5 | "github.com/xiusin/pinecms/src/common/helper" 6 | ) 7 | 8 | type AttachmentTypeController struct { 9 | BaseController 10 | } 11 | 12 | func (c *AttachmentTypeController) Construct() { 13 | c.SearchFields = []SearchFieldDsl{ 14 | {Op: "=", Field: "type"}, 15 | } 16 | c.Table = &tables.AttachmentType{} 17 | c.Entries = &[]tables.AttachmentType{} 18 | c.Group = "附件分类" 19 | c.ApiEntityName = "分类" 20 | c.BaseController.Construct() 21 | } 22 | 23 | func (c *AttachmentTypeController) PostList() { 24 | c.Orm.Find(c.Entries) 25 | helper.Ajax(c.Entries, 0, c.Ctx()) 26 | } 27 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_log.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import "github.com/silenceper/wechat/v2/officialaccount/message" 4 | 5 | type WechatLog struct { 6 | Id int64 `json:"id" xorm:"pk autoincr"` 7 | AppId string `json:"appid" xorm:"char(20)"` 8 | OpenId string `json:"openid" xorm:"varchar(32)"` 9 | Inout uint `json:"in_out" xorm:"tinyint(1)" xorm:"comment('1=来自公众号的回复,0=来自粉丝的消息')"` 10 | MsgType string `json:"msg_type" xorm:"varchar(50)"` 11 | Detail *message.MixMessage `json:"detail" xorm:"json"` 12 | CreatedAt LocalTime `json:"created_at" xorm:"created"` 13 | FansInfo *WechatMember `json:"fans_info" xrom:"-"` 14 | } 15 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/cacher.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "github.com/xiusin/pine/contracts" 5 | "time" 6 | ) 7 | 8 | type WechatTokenCacher struct { 9 | contracts.Cache 10 | } 11 | 12 | func (w WechatTokenCacher) Get(key string) any { 13 | byts, err := w.Cache.Get(key) 14 | if err != nil { 15 | return nil 16 | } 17 | return string(byts) 18 | } 19 | 20 | func (w WechatTokenCacher) Set(key string, val any, timeout time.Duration) error { 21 | return w.Cache.Set(key, []byte(val.(string)), int(timeout.Seconds())) 22 | } 23 | 24 | func (w WechatTokenCacher) IsExist(key string) bool { 25 | return w.Cache.Exists(key) 26 | } 27 | 28 | func (w WechatTokenCacher) Delete(key string) error { 29 | return w.Cache.Delete(key) 30 | } 31 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/data.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/controllers" 6 | "github.com/xiusin/pinecms/src/config" 7 | "strings" 8 | ) 9 | 10 | func SetGlobalConfigData() pine.Handler { 11 | return func(ctx *pine.Context) { 12 | settingData, err := config.SiteConfig() 13 | if err != nil { 14 | pine.Logger().Error("无法读取到配置内容:" + err.Error()) 15 | return 16 | } 17 | settingData["site_url"] = string(ctx.Host()) 18 | ctx.Set(controllers.CacheSetting, settingData) 19 | lower := map[string]string{} 20 | for k, v := range settingData { 21 | lower[strings.ToLower(k)] = v 22 | } 23 | ctx.Render().ViewData("global", lower) 24 | ctx.Next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/server/serve.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/takama/daemon" 8 | "github.com/xiusin/pinecms/src/common/helper" 9 | ) 10 | 11 | var ServeCmd = &cobra.Command{ 12 | Use: "serve", 13 | Short: "启动pinecms服务器", 14 | } 15 | 16 | func init() { 17 | ServeCmd.AddCommand(installCmd) 18 | ServeCmd.AddCommand(removeCmd) 19 | ServeCmd.AddCommand(runCmd) 20 | ServeCmd.AddCommand(startCmd) 21 | ServeCmd.AddCommand(statusCmd) 22 | ServeCmd.AddCommand(stopCmd) 23 | 24 | daemonKind := daemon.SystemDaemon 25 | if runtime.GOOS == "darwin" { 26 | daemonKind = daemon.UserAgent 27 | } 28 | srv, err := daemon.New("pinecms", "pinecms 内容管理系统服务", daemonKind) 29 | helper.PanicErr(err) 30 | serv = &Service{Daemon: srv} 31 | } 32 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "strings" 6 | ) 7 | 8 | func Cors() pine.Handler { 9 | return func(ctx *pine.Context) { 10 | hostOrigin := strings.TrimRight(string(ctx.RequestCtx.Referer()), "/") 11 | ctx.Response.Header.Set("Access-Control-Allow-Origin", hostOrigin) 12 | ctx.Response.Header.Set("Access-Control-Allow-Headers", "X-TOKEN, Content-Type, Origin, Referer, Content-Length, Access-Control-Allow-Headers, Authorization, x-requested-with") 13 | ctx.Response.Header.Set("Access-Control-Allow-Credentials", "true") 14 | ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 15 | if !ctx.IsOptions() { 16 | ctx.Next() 17 | } else { 18 | ctx.Stop() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/application/models/tables/advert.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Advert struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name" schema:"name"` 6 | SpaceID int64 `xorm:"space_id" json:"space_id" schema:"space_id"` 7 | SpaceName string `xorm:"-" json:"space_name" schema:"-"` 8 | LinkUrl string `json:"link_url" schema:"link_url"` 9 | Image string `json:"image" schema:"image"` 10 | ListOrder uint `xorm:"listorder default 0" json:"listorder" schema:"listorder"` 11 | StartTime *LocalTime `xorm:"datetime 'start_time' default NULL " json:"startTime"` 12 | EndTime *LocalTime `xorm:"datetime 'end_time' default NULL " json:"endTime"` 13 | DateRange []string `json:"date_range" schema:"-"` // 展示日期范围 14 | Status bool `json:"status" schema:"status"` // 状态 15 | } 16 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/route.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | ) 6 | 7 | func InitRouter(app *pine.Application, router *pine.Router) { 8 | app.ANY("/api/wechat/msg/:appid", msgHandler) 9 | router.Handle(new(WechatAccountController), "/wechat/account") 10 | router.Handle(new(WechatUserController), "/wechat/user") 11 | router.Handle(new(WechatMagController), "/wechat/msg") 12 | router.Handle(new(WechatQrcodeController), "/wechat/qrcode") 13 | router.Handle(new(WechatRuleController), "/wechat/rule") 14 | router.Handle(new(WechatMaterialController), "/wechat/material") 15 | router.Handle(new(WechatMsgTemplateController), "/wechat/template") 16 | router.Handle(new(WechatUserTagsController), "/wechat/user/tags") 17 | router.Handle(new(WechatMenuController), "/wechat/menu") 18 | } 19 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/route.go: -------------------------------------------------------------------------------- 1 | package webssh 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pine/render/engine/ptemplate" 6 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/middleware" 7 | "strings" 8 | ) 9 | 10 | func InitRouter(app *pine.Application, router *pine.Router) { 11 | InitInstall(app, urlPath("static"), "webssh/static") 12 | engine := ptemplate.New("webssh/view", ".html", true) 13 | 14 | pine.RegisterViewEngine(engine) 15 | 16 | webssh := app.Group(urlPath(), middleware.Auth()) 17 | { 18 | webssh.Handle(new(SshController), "/ui") 19 | webssh.Handle(new(ApiController), "/v1") 20 | } 21 | } 22 | 23 | func urlPath(str ...string) string { 24 | if len(str) == 0 { 25 | return "/webssh" 26 | } 27 | return "/webssh/" + strings.TrimPrefix(str[0], "/") 28 | } -------------------------------------------------------------------------------- /src/application/controllers/frontend/fesc_controller.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models" 6 | ) 7 | 8 | type FescController struct { 9 | pine.Controller 10 | } 11 | 12 | func (c *FescController) RegisterRoute(b pine.IRouterWrapper) { 13 | // 必须放到最后 否则搜索路由时会优先被此路由拦截到 14 | b.GET("/fesc/down-list.go", "DownList") 15 | } 16 | 17 | func (c *FescController) DownList() { 18 | aid, _ := c.Ctx().Input().GetInt("id", 0) 19 | tid, _ := c.Ctx().Input().GetInt64("tid", 0) 20 | cat, _ := models.NewCategoryModel().GetCategoryFByIdForBE(tid) 21 | if cat == nil { 22 | c.Ctx().Abort(404) 23 | return 24 | } 25 | sess := getOrmSess(cat.Model).Where("id = ?", aid).Limit(1) 26 | data, _ := sess.QueryString() 27 | c.ViewData("data", data[0]) 28 | c.View(template("down_list.jet")) 29 | } 30 | -------------------------------------------------------------------------------- /src/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | 6 | _ "github.com/go-sql-driver/mysql" 7 | "github.com/gorilla/securecookie" 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pinecms/src/application/plugins" 10 | "github.com/xiusin/pinecms/src/router" 11 | ) 12 | 13 | func Server() { 14 | InitCache() 15 | 16 | pine.SetControllerDefaultAction("Index") 17 | router.InitApiRouter(app) 18 | router.InitStatics(app) 19 | 20 | go plugins.Init() 21 | 22 | // 内部托管任意路由 23 | router.InitRouter(app) 24 | 25 | app.Run( 26 | pine.Addr(fmt.Sprintf("%s:%d", "127.0.0.1", conf.Port)), 27 | pine.WithCookieTranscoder(securecookie.New([]byte(conf.HashKey), []byte(conf.BlockKey))), 28 | pine.WithServerName("xiusin/pinecms"), 29 | pine.WithoutStartupLog(false), 30 | pine.WithCookie(true), 31 | pine.WithMaxMultipartMemory(100*1024*1024), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/TopType.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/CloudyKit/jet" 8 | "github.com/xiusin/pinecms/src/application/models" 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | ) 11 | 12 | /** 13 | * 标签:{{yield toptype() content}}{{field.Catname}}{{end}} 14 | * 作用:获取当前页面面包屑只可适用于列表页和详情页 15 | */ 16 | func TopType(args jet.Arguments) reflect.Value { 17 | fmt.Println("arclist") 18 | 19 | if !checkArgType(&args) { 20 | return defaultArrReturnVal 21 | } 22 | typeid := getNumber(args.Get(0)) 23 | cat, err := models.NewCategoryModel().GetCategoryFByIdForBE(typeid) 24 | helper.PanicErr(err) 25 | if cat.Topid != 0 { 26 | cat, err = models.NewCategoryModel().GetCategoryFByIdForBE(cat.Topid) 27 | helper.PanicErr(err) 28 | } 29 | cat.Content = "" 30 | cat.Page = nil 31 | return reflect.ValueOf(cat) 32 | } 33 | -------------------------------------------------------------------------------- /src/application/models/wechat_member_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/models/tables" 5 | "github.com/xiusin/pinecms/src/common/helper" 6 | "xorm.io/xorm" 7 | ) 8 | 9 | type WechatMemberModel struct { 10 | orm *xorm.Engine 11 | } 12 | 13 | func NewWechatMemberModel() *WechatMemberModel { 14 | return &WechatMemberModel{orm: helper.GetORM()} 15 | } 16 | 17 | func (w *WechatMemberModel) GetList(page, limit int64) (list []tables.WechatMember, total int64) { 18 | offset := (page - 1) * limit 19 | total, _ = w.orm.Limit(int(limit), int(offset)).FindAndCount(&list) 20 | if list == nil { 21 | list = []tables.WechatMember{} 22 | } 23 | return list, total 24 | } 25 | 26 | func (w *WechatMemberModel) GetInfo(id int64) tables.WechatMember { 27 | var member tables.WechatMember 28 | w.orm.Where("id = ?", id).Get(&member) 29 | return member 30 | } 31 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_msg_reply_rule.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type WechatMsgReplyRule struct { 4 | Id int64 `json:"id"` 5 | AppId string `json:"appid" xorm:"char(20) appid"` 6 | RuleName string `json:"ruleName"` 7 | MatchValue string `json:"matchValue"` 8 | ExactMatch bool `json:"exactMatch" xorm:"comment('是否精确匹配')"` 9 | ReplyType string `json:"replyType"` 10 | ReplyContent string `json:"replyContent"` 11 | Status bool `json:"status"` 12 | Desc string `json:"desc"` 13 | EffectTimeStart string `json:"effectTimeStart" xorm:"time"` 14 | EffectTimeEnd string `json:"effectTimeEnd" xorm:"time"` 15 | Priority uint `json:"priority"` 16 | CreatedAt LocalTime `json:"created_at" xorm:"created"` 17 | UpdatedAt LocalTime `json:"updated_at" xorm:"updated"` 18 | } 19 | -------------------------------------------------------------------------------- /src/application/controllers/backend/level_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/models/tables" 5 | "xorm.io/xorm" 6 | ) 7 | 8 | type LevelController struct { 9 | BaseController 10 | } 11 | 12 | func (c *LevelController) Construct() { 13 | c.Table = &tables.Level{} 14 | c.Entries = &[]tables.Level{} 15 | c.Group = "系统管理" 16 | c.SubGroup = "职级管理" 17 | c.ApiEntityName = "职级" 18 | c.BaseController.Construct() 19 | c.KeywordsSearch = []SearchFieldDsl{ 20 | {Field: "name", Op: "="}, 21 | } 22 | c.SelectOp = func(session *xorm.Session) { 23 | session.Where("status = ?", 1) 24 | } 25 | 26 | c.UniqCheckOp = func(i int, session *xorm.Session) { 27 | entity := c.Table.(*tables.Level) 28 | if i == OpAdd { 29 | session.Where("name = ?", entity.Name) 30 | } else { 31 | session.Where("name = ?", entity.Name).Where("id <> ?", entity.Id) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_user.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | // WechatMember 微信粉丝 4 | type WechatMember struct { 5 | Id int64 `xorm:"pk autoincr" json:"id"` 6 | Appid string `json:"appid"` 7 | Openid string `json:"openid"` 8 | Phone string `json:"phone"` 9 | Nickname string `json:"nickname"` 10 | Sex int `json:"sex"` 11 | City string `json:"city"` 12 | Province string `json:"province"` 13 | Headimgurl string `json:"headimgurl"` 14 | SubscribeTime string `json:"subscribe_time"` 15 | Subscribe bool `json:"subscribe"` 16 | Unionid string `json:"unionid"` 17 | Remark string `json:"remark"` 18 | TagidList []int32 `json:"tagid_list" xorm:"json"` 19 | SubscribeScene string `json:"subscribe_scene"` 20 | QrSceneStr string `json:"qr_scene_str"` 21 | Poster string `json:"poster"` 22 | } 23 | -------------------------------------------------------------------------------- /src/application/models/tables/link.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Link struct { 4 | Id int64 `xorm:"int(11) autoincr not null pk 'id'" json:"id" schema:"id"` 5 | Linktype int `xorm:"tinyint(3) not null 'linktype'" json:"linktype" schema:"linktype"` 6 | Name string `xorm:"varchar(50) not null 'name'" json:"name" schema:"name"` 7 | Url string `xorm:"varchar(255) not null 'url'" json:"url" schema:"url"` 8 | Logo string `xorm:"varchar(100) not null 'logo'" json:"logo" schema:"logo"` 9 | Introduce string `xorm:"varchar(255) not null 'introduce'" json:"introduce" schema:"introduce"` 10 | Listorder int64 `xorm:"int(11) not null 'listorder'" json:"listorder" schema:"listorder"` 11 | Passed int `xorm:"tinyint(1) not null default '0' 'passed'" json:"passed" schema:"passed"` 12 | Addtime LocalTime `xorm:"datetime default 'null' 'addtime'" json:"addtime" schema:"addtime"` 13 | } 14 | -------------------------------------------------------------------------------- /src/application/controllers/backend/errlog_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/controllers/middleware/apidoc" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | ) 8 | 9 | type ErrorLogController struct { 10 | BaseController 11 | } 12 | 13 | func (c *ErrorLogController) Construct() { 14 | c.Group = "系统日志" 15 | c.KeywordsSearch = []SearchFieldDsl{ 16 | {Field: "message", Op: "LIKE", DataExp: "%$?%"}, 17 | } 18 | c.Table = &tables.Log{} 19 | c.Entries = &[]tables.Log{} 20 | c.apiEntities = map[string]apidoc.Entity{ 21 | "list": {Title: "日志列表", Desc: "查询所有系统日志列表"}, 22 | "clear": {Title: "清空日志", Desc: "一键清理所有日志"}, 23 | } 24 | c.BaseController.Construct() 25 | } 26 | 27 | func (c *ErrorLogController) PostClear() { 28 | _, _ = c.Orm.Where("id > 0").Delete(c.Table) 29 | helper.Ajax("清理成功", 0, c.Ctx()) 30 | } 31 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/common/core/helper.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/fasthttp/websocket" 8 | "github.com/xiusin/pine" 9 | ) 10 | 11 | func JsonError(c *pine.Context, msg any) { 12 | c.WriteJSON(pine.H{"ok": false, "msg": msg}) 13 | } 14 | 15 | func HandleError(c *pine.Context, err error) bool { 16 | if err != nil { 17 | //logrus.WithError(err).Error("gin context http handler error") 18 | JsonError(c, err.Error()) 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | func WshandleError(ws *websocket.Conn, err error) bool { 25 | if err != nil { 26 | log.Println("handler ws ERROR:", err.Error()) 27 | dt := time.Now().Add(time.Second) 28 | if err := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); err != nil { 29 | log.Println("websocket writes control message failed:", err.Error()) 30 | } 31 | return true 32 | } 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/myad.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "github.com/CloudyKit/jet" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | /** 12 | 广告标签 13 | {{yield myad(id="", name="")}} 14 | 返回一组广告 15 | */ 16 | func MyAd(args jet.Arguments) reflect.Value { 17 | if !checkArgType(&args) { 18 | return defaultSignalVal 19 | } 20 | id := int(getNumber(args.Get(0))) 21 | name := args.Get(1).String() 22 | now := time.Now().In(helper.GetLocation()).Format(helper.TimeFormat) 23 | orm := helper.GetORM().Where("status = 1").Where("start_time <= ?", now).Where("end_time >= ?", now).Select("id, name, image, link_url") 24 | if id > 0 { 25 | orm.ID(id) 26 | } 27 | if id == 0 && name != "" { 28 | orm.Where("name = ?", name) 29 | } 30 | var advs = tables.Advert{} 31 | orm.Get(&advs) 32 | return reflect.ValueOf(advs) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/ssh_controller.go: -------------------------------------------------------------------------------- 1 | package webssh 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/controllers/backend" 5 | ) 6 | 7 | type SshController struct { 8 | backend.BaseController 9 | } 10 | 11 | func (c *SshController) Index() { 12 | c.Render().HTML("index.html") 13 | } 14 | 15 | func (c *SshController) GetLogin() { 16 | c.Render().HTML("index.html") 17 | } 18 | 19 | func (c *SshController) GetConsole() { 20 | c.Render().HTML("console.html") 21 | } 22 | 23 | func (c *SshController) GetServers() { 24 | c.Render().HTML("s_list.html") 25 | } 26 | 27 | func (c *SshController) GetAdd() { 28 | c.Render().HTML("add.html") 29 | } 30 | 31 | func (c *SshController) GetSetpass() { 32 | c.Render().HTML("reset.html") 33 | } 34 | 35 | func (c *SshController) GetOpenterm() { 36 | c.Render().HTML("open_term.html") 37 | } 38 | 39 | func (c *SshController) GetTerm() { 40 | c.Render().HTML("term.html") 41 | } 42 | -------------------------------------------------------------------------------- /src/application/controllers/service_key.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "xorm.io/xorm" 4 | 5 | var ServiceXorm = &xorm.Engine{} 6 | 7 | const ServiceConfig = "pinecms.config" 8 | const ServiceSiteConfig = "pinecms.site.config" 9 | const ServiceICache = "cache.AbstractCache" 10 | const ServiceTablePrefix = "pinecms.table_prefix" 11 | const ServiceJetEngine = "pinecms.jet" 12 | const ServiceCasbinEnforcer = "pinecms.casbin.enforcer" 13 | const ServiceCasbinClearPolicy = "pinecms.casbin.enforcer.policy.clear" 14 | const ServiceCasbinAddPolicy = "pinecms.casbin.enforcer.policy.add" 15 | const ServiceUploader = "pinecms.uploader" 16 | const ServiceUploaderEngine = "pinecms.uploader.%s" 17 | const ServiceApplication = "pinecms.application" 18 | const ServiceBackendRouter = "pinecms.router.backend" 19 | const ServiceCatUrlPrefixFunc = "pinecms.cat.url.prefix.func" 20 | const ServiceSearchName = "pinecms.search.engine" 21 | 22 | // 允许插件自动注册上传驱动 并注册服务进DI 23 | // DI内自动获取选中 (根据驱动名称) 驱动 24 | -------------------------------------------------------------------------------- /src/common/storage/storage_intf.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "runtime" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Uploader interface { 11 | Upload(storageName string, LocalFile io.Reader) (string, error) 12 | List(dir string) (list []File, err error) 13 | Exists(name string) (bool, error) 14 | GetFullUrl(name string) string 15 | Remove(name string) error 16 | GetEngineName() string 17 | Content(string) ([]byte, error) 18 | Rename(string, string) error 19 | Mkdir(string) error 20 | Rmdir(string) error 21 | } 22 | 23 | type File struct { 24 | Id string `json:"id"` 25 | FullPath string `json:"full_path"` 26 | Name string `json:"name"` 27 | Size int64 `json:"size"` 28 | Ctime time.Time `json:"ctime"` 29 | IsDir bool `json:"is_dir"` 30 | } 31 | 32 | func getAvailableUrl(path string) string { 33 | if runtime.GOOS == "windows" { 34 | path = strings.ReplaceAll(path, "\\", "/") 35 | } 36 | return path 37 | } 38 | -------------------------------------------------------------------------------- /src/application/controllers/backend/ad_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pine/pointer" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | ) 8 | 9 | type AdController struct { 10 | BaseController 11 | } 12 | 13 | func (c *AdController) Construct() { 14 | c.Table = &tables.Advert{} 15 | c.Entries = &[]tables.Advert{} 16 | 17 | c.KeywordsSearch = []SearchFieldDsl{ 18 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 19 | } 20 | 21 | c.AppId = "admin" 22 | c.Group = "广告管理" 23 | c.SubGroup = "广告管理" 24 | c.ApiEntityName = "广告" 25 | c.BaseController.Construct() 26 | 27 | c.OpBefore = func(i int, a any) error { 28 | if c.IsOperate(i) { 29 | t := a.(*tables.Advert) 30 | if len(t.DateRange) == 2 { 31 | t.StartTime = pointer.To(helper.ToTableTime(t.DateRange[0])) 32 | t.EndTime = pointer.To(helper.ToTableTime(t.DateRange[1])) 33 | } 34 | } 35 | return nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/application/models/document_field_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/models/tables" 5 | "github.com/xiusin/pinecms/src/common/helper" 6 | "xorm.io/xorm" 7 | ) 8 | 9 | type DocumentModelFieldModel struct { 10 | orm *xorm.Engine 11 | } 12 | 13 | func NewDocumentModelFieldModel() *DocumentModelFieldModel { 14 | return &DocumentModelFieldModel{orm: helper.GetORM()} 15 | } 16 | 17 | func (w *DocumentModelFieldModel) GetList(page, limit int64) (list []*tables.DocumentModelField, total int64) { 18 | offset := (page - 1) * limit 19 | total, _ = w.orm.Limit(int(limit), int(offset)).FindAndCount(&list) 20 | return list, total 21 | } 22 | 23 | func (w *DocumentModelFieldModel) GetMap() map[int64]*tables.DocumentModelField { 24 | var list []*tables.DocumentModelField 25 | var mapList = map[int64]*tables.DocumentModelField{} 26 | _ = w.orm.Find(&list) 27 | for _, v := range list { 28 | mapList[v.Id] = v 29 | } 30 | return mapList 31 | } 32 | -------------------------------------------------------------------------------- /src/application/models/tables/document_model.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type DocumentModel struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"table_name" xorm:"comment('模型名') varchar(100)"` 6 | Table string `json:"table" xorm:"comment('模型表名') varchar(100)"` 7 | Enabled int `json:"enabled"` 8 | ModelType int64 `json:"model_type" xorm:"comment('模型类型: 1=系统模型 0=普通模型')"` 9 | FeTplIndex string `json:"fe_tpl_index" xorm:"comment('前端列表主页模板') varchar(70)"` 10 | FeTplList string `json:"fe_tpl_list" xorm:"comment('前端列表模板') varchar(70)"` 11 | FeTplDetail string `json:"fe_tpl_detail" xorm:"comment('前端详情模板') varchar(70)"` 12 | Remark string `json:"remark" api:"remark:备注" xorm:"comment('备注') text"` 13 | CreatedAt *LocalTime `json:"created"` 14 | UpdatedAt *LocalTime `json:"updated"` 15 | Execed bool `json:"execed" xorm:"comment('是否已执行') tinyint(1)"` 16 | DeletedAt *LocalTime `xorm:"deleted" json:"-"` 17 | } 18 | -------------------------------------------------------------------------------- /src/application/controllers/backend/filemanager/tables/file_manager_account.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/xiusin/pinecms/src/common/helper" 9 | ) 10 | 11 | type FileManagerAccount struct { 12 | Id int64 `json:"id"` 13 | Nickname string `json:"nickname" xorm:"nickname"` 14 | Username string `json:"username" xorm:"username"` 15 | Password string `json:"pwd" xorm:"pwd"` 16 | Salt string `json:"-" xorm:"salt"` 17 | Disk string `json:"disk" xorm:"disk"` 18 | Engine string `json:"engine" xorm:"engine"` 19 | } 20 | 21 | func (c *FileManagerAccount) Init() error { 22 | if c.Username == "" { 23 | return fmt.Errorf("用户不能为空") 24 | } 25 | if c.Salt == "" { 26 | c.Salt = helper.GetRandomString(4) 27 | } 28 | return nil 29 | } 30 | 31 | func (c *FileManagerAccount) GetMd5Pwd(pwd string) string { 32 | hash := md5.New() 33 | hash.Write([]byte(pwd + c.Salt)) 34 | return hex.EncodeToString(hash.Sum(nil)) 35 | } 36 | -------------------------------------------------------------------------------- /resources/configs/application.yml: -------------------------------------------------------------------------------- 1 | debug: true 2 | port: 2019 3 | view: 4 | reload: false 5 | fedirname: "./resources/themes/" 6 | bedirname: "./resources/views/" 7 | theme: "default" 8 | session: 9 | name: "gosessionid" 10 | expires: 0 11 | 12 | runtime_path: "runtime" 13 | cache_db: "cache.db" 14 | log_path: "logs" 15 | 16 | plugin_enable: false 17 | plugin_path: "plugins" 18 | 19 | favicon: "./resources/assets/favicon.ico" 20 | charset: "UTF-8" 21 | jwtkey: "jwt_token_you_need_set_again" 22 | hashkey: "the-big-and-secret-fash-key-here" # 只支持固定大小到字节, AES only supports key sizes of 16, 24 or 32 bytes. 23 | blockkey: "lot-secret-of-characters-big-too" 24 | max_bodysize: 32 # MB 25 | 26 | upload: 27 | engine: "oss" #oss or file 28 | base_path: "uploads" # 基本路径 29 | 30 | statics: 31 | - { route: "/assets/", path: "./resources/assets/"} 32 | - { route: "/uploads/", path: "./resources/assets/uploads/"} 33 | 34 | 35 | search: 36 | url: http://127.0.0.1:4080 37 | username: admin 38 | password: 123456 39 | -------------------------------------------------------------------------------- /src/application/models/tables/setting.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Setting struct { 4 | Id uint `json:"id" xorm:"pk autoincr"` 5 | Key string `xorm:"unique comment('配置KEY') varchar(50)" json:"key" schema:"key"` 6 | FormName string `json:"form_name" xorm:"comment('名称') varchar(100)" schema:"form_name"` 7 | Value string `json:"value" schema:"value" xorm:"comment('配置值') text"` 8 | Group string `json:"group" schema:"group" xorm:"comment('所属分组')"` 9 | Default string `json:"default" schema:"default" xorm:"comment('默认值')"` 10 | Listorder uint `json:"listorder" xorm:"comment('列表排序')"` 11 | Remark string `json:"remark" xorm:"comment('配置描述')"` 12 | Editor string `json:"editor" schema:"editor"` 13 | Extra string `json:"extra" xorm:"-" schema:"extra"` 14 | Options map[string]any `json:"options" xorm:"json"` 15 | } 16 | 17 | type KV struct { 18 | Label string `json:"label"` 19 | Value any `json:"value"` 20 | } 21 | -------------------------------------------------------------------------------- /src/application/models/document_dsl_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type DocumentModelDslModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewDocumentFieldDslModel() *DocumentModelDslModel { 15 | return &DocumentModelDslModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (w *DocumentModelDslModel) GetList(mid int64) []tables.DocumentModelDsl { 19 | var list []tables.DocumentModelDsl 20 | err := w.orm.Where("mid = ?", mid).Asc("listorder").Asc("id").Find(&list) 21 | if err != nil { 22 | pine.Logger().Error("NewDocumentFieldDslModel: ", err) 23 | } 24 | return list 25 | } 26 | 27 | func (w *DocumentModelDslModel) DeleteByMID(mid int64) bool { 28 | _, err := w.orm.Where("mid=?", mid).Delete(&tables.DocumentModelDsl{}) 29 | if err != nil { 30 | pine.Logger().Error("DocumentModelDslModel.DeleteByMID", err) 31 | return false 32 | } 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/type.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/CloudyKit/jet" 6 | "github.com/xiusin/pine" 7 | "github.com/xiusin/pinecms/src/application/models" 8 | "github.com/xiusin/pinecms/src/application/models/tables" 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | "reflect" 11 | "runtime/debug" 12 | ) 13 | 14 | func Type(args jet.Arguments) reflect.Value { 15 | fmt.Println("arclist") 16 | 17 | if !checkArgType(&args) { 18 | return defaultArrReturnVal 19 | } 20 | defer func() { 21 | if err := recover(); err != nil { 22 | pine.Logger().Error("Type Failed", string(debug.Stack())) 23 | } 24 | }() 25 | catid := getNumber(args.Get(0)) 26 | if catid < 0 { 27 | panic("typeid参数不能小于1") 28 | } 29 | orm := helper.GetORM() 30 | var data = &tables.Category{} 31 | sess := orm.Table(data) 32 | defer sess.Close() 33 | exists, _ := sess.ID(catid).Get(data) 34 | if exists && data.Type != 2 { 35 | data.Url = fmt.Sprintf("/%s/", models.NewCategoryModel().GetUrlPrefix(data.Catid)) 36 | } 37 | return reflect.ValueOf(data) 38 | } 39 | -------------------------------------------------------------------------------- /src/application/models/page_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type PageModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewPageModel() *PageModel { 15 | return &PageModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (p *PageModel) AddPage(page *tables.Page) bool { 19 | res, _ := p.orm.Insert(page) 20 | return res != 0 21 | } 22 | 23 | func (p *PageModel) UpdatePage(page *tables.Page) bool { 24 | res, _ := p.orm.Where("catid=?", page.Id).Update(page) 25 | return res != 0 26 | } 27 | 28 | func (p *PageModel) DelPage(catid int64) bool { 29 | res, _ := p.orm.Delete(&tables.Page{Id: catid}) 30 | return res != 0 31 | } 32 | 33 | func (p *PageModel) GetPage(catid int64) *tables.Page { 34 | page := &tables.Page{Id: catid} 35 | exists, err := p.orm.Get(page) 36 | if err != nil { 37 | pine.Logger().Error("获取page信息失败:", err) 38 | } 39 | if exists { 40 | return page 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /src/application/models/tables/admin.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Admin struct { 4 | Userid int64 `xorm:"pk autoincr 'id'" json:"id"` 5 | Username string `json:"username"` 6 | Password string `json:"password,omitempty"` 7 | Roleid []int64 `json:"roleid" xorm:"-"` 8 | Encrypt string `json:"-"` 9 | Lastloginip string `json:"lastloginip"` 10 | Lastlogintime int64 `json:"lastlogintime"` 11 | Email string `json:"email"` 12 | Realname string `json:"realname"` 13 | Avatar string `json:"avatar"` 14 | Remark string `json:"remark"` 15 | RoleName string `json:"roleName" xorm:"-"` 16 | Phone string `json:"phone"` 17 | Status uint `json:"status"` 18 | PositionId uint `json:"position_id"` 19 | LevelId uint `json:"level_id"` 20 | DepartmentId uint `json:"department_id"` 21 | Birthday *LocalTime `json:"birthday"` 22 | Sex uint `json:"sex"` 23 | 24 | RoleIdList []int64 `json:"roleIdList" xorm:"json roles"` 25 | } 26 | -------------------------------------------------------------------------------- /src/application/controllers/frontend/index.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/xiusin/pine" 8 | "github.com/xiusin/pine/render/engine/pjet" 9 | "github.com/xiusin/pinecms/src/application/controllers" 10 | ) 11 | 12 | func (c *IndexController) Index() { 13 | c.setTemplateData() 14 | indexPage := "editor.tpl" 15 | pageFilePath := GetStaticFile(indexPage) 16 | _ = os.MkdirAll(filepath.Dir(pageFilePath), os.ModePerm) 17 | f, err := os.OpenFile(pageFilePath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.ModePerm) 18 | if err != nil { 19 | c.Logger().Error(err.Error()) 20 | return 21 | } 22 | defer f.Close() 23 | jet := pine.Make(controllers.ServiceJetEngine).(*pjet.PineJet) 24 | temp, err := jet.GetTemplate(template("index.jet")) 25 | if err != nil { 26 | c.Logger().Error(err.Error()) 27 | return 28 | } 29 | err = temp.Execute(f, viewDataToJetMap(c.Render().GetViewData()), nil) 30 | if err != nil { 31 | c.Logger().Error(err.Error()) 32 | return 33 | } 34 | data, _ := os.ReadFile(pageFilePath) 35 | 36 | _ = c.Ctx().WriteHTMLBytes(data) 37 | } 38 | -------------------------------------------------------------------------------- /src/application/models/tables/document_model_field.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type DocumentModelField struct { 4 | Id int64 `json:"id" xorm:"id pk autoincr"` 5 | Name string `json:"name" xorm:"comment('组件名称') varchar(30)"` 6 | Type string `json:"type" xorm:"comment('数据库字段类型') varchar(20)"` 7 | Desc string `json:"desc" xorm:"comment('组件使用场景描述') text"` 8 | ListComp string `json:"list_comp" xorm:"comment('列表渲染组件') text"` 9 | FormComp string `json:"form_comp" xorm:"comment('对应vue组件') varchar(30)"` 10 | Props string `json:"props" xorm:"comment('属性配置') text"` 11 | } 12 | 13 | const ( 14 | FieldTypeNull = iota 15 | FieldTypeInput 16 | FieldTypeMulInput 17 | FieldTypeEditorQuill 18 | FieldTypeAttachment 19 | FieldTypeSelect 20 | FieldTypeCascader 21 | FieldTypeRadio 22 | FieldTypeCheckbox 23 | FieldTypeInputNumberInt 24 | FieldTypeInputNumberFloat 25 | FieldTypeImageUpload 26 | FieldTypeImageMulUpload 27 | FieldTypeSwitch 28 | FieldTypeDate 29 | FieldTypeTags 30 | FieldTypeFlag 31 | FieldTypeMarkdown 32 | FieldTypeCodeEditor 33 | FieldTypeRate 34 | FieldTypeUeditor 35 | ) 36 | -------------------------------------------------------------------------------- /src/application/controllers/cache_key.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | const CacheTheme = "theme" 4 | const CacheStatistics = "pinecms_persist.statistics" 5 | const CacheRefer = "statistics_refer" 6 | const CacheMemCollect = "pinecms.mem.collect" 7 | const CacheAdminMenuByRoleIdAndMenuId = "pinecms.admin_menu_%d_%d" 8 | const CacheAdminPriv = "pinecms.admin_priv_%d" 9 | const CacheSetting = "pinecms.setting" 10 | const CacheFeTplList = "pinecms.fe_tpl_list" 11 | const CacheDocumentModelPrefix = "pinecms.document_model.%d" 12 | const CacheModels = "pinecms.models" 13 | const CacheCategories = "pinecms.categories" 14 | const CacheCategoryPosPrefix = "pinecms.category.pos.%d" 15 | const CacheCategoryInfoPrefix = "pinecms.category.%d" 16 | const CacheCategoryContentPrefix = "pinecms.content.%d_%d" 17 | const CacheModelTablePrefix = "pinecms.model.table.%d" 18 | const CacheMysqlVersion = "pinecms.mysql.version" 19 | const CacheAdminRoles = "pinecms.admin.roles" 20 | const CacheTableNameFields = "pinecms.orm.tbl.%s.fields" 21 | const CacheAdminRolesList = "pinecms.admin.roles.%d" 22 | const CacheTableNames = "pinecms.orm.table.name" 23 | 24 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/apidoc/entity.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "github.com/fatih/structs" 5 | "github.com/xiusin/pine" 6 | ) 7 | 8 | type Entity struct { 9 | Title string // 标题 10 | Desc string // 描述 11 | ApiParam any // 参数 12 | AppId string // 应用id 13 | Group string // 分组名称 14 | SubGroup string // 子分组名称 15 | Tag []string // 标签 16 | } 17 | 18 | // SetApiEntity 设置接口实体信息 19 | func SetApiEntity(ctx *pine.Context, entity *Entity, configures ...Configure) { 20 | if entity.ApiParam != nil && !structs.IsStruct(entity.ApiParam) { 21 | ctx.Logger().Warn("不支持非struct类型的请求参数") 22 | } else if defaultConfig.Enable { 23 | e, ok := ctx.Value(apiDocKey).(*apiEntity) 24 | if ok { 25 | e.Title = entity.Title 26 | e.Desc = entity.Desc 27 | e.configured = true 28 | e.Group = apiGroup{Title: entity.Group, Name: entity.Group} 29 | e.SubGroup = entity.SubGroup 30 | for _, configure := range configures { 31 | configure(e) 32 | } 33 | e.AppId = entity.AppId 34 | e.Param, _ = parseInterface(entity.ApiParam) 35 | e.FilterParams() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/common/jwt.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/golang-jwt/jwt" 7 | ) 8 | 9 | var jwtKet = []byte("config.Config.Jwt.Key") 10 | 11 | type Claims struct { 12 | Userid int64 13 | jwt.StandardClaims 14 | } 15 | 16 | func ReleaseToken(id int64) (token string, err error) { 17 | expireTime := time.Now().Add(3 * 24 * time.Hour) 18 | claims := &Claims{ 19 | Userid: id, 20 | StandardClaims: jwt.StandardClaims{ 21 | ExpiresAt: expireTime.Unix(), 22 | IssuedAt: time.Now().Unix(), 23 | Issuer: "admin", 24 | Subject: "user", 25 | }, 26 | } 27 | tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 28 | token, err = tokenObj.SignedString(jwtKet) 29 | return 30 | } 31 | 32 | func ParseToken(token string) (*Claims, error) { 33 | tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (any, error) { 34 | return jwtKet, nil 35 | }) 36 | 37 | if tokenClaims != nil { 38 | if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid { 39 | return claims, nil 40 | } 41 | } 42 | return nil, err 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/flink.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/CloudyKit/jet" 9 | "github.com/xiusin/pine" 10 | "github.com/xiusin/pinecms/src/application/models/tables" 11 | "github.com/xiusin/pinecms/src/common/helper" 12 | ) 13 | 14 | func Flink(args jet.Arguments) reflect.Value { 15 | if !checkArgType(&args) { 16 | return defaultArrReturnVal 17 | } 18 | defer func() { 19 | if err := recover(); err != nil { 20 | pine.Logger().Warn(fmt.Sprintf("flink panic: %v", err)) 21 | } 22 | }() 23 | orm := helper.GetORM() 24 | sess := orm.Table(&tables.Link{}) 25 | defer sess.Close() 26 | row := int(args.Get(0).Float()) 27 | if row == 0 { 28 | row = 10 29 | sess.Limit(row) 30 | } 31 | 32 | idParam := args.Get(1).String() 33 | if len(idParam) != 0 { 34 | ids := strings.Split(idParam, ",") 35 | sess.In("id", ids) 36 | } 37 | 38 | sort := args.Get(2).String() 39 | if len(sort) != 0 { 40 | sess.OrderBy(sort) 41 | } else { 42 | sess.Desc("id") 43 | } 44 | data := []tables.Link{} 45 | helper.PanicErr(sess.Find(&data)) 46 | return reflect.ValueOf(data) 47 | } 48 | -------------------------------------------------------------------------------- /src/application/controllers/backend/log_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/controllers/middleware/apidoc" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | ) 8 | 9 | type LogController struct { 10 | BaseController 11 | } 12 | 13 | func (c *LogController) Construct() { 14 | c.Group = "操作日志" 15 | 16 | c.KeywordsSearch = []SearchFieldDsl{ 17 | {Field: "username", Op: "LIKE", DataExp: "%$?%"}, 18 | {Field: "ip", Op: "LIKE", DataExp: "%$?%"}, 19 | {Field: "uri", Op: "LIKE", DataExp: "%$?%"}, 20 | {Field: "params", Op: "LIKE", DataExp: "%$?%"}, 21 | } 22 | c.SearchFields = []SearchFieldDsl{ 23 | {Field: "method"}, 24 | } 25 | c.Table = &tables.RequestLog{} 26 | c.Entries = &[]tables.RequestLog{} 27 | c.apiEntities = map[string]apidoc.Entity{ 28 | "list": {Title: "日志列表", Desc: "查询系统接口请求日志列表"}, 29 | "clear": {Title: "清空日志", Desc: "一键清理系统所有日志"}, 30 | } 31 | c.BaseController.Construct() 32 | } 33 | 34 | func (c *LogController) PostClear() { 35 | _, _ = c.Orm.Where("id > 0").Delete(c.Table) 36 | helper.Ajax("清理成功", 0, c.Ctx()) 37 | } 38 | -------------------------------------------------------------------------------- /src/application/controllers/frontend/click.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | func (c *IndexController) Click() { 4 | 5 | //aid, _ := c.Ctx().Input().GetInt64("aid") 6 | //tid, _ := c.Ctx().Input().GetInt64("tid") 7 | //if aid < 1 || tid < 1 { 8 | // c.Ctx().Abort(http.StatusNotFound) 9 | // return 10 | //} 11 | //clickCache := fmt.Sprintf("click_%d_%d", tid, aid) 12 | //info := c.Ctx().GetCookie(clickCache) 13 | //if len(info) == 0 { 14 | // res, err := di.MustGet("orm").(*xorm.Engine).Table(models.NewCategoryModel().GetTable(tid)).ID(aid).Incr("visit_count").Exec() 15 | // if err != nil { 16 | // logger.Error("无法更新点击数据", err) 17 | // return 18 | // } 19 | // if affe, _ := res.RowsAffected(); affe > 0 { 20 | // c.Ctx().SetCookie(clickCache, "1", 0) 21 | // } 22 | //} 23 | } 24 | 25 | func (c *IndexController) GetClick() { 26 | //aid, _ := c.Ctx().Input().GetInt64("aid") 27 | //tid, _ := c.Ctx().Input().GetInt64("tid") 28 | //if aid < 1 || tid < 1 { 29 | // c.Ctx().Abort(http.StatusNotFound) 30 | // return 31 | //} 32 | //fmt.Println(di.MustGet("orm").(*xorm.Engine).Table(models.NewCategoryModel().GetTable(tid)).ID(aid).Select("visit_count").QueryString()) 33 | } 34 | -------------------------------------------------------------------------------- /tests/ast_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "go/parser" 7 | "go/printer" 8 | "go/token" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestAST(t *testing.T) { 14 | filePath := "../src/router/module.go" 15 | set := token.NewFileSet() 16 | content, _ := os.ReadFile(filePath) 17 | 18 | node, err := parser.ParseFile(set, filePath, content, parser.AllErrors) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | // TODO 动态解析代码,找到 InitModuleRouter 函数,然后在函数体中添加新的代码 24 | ast.Inspect(node, func(n ast.Node) bool { 25 | // 查找函数声明节点 26 | if fn, ok := n.(*ast.FuncDecl); ok && fn.Name.Name == "InitModuleRouter" { 27 | // 创建新的语句 28 | newStmt := &ast.ExprStmt{ 29 | X: &ast.CallExpr{ 30 | Fun: ast.NewIdent("fmt.Println"), 31 | Args: []ast.Expr{ast.NewIdent("\"New code added to InitModuleRouter\"")}, 32 | }, 33 | } 34 | // 将新语句追加到函数体中 35 | fn.Body.List = append(fn.Body.List, newStmt) 36 | 37 | // 打印修改后的代码 38 | var buf bytes.Buffer 39 | printer.Fprint(&buf, set, node) 40 | os.WriteFile(filePath, buf.Bytes(), 0644) // webssh.InitRouter(app, backendRouter) 41 | } 42 | 43 | return true 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/application/models/tables/content.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Content struct { 4 | Id int64 `form:"id" json:"id"` 5 | Catid int64 `form:"catid" json:"catid"` 6 | Title string `form:"title" json:"title"` 7 | Thumb string `form:"thumb" json:"thumb"` 8 | Keywords string `form:"keywords" json:"keywords"` 9 | Description string `form:"description" json:"description"` 10 | Content string `form:"content" json:"content"` 11 | Listorder int64 `form:"listorder" json:"listorder"` 12 | Status int64 `form:"status" json:"status"` 13 | Recommend int64 `form:"recommend" json:"recommend"` 14 | PwdType int64 `form:"pwd_type" json:"pwd_type"` 15 | Money int64 `form:"money" json:"money"` 16 | Userid int64 `form:"userid" json:"userid"` 17 | CreatedAt int64 `form:"created_at" json:"created_at"` 18 | UpdatedAt int64 `form:"updated_at" json:"updated_at"` 19 | DeletedAt int64 `form:"deleted_at" json:"deleted_at"` 20 | SourceUrl string `form:"source_url" json:"source_url"` 21 | SourcePwd string `form:"source_pwd" json:"source_pwd"` 22 | Catids string `form:"catids" json:"catids"` 23 | Tags string `form:"tags" json:"tags"` 24 | } 25 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/query.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | 9 | "github.com/CloudyKit/jet" 10 | "github.com/xiusin/pine" 11 | "github.com/xiusin/pinecms/src/config" 12 | ) 13 | 14 | /** 15 | * 标签:{{yield query(sql="") content}} {{end}} 16 | * 作用:特殊标签,SQL查询标签 17 | * 用法示例: {{yield query(sql="SELECT * FROM #@_tables") content}} .. HTML ..{{end}} 18 | * 参数说明: 19 | * sql SQL语句,只用于select类型语句 20 | */ 21 | func Query(args jet.Arguments) reflect.Value { 22 | if !checkArgType(&args) { 23 | return defaultArrReturnVal 24 | } 25 | defer func() { 26 | if err := recover(); err != nil { 27 | pine.Logger().Error("Query Failed", err) 28 | } 29 | }() 30 | sess := helper.GetORM() 31 | query := strings.Trim(args.Get(0).String(), " \n\t") 32 | // 只允许查询操作 33 | conf := config.DB() 34 | if strings.HasPrefix(query, "SELECT") || strings.HasPrefix(query, "select") { 35 | rest, err := sess.QueryString(strings.ReplaceAll(query, "#@_", conf.Db.DbPrefix)) 36 | helper.PanicErr(err) 37 | if rest != nil { 38 | return reflect.ValueOf(rest) 39 | } 40 | } 41 | return reflect.ValueOf([]map[string]string{}) 42 | } 43 | -------------------------------------------------------------------------------- /src/application/controllers/backend/menu_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/controllers/middleware/apidoc" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | ) 8 | 9 | type MenuController struct { 10 | BaseController 11 | } 12 | 13 | func (c *MenuController) Construct() { 14 | c.KeywordsSearch = []SearchFieldDsl{ 15 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 16 | } 17 | c.Table = &tables.Menu{} 18 | c.Entries = &[]tables.Menu{} 19 | 20 | c.AppId = "admin" 21 | c.Group = "菜单管理" 22 | c.SubGroup = "菜单管理" 23 | 24 | c.apiEntities = map[string]apidoc.Entity{ 25 | "list": {Title: "菜单列表", Desc: "获悉系统内配置的菜单列表"}, 26 | "add": {Title: "新增菜单", Desc: "新增一个菜单,可基于任何菜单添加子菜单"}, 27 | "edit": {Title: "编辑菜单", Desc: "修改给定菜单信息"}, 28 | "delete": {Title: "删除菜单", Desc: "删除指定菜单"}, 29 | "info": {Title: "菜单详情", Desc: "获取指定菜单的详情信息"}, 30 | } 31 | 32 | c.BaseController.Construct() 33 | } 34 | 35 | func (c *MenuController) PostList() { 36 | if err := c.Orm.Find(c.Entries); err != nil { 37 | helper.Ajax(err, 1, c.Ctx()) 38 | return 39 | } 40 | helper.Ajax(c.Entries, 0, c.Ctx()) 41 | } 42 | -------------------------------------------------------------------------------- /src/application/models/tables/dict_category.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import "time" 4 | 5 | type DictCategory struct { 6 | Id uint `xorm:"pk autoincr 'id'" json:"id" api:"remark:分类ID"` 7 | Key string `json:"key" xorm:"key unique notnull" api:"remark:分类标识,唯一|require:true"` 8 | Name string `json:"name" xorm:"notnull" api:"remark:分类名称|require:true"` 9 | Remark string `json:"remark" api:"remark:分类备注信息"` 10 | Status bool `json:"status" api:"remark:状态|require:true"` 11 | CreatedAt time.Time `xorm:"created"` 12 | UpdatedAt time.Time `xorm:"updated"` 13 | } 14 | 15 | type Dict struct { 16 | Id uint `xorm:"pk autoincr 'id'" json:"id" api:"remark:字典属性ID"` 17 | Cid uint `json:"cid" xorm:"notnull" api:"remark:分类ID"` 18 | CatName string `json:"cat_name" xorm:"-"` 19 | Name string `json:"name" xorm:"unique notnull" api:"remark:字典名称|require:true"` 20 | Value string `json:"value" api:"remark:字典值|require:true"` 21 | Remark string `json:"remark" api:"remark:分类备注信息"` 22 | Sort uint `json:"sort"` 23 | Status bool `json:"status"` 24 | CreatedAt time.Time `xorm:"created"` 25 | UpdatedAt time.Time `xorm:"updated"` 26 | } 27 | -------------------------------------------------------------------------------- /src/application/controllers/backend/member_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "xorm.io/builder" 8 | ) 9 | 10 | type MemberController struct { 11 | BaseController 12 | } 13 | 14 | func (c *MemberController) Construct() { 15 | c.KeywordsSearch = []SearchFieldDsl{ 16 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 17 | } 18 | c.Table = &tables.Member{} 19 | c.Entries = &[]tables.Member{} 20 | c.ApiEntityName = "会员" 21 | c.Group = "会员管理" 22 | 23 | c.BaseController.Construct() 24 | c.OpBefore = c.before 25 | } 26 | 27 | func (c *MemberController) before(act int, params any) error { 28 | switch act { 29 | case OpAdd: 30 | data := c.Table.(*tables.Member) 31 | if exist, _ := c.Orm.Table(c.Table).Where("account = ?", data.Account).Or("email = ?", data.Email).Exist(); exist { 32 | return errors.New("账号或邮箱已存在") 33 | } 34 | 35 | case OpEdit: 36 | data := c.Table.(*tables.Member) 37 | if exist, _ := c.Orm.Table(c.Table).Where("id <> ?", data.Id). 38 | Where(builder.Eq{"account": data.Account}.Or(builder.Eq{"email": data.Email})).Exist(); exist { 39 | return errors.New("账号或邮箱已存在") 40 | } 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /src/application/models/tables/wechat_template.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type WechatMsgTemplate struct { 4 | Id int64 `json:"id" xorm:"pk autoincr"` 5 | Appid string `json:"appid" xorm:"char(20) not null comment('appid')"` 6 | TemplateId string `json:"template_id" xorm:"varchar(50) not null comment('模板ID')"` 7 | Title string `json:"title" xorm:"varchar(20) comment('模板标题')"` 8 | Name string `json:"name" xorm:"varchar(20) comment('模板名称')"` 9 | PrimaryIndustry string `json:"primary_industry"` 10 | DeputyIndustry string `json:"deputy_industry"` 11 | Content string `json:"content" xorm:"text"` 12 | Data []map[string]any `json:"data" xorm:"json"` 13 | Url string `json:"url" xorm:"varchar(255)"` 14 | MiniProgram map[string]any `json:"miniprogram" xorm:"json"` 15 | Status bool `json:"status" xorm:"comment('是否有效0=无效,1=有效')"` 16 | Example string `json:"example" xorm:"comment('模板示例')"` 17 | CreatedAt LocalTime `json:"created_at" xorm:"created"` 18 | UpdatedAt LocalTime `json:"updated_at" xom:"updated"` 19 | } 20 | -------------------------------------------------------------------------------- /src/application/models/attachments_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | const ( 11 | IMG_TYPE = "img" 12 | FILE_TYPE = "file" 13 | ) 14 | 15 | type AttachmentsModel struct { 16 | orm *xorm.Engine 17 | } 18 | 19 | func NewAttachmentsModel() *AttachmentsModel { 20 | return &AttachmentsModel{orm: helper.GetORM()} 21 | } 22 | 23 | func (a *AttachmentsModel) GetList(keywords string, page, limit int64) (list []tables.Attachments, total int64) { 24 | offset := (page - 1) * limit 25 | var err error 26 | sess := a.orm.Limit(int(limit), int(offset)).Desc("id") 27 | if len(keywords) != 0 { 28 | likePrtten := "%" + keywords + "%" 29 | sess.Where("origin_name like ?", likePrtten).Or("name like ?", likePrtten) 30 | } 31 | total, err = sess.FindAndCount(&list) 32 | if err != nil { 33 | pine.Logger().Error(err.Error()) 34 | } 35 | if list == nil { 36 | list = []tables.Attachments{} 37 | } 38 | return list, total 39 | } 40 | 41 | func (a *AttachmentsModel) Delete(id int64) bool { 42 | res, _ := a.orm.ID(id).Delete(&tables.Attachments{}) 43 | return res > 0 44 | } 45 | -------------------------------------------------------------------------------- /cmd/server/service.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/takama/daemon" 10 | "github.com/xiusin/pinecms/src/common/helper" 11 | "github.com/xiusin/pinecms/src/config" 12 | "github.com/xiusin/pinecms/src/server" 13 | ) 14 | 15 | type Service struct{ daemon.Daemon } 16 | 17 | var serv *Service 18 | 19 | func (service *Service) Manage(args []string, usage string) (string, error) { 20 | if len(args) >= 1 && args[0] != "run" { 21 | switch args[0] { 22 | case "install": 23 | return service.Install() 24 | case "remove": 25 | return service.Remove() 26 | case "start": 27 | return service.Start() 28 | case "stop": 29 | return service.Stop() 30 | case "status": 31 | return service.Status() 32 | default: 33 | return usage, nil 34 | } 35 | } 36 | 37 | interrupt := make(chan os.Signal, 1) 38 | signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 39 | 40 | go func() { 41 | defer helper.Recover() 42 | config.InitDB() 43 | fmt.Println("start server...") 44 | server.Server() 45 | }() 46 | 47 | killSignal := <-interrupt 48 | if killSignal == os.Interrupt { 49 | return "Daemon was interrupted by system signal", nil 50 | } 51 | return "Daemon was killed", nil 52 | } 53 | -------------------------------------------------------------------------------- /src/common/helper/service.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/xiusin/pine" 8 | "github.com/xiusin/pine/contracts" 9 | "github.com/xiusin/pine/di" 10 | "github.com/xiusin/pine/middlewares/traceid" 11 | "github.com/xiusin/pinecms/src/application/controllers" 12 | ) 13 | 14 | // Inject 注入依赖 15 | func Inject(key any, v any, single ...bool) { 16 | if len(single) == 0 { 17 | single = append(single, true) 18 | } 19 | if vi, ok := v.(di.BuildHandler); ok { 20 | di.Set(key, vi, single[0]) 21 | } else { 22 | di.Set(key, func(_ di.AbstractBuilder) (i any, e error) { 23 | return v, nil 24 | }, single[0]) 25 | } 26 | } 27 | 28 | // Cache 获取缓存服务 29 | func Cache() contracts.Cache { 30 | return pine.Make(controllers.ServiceICache).(contracts.Cache) 31 | } 32 | 33 | // App 获取应用实例 34 | func App() *pine.Application { 35 | return pine.Make(controllers.ServiceApplication).(*pine.Application) 36 | } 37 | 38 | // Slog 获取slog对象 39 | func Slog(ctxs ...context.Context) *slog.Logger { 40 | logger := pine.Logger().(*slog.Logger) 41 | 42 | if len(ctxs) > 0 { 43 | requestID := ctxs[0].Value(traceid.Key) 44 | if requestID != nil { 45 | logger = logger.With(traceid.Key, requestID) 46 | } 47 | } 48 | return logger 49 | } 50 | -------------------------------------------------------------------------------- /src/common/template/gktemplate.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/gokeeptech/gktemplate" 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | ) 11 | 12 | type GkTemplate struct { 13 | *GkTemplateConf 14 | } 15 | 16 | type GkTemplateConf struct { 17 | NameSpace string 18 | L string 19 | R string 20 | Path string 21 | Ext string 22 | } 23 | 24 | func NewGkTemplate(conf *GkTemplateConf) *GkTemplate { 25 | if conf == nil { 26 | conf = &GkTemplateConf{"pinecms", "{", "}", "resources/themes", ".html"} 27 | } 28 | helper.PanicErr(gktemplate.LoadDir(strings.TrimRight(conf.Path, "/") + "/*" + conf.Ext)) 29 | return &GkTemplate{conf} 30 | } 31 | 32 | func (g GkTemplate) Ext() string { 33 | return g.GkTemplateConf.Ext 34 | } 35 | 36 | func (g GkTemplate) AddFunc(s string, i any) { 37 | switch i := i.(type) { 38 | case gktemplate.TagFunc: 39 | fun := map[string]gktemplate.TagFunc{s: i} 40 | gktemplate.ExtFuncs(&fun) 41 | } 42 | } 43 | 44 | func (g GkTemplate) HTML(writer io.Writer, name string, binding map[string]any) error { 45 | var str string 46 | var err error 47 | if str, err = gktemplate.ParseFile(name, binding); err == nil { 48 | _, err = fmt.Fprintf(writer, str) 49 | } 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /src/application/controllers/backend/ad_space_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | ) 8 | 9 | type AdSpaceController struct { 10 | BaseController 11 | } 12 | 13 | func (c *AdSpaceController) Construct() { 14 | c.Table = &tables.AdvertSpace{} 15 | c.Entries = &[]tables.AdvertSpace{} 16 | c.AppId = "admin" 17 | c.Group = "广告管理" 18 | c.SubGroup = "广告位管理" 19 | c.ApiEntityName = "广告位" 20 | c.BaseController.Construct() 21 | c.OpBefore = c.before 22 | } 23 | 24 | func (c *AdSpaceController) before(act int, params any) error { 25 | if act == OpDel { 26 | ids := params.(*idParams).Ids 27 | count, _ := c.Orm.In("space_id", ids).Count(&tables.Advert{}) 28 | if count > 0 { 29 | return errors.New("广告位下还有广告,无法直接删除") 30 | } 31 | } 32 | if act == OpEdit || act == OpAdd { 33 | t, p := &tables.AdvertSpace{}, params.(*tables.AdvertSpace) 34 | if act == OpAdd { 35 | if exists, _ := c.Orm.Where("name = ? or `key` = ?", p.Name, p.Key).Exist(t); exists { 36 | return errors.New("广告位名称或标识已经存在") 37 | } 38 | } else { 39 | if exists, _ := c.Orm.Where("id <> ? and (name = ? or `key` = ?)", p.Id, p.Name, p.Key).Exist(t); exists { 40 | return errors.New("广告位名称或标识已经存在") 41 | } 42 | } 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /src/application/controllers/backend/member_group_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | "xorm.io/xorm" 9 | ) 10 | 11 | type MemberGroupController struct { 12 | BaseController 13 | } 14 | 15 | func (c *MemberGroupController) Construct() { 16 | c.Table = &tables.MemberGroup{} 17 | c.Entries = &[]tables.MemberGroup{} 18 | c.ApiEntityName = "会员分组" 19 | c.Group = "会员分组管理" 20 | c.BaseController.Construct() 21 | c.OpBefore = c.before 22 | } 23 | 24 | func (c *MemberGroupController) before(act int, params any) error { 25 | switch act { 26 | case OpList: 27 | (params.(*xorm.Session)).Asc("listorder", "id") 28 | case OpAdd: 29 | data := c.Table.(*tables.MemberGroup) 30 | if exist, _ := c.Orm.Table(c.Table).Where("name = ?", data.Name).Exist(); exist { 31 | return errors.New("分组已存在") 32 | } 33 | 34 | case OpEdit: 35 | data := c.Table.(*tables.MemberGroup) 36 | if exist, _ := c.Orm.Table(c.Table).Where("id <> ?", data.Id).Where("name = ?", data.Name).Exist(); exist { 37 | return errors.New("分组已存在") 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (c *MemberGroupController) GetSelect() { 44 | c.Orm.Find(c.Entries) 45 | helper.Ajax(c.Entries, 0, c.Ctx()) 46 | } 47 | -------------------------------------------------------------------------------- /src/application/models/log_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type LogModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewLogModel() *LogModel { 15 | return &LogModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (l *LogModel) GetList(page, limit int64) ([]tables.Log, int64) { 19 | offset := (page - 1) * limit 20 | var list = []tables.Log{} 21 | var total int64 22 | total, _ = l.orm.Count(&tables.Log{}) 23 | if err := l.orm.Desc("logid").Limit(int(limit), int(offset)).Find(&list); err != nil { 24 | pine.Logger().Error(err.Error()) 25 | } 26 | return list, total 27 | } 28 | 29 | func (l *LogModel) DeleteAll() bool { 30 | res, err := l.orm.Where("1=1").Delete(&tables.Log{}) 31 | if err != nil { 32 | pine.Logger().Error(err.Error()) 33 | return false 34 | } 35 | if res > 0 { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (l *LogModel) DeleteBeforeByDate(date string) bool { 42 | res, err := l.orm.Where("`time` <= ? ", date).Delete(&tables.Log{}) 43 | if err != nil { 44 | pine.Logger().Error(err.Error()) 45 | return false 46 | } 47 | if res > 0 { 48 | return true 49 | } 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /src/application/models/tables/menu.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Menu struct { 4 | Id int64 `xorm:"pk autoincr" json:"id" api:"remark:菜单ID"` 5 | Name string `json:"name" api:"remark:菜单名称"` 6 | Parentid int64 `json:"parentId" api:"remark:父菜单ID"` 7 | PluginId int64 `json:"plugin_id" api:"remark: 插件ID,用于标记插件创建菜单,卸载时使用"` 8 | Listorder int64 `json:"orderNum" api:"remark:排序号"` 9 | Display bool `json:"isShow" api:"remark:是否显示"` 10 | Type int64 `json:"type" api:"remark:菜单类型" xorm:"comment('类型 0:目录 1:菜单 2:按钮')"` 11 | Children []Menu `json:"children" xorm:"-" form:"-"` 12 | Icon string `json:"icon" api:"remark:图标"` 13 | ViewPath string `json:"viewPath" api:"remark:视图路径"` 14 | KeepAlive bool `json:"keepAlive" api:"remark:路由缓存"` 15 | Router string `json:"router" api:"remark:路由地址"` 16 | Perms string `json:"perms" xorm:"comment('权限标识')"` 17 | Identification string `json:"identification" xorm:"comment('权限标识, 查询时唯一索引') unique"` 18 | 19 | //C string `json:"c" form:"c" api:"remark:控制器(旧版)"` 20 | //A string `json:"a" form:"a" api:"remark:操作(旧版)"` 21 | //Data string `json:"data" form:"data" api:"remark:菜单附加参数"` 22 | //IsSystem uint64 `json:"is_system" form:"is_system" api:"remark:是否为系统菜单"` 23 | } 24 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/xiusin/pinecms/cmd/crud" 7 | "github.com/xiusin/pinecms/cmd/dede" 8 | "github.com/xiusin/pinecms/cmd/plugin" 9 | servCmd "github.com/xiusin/pinecms/cmd/server" 10 | "github.com/xiusin/pinecms/cmd/version" 11 | "github.com/xiusin/pinecms/src/config" 12 | "github.com/xiusin/pinecms/src/server" 13 | ) 14 | 15 | var rootCmd = &cobra.Command{ 16 | Use: "pinecms", 17 | CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true, DisableNoDescFlag: true}, 18 | Long: ` _ 19 | (_) 20 | ____ _ ____ ____ ____ ____ ___ 21 | | _ \| | _ \ / _ ) ___) \ /___) 22 | | | | | | | | ( (/ ( (___| | | |___ | 23 | | ||_/|_|_| |_|\____)____)_|_|_(___/ 24 | |_| version: ` + version.Version, 25 | 26 | Run: func(cmd *cobra.Command, args []string) { 27 | config.InitDB() 28 | server.Server() 29 | }, 30 | } 31 | 32 | func Execute() { 33 | _ = rootCmd.Execute() 34 | } 35 | 36 | func init() { 37 | rootCmd.AddCommand(initCmd) 38 | rootCmd.AddCommand(plugin.Cmd) 39 | rootCmd.AddCommand(version.Cmd) 40 | rootCmd.AddCommand(servCmd.ServeCmd) 41 | rootCmd.AddCommand(crud.Cmd) 42 | rootCmd.AddCommand(menuCmd) 43 | rootCmd.AddCommand(dede.Cmd) 44 | rootCmd.AddCommand(annotationsCmd) 45 | 46 | server.InitApp() 47 | } 48 | -------------------------------------------------------------------------------- /src/application/controllers/backend/request_params.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | // loginUserParam 登录参数 4 | type loginUserParam struct { 5 | Username string `json:"username" api:"remark:登录账号|require:true"` 6 | Password string `json:"password" api:"remark:登录密码|require:true"` 7 | CaptchaId string `json:"captchaId" api:"remark:验证码ID|require:true"` 8 | CaptchaValue string `json:"verifyCode" api:"remark:验证码|require:true"` 9 | } 10 | 11 | type idParams struct { 12 | Id int64 `json:"id" api:"remark:删除单个记录"` 13 | Ids []int64 `json:"ids" api:"remark:删除多个记录"` 14 | } 15 | 16 | type listParam struct { 17 | Page int `json:"page" api:"remark:分页数|default:1|require:true"` // 分页数 18 | Size int `json:"size" api:"remark:分页条数|default:10|require:true"` // 页码 19 | OrderField string `json:"order" api:"remark:排序字段"` // 排序字段 20 | Sort string `json:"sort" api:"remark:排序方法desc=逆序,asc=正序"` // 排序规则 21 | Keywords string `json:"keyWord" api:"remark:查询关键字"` // 搜索关键字 22 | Export bool `json:"_isExport" api:"remark:是否导出"` // 是否导出 23 | Params map[string]any `json:"params" api:"remark:额外参数用于非配置字段导出"` // 额外附加参数 24 | Param map[string]any `json:"param" api:"remark:cl-filter组件参数"` // 额外附加参数 25 | } 26 | -------------------------------------------------------------------------------- /src/application/models/tables/category.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | // Category 分类 4 | type Category struct { 5 | Catid int64 `xorm:"pk autoincr id" json:"id"` 6 | Parentid int64 `json:"parentId" xorm:"comment('所属栏目ID')"` 7 | Topid int64 `json:"topid" xorm:"comment('顶级栏目ID')"` 8 | ModelId int64 `json:"model_id" xorm:"comment('绑定模型ID')"` 9 | Catname string `json:"name" xorm:"comment('分类ID')"` 10 | Type int64 `json:"type"` 11 | Keywords string `json:"keywords"` 12 | Description string `json:"description"` 13 | Content string `xorm:"-"` 14 | Thumb string `json:"thumb"` 15 | Dir string `json:"dir"` 16 | Url string `json:"url"` 17 | Listorder int64 `json:"listorder"` 18 | Ismenu bool `json:"ismenu"` 19 | ListTpl string `json:"list_tpl"` 20 | DetailTpl string `json:"detail_tpl"` 21 | UrlPrefix string `xorm:"-" json:"url_prefix"` 22 | CreatedAt *LocalTime `json:"created_at" xorm:"created"` 23 | UpdatedAt *LocalTime `json:"updated_at" xorm:"updated"` 24 | Active bool `xorm:"-"` 25 | HasSon bool `xorm:"-"` 26 | Model *DocumentModel `xorm:"-" json:"model"` 27 | Page *Page `xorm:"-"` 28 | } 29 | -------------------------------------------------------------------------------- /cmd/crud/crud_format_stub.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | // public/assets/js/require-table.js:403 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | ) 8 | 9 | func FormatEnum(field string, opts []map[string]any, item map[string]any) { 10 | item["type"] = "tpl" 11 | vmap := map[string]any{} 12 | for _, opt := range opts { 13 | vmap[opt["value"].(string)] = opt["label"] 14 | } 15 | enumsInfo, _ := json.Marshal(vmap) 16 | if len(enumsInfo) == 0 { 17 | enumsInfo = []byte("[]") 18 | } 19 | topCode = append(topCode, `let _`+field+` =`+string(enumsInfo)+`;`) 20 | item["tpl"] = "<%=formatterEnum(data." + field + ", _" + field + ")%>" 21 | } 22 | 23 | func FormatSet(field string, opts []map[string]any, item map[string]any) { 24 | item["type"] = "tpl" 25 | vmap := map[string]any{} 26 | for _, opt := range opts { 27 | vmap[opt["value"].(string)] = opt["label"] 28 | } 29 | enumsInfo, _ := json.Marshal(vmap) 30 | if len(enumsInfo) == 0 { 31 | enumsInfo = []byte("[]") 32 | } 33 | topCode = append(topCode, `let _`+field+` =`+string(enumsInfo)+`;`) 34 | item["tpl"] = "<%=formatterSet(data." + field + ", _" + field + ")%>" 35 | } 36 | 37 | // JSONMarshal 不转义字符串编码 38 | func JSONMarshal(t any) ([]byte, error) { 39 | buffer := &bytes.Buffer{} 40 | encoder := json.NewEncoder(buffer) 41 | encoder.SetEscapeHTML(false) 42 | err := encoder.Encode(t) 43 | return buffer.Bytes(), err 44 | } 45 | -------------------------------------------------------------------------------- /src/application/models/member_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type MemberModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewMemberModel() *MemberModel { 15 | return &MemberModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (m *MemberModel) GetList(page, limit int64) (list []tables.Member, total int64) { 19 | offset := (page - 1) * limit 20 | total, _ = m.orm.Limit(int(limit), int(offset)).FindAndCount(&list) 21 | return list, total 22 | } 23 | 24 | func (m *MemberModel) GetInfo(id int64) *tables.Member { 25 | var member tables.Member 26 | m.orm.ID(id).Get(&member) 27 | return &member 28 | } 29 | 30 | func (m *MemberModel) Add(members *tables.Member) int64 { 31 | res, err := m.orm.InsertOne(members) 32 | if err != nil { 33 | pine.Logger().Error(err.Error()) 34 | return 0 35 | } 36 | return res 37 | } 38 | 39 | func (m *MemberModel) Exist(account string) bool { 40 | count, _ := m.orm.Where("account = ?", account).Count(&tables.Member{}) 41 | return count > 0 42 | } 43 | 44 | func (m *MemberModel) Edit(id int64, members *tables.Member) bool { 45 | res, err := m.orm.ID(id).MustCols("enabled", "integral").Update(members) 46 | if err != nil { 47 | return false 48 | } 49 | return res > 0 50 | } 51 | -------------------------------------------------------------------------------- /src/application/models/tables/plugin.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Plugin struct { 4 | Id int64 `json:"id"` 5 | Name string `json:"name" schema:"name" xorm:"varchar(100) comment('插件名称')"` 6 | Author string `json:"author" xorm:"varchar(100) comment('作者')"` 7 | Contact string `json:"contact" xorm:"varchar(100) comment('联系方式')"` 8 | Description string `json:"description" xorm:"text comment('功能描述')"` 9 | Version string `json:"version" xorm:"varchar(100) comment('版本号')"` 10 | Sign string `json:"sign" xorm:"comment('标志') unique"` 11 | Path string `json:"path" xorm:"comment('插件本地路径') unique"` 12 | Enable bool `json:"enable" xorm:"comment('是否启用 0:否 1:是')"` 13 | Status uint `json:"status" xorm:"comment('状态 0:缺少配置 1:可用 2: 配置错误 3:未知错误')"` 14 | View []map[string]any `json:"view" xorm:"json comment('页面配置信息')"` 15 | Page string `json:"page" xorm:"-"` 16 | Config map[string]any `json:"config" xorm:"json comment('插件绑定配置')"` 17 | Prefix string `json:"prefix" xorm:"comment('插件访问前缀')"` 18 | NoInstall bool `json:"no_install" xorm:"-"` 19 | ErrMsg string `json:"errmsg" xorm:"-"` 20 | CreatedAt LocalTime `json:"created_at"` 21 | UpdatedAt LocalTime `json:"updated_at"` 22 | } 23 | -------------------------------------------------------------------------------- /src/common/storage/storage_ftp_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestFtpUploader(t *testing.T) { 11 | uploader := NewFtpUploader(map[string]string{ 12 | "FTP_SERVER_URL": "124.222.103.232", 13 | "FTP_SERVER_PORT": "21", 14 | "FTP_USER_NAME": "test", 15 | "FTP_USER_PWD": "", 16 | "SITE_URL": "http://localhost:2019/xxx/", 17 | "FTP_URL_PREFIX": "", // 如果配置则使用此配置拼接地址, 否则使用系统接口 18 | }) 19 | 20 | t.Log(uploader.Mkdir("goftp")) 21 | t.Log(uploader.Mkdir("goftp2/subftpdir")) 22 | 23 | tmp, _ := ioutil.TempFile("", "") 24 | t.Log(io.WriteString(tmp, "hello world")) 25 | 26 | tmp.Close() 27 | tmp, _ = os.Open(tmp.Name()) 28 | defer func() { 29 | tmp.Close() 30 | os.Remove(tmp.Name()) 31 | }() 32 | 33 | t.Log("delete", uploader.Remove("goftp/index.html")) 34 | t.Log(uploader.Upload("goftp/index.html", tmp)) 35 | byts, err := uploader.Content("goftp/index.html") 36 | if err != nil { 37 | t.Log(err) 38 | } 39 | t.Log("byts", string(byts)) 40 | 41 | t.Log("exist: ") 42 | t.Log(uploader.Exists("goftp/index.html")) 43 | 44 | t.Log("rename", uploader.Rename("goftp/index.html", "goftp2/index_1.html")) 45 | 46 | t.Log("exist: ") 47 | t.Log(uploader.Exists("goftp/index.html")) 48 | 49 | t.Log("rmdir", uploader.Rmdir("goftp2")) 50 | 51 | t.Log(uploader.List("113123123/111111/22222")) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /resources/themes/default/page.jet: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{.Field.Title}}_{{global["site_name"]}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 29 | {{include "head2.jet"}} 30 | 31 | {{.Field.Content | unsafe}} 32 | 33 | 34 | 35 |
36 | 37 | 38 | {{include "footer.jet"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/pagelist.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/xiusin/pine" 6 | "github.com/xiusin/pinecms/src/application/models" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | "github.com/xiusin/pinecms/src/config" 9 | "math" 10 | "reflect" 11 | "strconv" 12 | 13 | "github.com/CloudyKit/jet" 14 | ) 15 | 16 | // PageList {{pagelist(row, .TypeID, .ArtCount, .PageNum, .QP) | unsafe}} 17 | func PageList(args jet.Arguments) reflect.Value { 18 | defer func() { 19 | if err := recover(); err != nil { 20 | pine.Logger().Error(fmt.Sprintf("pagelist Failed %s", err)) 21 | } 22 | }() 23 | limit := int(getNumber(args.Get(0))) 24 | tid := getNumber(args.Get(1)) 25 | total := int(getNumber(args.Get(2))) 26 | page := int(getNumber(args.Get(3))) 27 | qp, _ := args.Get(4).Interface().(map[string][]string) 28 | conf, _ := config.SiteConfig() 29 | if limit == 0 { 30 | limit, _ = strconv.Atoi(conf["SITE_PAGE_SIZE"]) 31 | if limit == 0 { 32 | limit = 15 33 | } 34 | } 35 | totalPage := int(math.Ceil(float64(total) / float64(limit))) 36 | if page > totalPage { 37 | page = totalPage 38 | } 39 | if len(qp) == 0 { 40 | return reflect.ValueOf(helper.NewPage("/"+models.NewCategoryModel().GetUrlPrefix(tid), page, limit, total, nil, false).String()) 41 | } else { 42 | // todo 地址后期path 43 | return reflect.ValueOf(helper.NewPage("/search.go", page, limit, total, qp, true).String()) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /cmd/example.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/xiusin/pine" 11 | ) 12 | 13 | var exampleCmd = &cobra.Command{ 14 | Use: "example", 15 | Short: "获取案例demo数据库以及资源", 16 | Run: func(_ *cobra.Command, _ []string) { 17 | var input string 18 | fmt.Print("下载demo会覆盖本地数据资源以及清理本地缓存,确定要执行吗?[Y/n]") 19 | _, _ = fmt.Scanln(&input) 20 | if strings.ToLower(input) == "n" || input == "" { 21 | return 22 | } 23 | _ = os.RemoveAll("pinecms_demo") 24 | gitCmd := exec.Command("git", "clone", "https://github.com/xiusin/pinecms_demo.git") 25 | gitCmd.Stdout = os.Stdout 26 | gitCmd.Stdin = os.Stdin 27 | if err := gitCmd.Run(); err != nil { 28 | pine.Logger().Error(err.Error()) 29 | return 30 | } 31 | _ = os.Rename("pinecms_demo/data.db.demo", "data.db.demo") 32 | _ = os.Rename("pinecms_demo/resources/themes/example", "resources/themes/example") 33 | _ = os.Rename("pinecms_demo/resources/assets/example", "resources/assets/example") 34 | _ = os.Rename("pinecms_demo/resources/uploads", "resources/uploads") 35 | _ = os.RemoveAll("pinecms_demo") 36 | fmt.Println(`1. 请修改配置文件database.yml的数据源为: data.db.demo`) 37 | fmt.Println(`2. 请配置application.yml主题为: example`) 38 | err := os.RemoveAll("runtime/cache.db") 39 | if err != nil { 40 | pine.Logger().Warn("删除缓存文件失败, 请手动删除缓存文件") 41 | return 42 | } 43 | }, 44 | } 45 | 46 | func init() { 47 | rootCmd.AddCommand(exampleCmd) 48 | } 49 | -------------------------------------------------------------------------------- /cmd/plugin/build_plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var buildPluginCmd = &cobra.Command{ 15 | Use: "build", 16 | Short: "构建插件", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | name, _ := cmd.Flags().GetString("name") 19 | if len(name) == 0 { 20 | _ = cmd.Usage() 21 | return 22 | } 23 | buildPluginDir := filepath.Join(outputPluginDir, name) 24 | _ = os.MkdirAll(buildPluginDir, os.ModePerm) 25 | 26 | scriptName := filepath.Join(sourcePluginDir, name, name+".go") 27 | 28 | _, err := os.Stat(scriptName) 29 | helper.PanicErr(err) 30 | 31 | if conf, err := os.ReadFile(filepath.Join(sourcePluginDir, name, configName)); err != nil { 32 | helper.PanicErr(err) 33 | } else { 34 | helper.PanicErr(os.WriteFile(filepath.Join(buildPluginDir, configName), conf, os.ModePerm)) 35 | } 36 | 37 | outPluginName := filepath.Join(buildPluginDir, name+".so") 38 | buildCmd := exec.Command("go", "build", "-buildmode=plugin", "-o", outPluginName, scriptName) 39 | buildCmd.Stdout = os.Stdout 40 | buildCmd.Stderr = os.Stdout 41 | buildCmd.Env = os.Environ() 42 | buildCmd.Dir = helper.AppPath() 43 | 44 | helper.PanicErr(buildCmd.Run()) 45 | 46 | fmt.Println("构建插件", outPluginName, "成功") 47 | }, 48 | } 49 | 50 | func init() { 51 | buildPluginCmd.Flags().String("name", "", "传入需要构建的插件文件名称") 52 | } 53 | -------------------------------------------------------------------------------- /resources/themes/default/error.jet: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{global["site_name"]}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 29 | {{include "head2.jet"}} 30 | 31 |
32 | 您访问的页面不存在或功能未开发 33 |
34 |
35 | 36 | 37 |
38 | 39 | 40 | {{include "footer.jet"}} 41 | 42 | 43 | -------------------------------------------------------------------------------- /resources/themes/default/right.jet: -------------------------------------------------------------------------------- 1 | {{ import "tags.jet" }} 2 | 3 |
  • 4 |
    5 | 6 | {{yield myad(name="right1")}} 7 |
    8 |
  • 9 |
    本类最热新闻
      10 | {{yield artlist(row=2, orderby="visit_count") content}} 11 |
    • {{field["title"]}}
    • 12 | {{end}} 13 | {{yield artlist(offset=2, row=10, orderby="visit_count") content}} 14 |
    • • {{field["title"]}}
    • 15 | {{end}} 16 |
    17 |
  • 18 |
    19 | 20 | {{yield myad(name="right2")}} 21 |
    22 |
  • 23 |
    -------------------------------------------------------------------------------- /src/application/models/ad_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type AdModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewAdModel() *AdModel { 15 | return &AdModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (l *AdModel) GetList(page, limit int) ([]tables.Advert, int64) { 19 | offset := (page - 1) * limit 20 | var list = []tables.Advert{} 21 | var total int64 22 | var err error 23 | if total, err = l.orm.Desc("listorder").Limit(limit, offset).FindAndCount(&list); err != nil { 24 | pine.Logger().Error(err.Error()) 25 | } 26 | return list, total 27 | } 28 | 29 | func (l *AdModel) Add(data *tables.Advert) int64 { 30 | id, err := l.orm.InsertOne(data) 31 | if err != nil { 32 | pine.Logger().Error(err.Error()) 33 | } 34 | return id 35 | } 36 | 37 | func (l *AdModel) Delete(id []int64) bool { 38 | res, err := l.orm.ID(id).Delete(&tables.Advert{}) 39 | if err != nil { 40 | pine.Logger().Error(err.Error()) 41 | } 42 | return res > 0 43 | } 44 | 45 | func (l *AdModel) Get(id int64) *tables.Advert { 46 | var link = &tables.Advert{} 47 | ok, _ := l.orm.ID(id).Get(link) 48 | if !ok { 49 | return nil 50 | } 51 | return link 52 | } 53 | 54 | func (l *AdModel) Update(data *tables.Advert) bool { 55 | id, err := l.orm.ID(data.Id).Update(data) 56 | if err != nil { 57 | pine.Logger().Error(err.Error()) 58 | } 59 | 60 | return id > 0 61 | } 62 | -------------------------------------------------------------------------------- /cmd/dede/import.go: -------------------------------------------------------------------------------- 1 | package dede 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var Cmd = &cobra.Command{ 8 | Use: "dede", 9 | Short: "导入织梦CMS数据", 10 | Long: `从织梦CMS源中匹配已知数据`, 11 | TraverseChildren: true, 12 | } 13 | var SqlFieldTypeMap = map[string]string{ 14 | "varchar": "varchar(100)", 15 | "int": "int(10)", 16 | } 17 | var SqlLite3FieldTypeMap = map[string]string{ 18 | "varchar": "TEXT", 19 | "int": "INTEGER", 20 | } 21 | var ExtraFields = []map[string]string{ 22 | { 23 | "COLUMN_NAME": "catid", 24 | "EXTRA": "", 25 | "COLUMN_TYPE": "int", 26 | "IS_NULLABLE": "NO", 27 | "COLUMN_COMMENT": "所属栏目ID", 28 | "COLUMN_DEFAULT": "0", 29 | }, 30 | { 31 | "COLUMN_NAME": "mid", 32 | "EXTRA": "", 33 | "COLUMN_TYPE": "int", 34 | "IS_NULLABLE": "NO", 35 | "COLUMN_COMMENT": "模型ID", 36 | "COLUMN_DEFAULT": "0", 37 | }, 38 | { 39 | "COLUMN_NAME": "created_time", 40 | "EXTRA": "", 41 | "COLUMN_TYPE": "datetime", 42 | "IS_NULLABLE": "YES", 43 | "COLUMN_COMMENT": "", 44 | "COLUMN_DEFAULT": "", 45 | }, 46 | { 47 | "COLUMN_NAME": "updated_time", 48 | "EXTRA": "", 49 | "COLUMN_TYPE": "datetime", 50 | "IS_NULLABLE": "YES", 51 | "COLUMN_COMMENT": "", 52 | "COLUMN_DEFAULT": "", 53 | }, 54 | { 55 | "COLUMN_NAME": "deleted_time", 56 | "EXTRA": "", 57 | "COLUMN_TYPE": "datetime", 58 | "IS_NULLABLE": "YES", 59 | "COLUMN_COMMENT": "", 60 | "COLUMN_DEFAULT": "", 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /src/application/models/tables/todo.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Todo struct { 4 | Id int64 `xorm:"int(11) autoincr not null pk 'id'" json:"id"` 5 | Type string `xorm:"varchar(30) not null default '1' 'type' comment('字符串多选:1=外部链接,2=内部链接,3=通用链接')" json:"type" validate:"required"` 6 | Name string `xorm:"varchar(50) not null 'name' comment('普通输入框')" json:"name" validate:"required"` 7 | Introduce string `xorm:"varchar(255) not null 'introduce' comment('普通多行输入框::cms-textarea')" json:"introduce" validate:"required"` 8 | Listorder int64 `xorm:"int(11) not null 'listorder' comment('不可为空数字')" json:"listorder" validate:"required"` 9 | Status int `xorm:"tinyint(1) not null default 0 'status' comment('tinyint单选:0=待审核,1=通过,2=拒绝:cms-radio')" json:"status" validate:"required"` 10 | PutDate LocalTime `xorm:"date default null 'put_date' comment('日期')" json:"put_date" validate:"required"` 11 | PutDatetime LocalTime `xorm:"datetime default null 'put_datetime' comment('时间日期')" json:"put_datetime" validate:"required"` 12 | StartTime LocalTime `xorm:"datetime default null 'start_time' comment('开始时间$end=end_time')" json:"start_time" validate:"required"` 13 | EndTime LocalTime `xorm:"datetime default null 'end_time' comment('结束时间被引用隐藏到代码区间选择器')" json:"end_time" validate:"required"` 14 | Logo string `xorm:"varchar(30) default null 'logo' comment('单图上传')" json:"logo" validate:"required"` 15 | Logos string `xorm:"varchar(255) default null 'logos' comment('多图上传')" json:"logos" validate:"required"` 16 | } 17 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/rule_controller.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/controllers/backend" 7 | "github.com/xiusin/pinecms/src/application/controllers/backend/wechat/dto" 8 | "github.com/xiusin/pinecms/src/application/models/tables" 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | "xorm.io/xorm" 11 | ) 12 | 13 | type WechatRuleController struct { 14 | backend.BaseController 15 | } 16 | 17 | func (c *WechatRuleController) Construct() { 18 | c.Table = &tables.WechatMsgReplyRule{} 19 | c.Entries = &[]tables.WechatMsgReplyRule{} 20 | c.SearchFields = []backend.SearchFieldDsl{ 21 | {Field: "appid"}, 22 | } 23 | c.BaseController.Construct() 24 | c.OpBefore = c.before 25 | } 26 | 27 | func (c WechatRuleController) before(act int, params any) error { 28 | if act == backend.OpEdit || act == backend.OpAdd { 29 | //sess := params.(*xorm.Session).Clone() 30 | sess := params.(*xorm.Session) 31 | data := c.Table.(*tables.WechatMsgReplyRule) 32 | sess.Where("Match_Value = ?", data.MatchValue).Where("appid = ?", data.AppId) 33 | if act == backend.OpEdit { 34 | sess.Where("id = ?", data.Id) 35 | } 36 | if exist, _ := sess.Exist(&tables.WechatMsgReplyRule{}); exist { 37 | return errors.New("规则匹配值已经存在") 38 | } 39 | } else if act == backend.OpList { 40 | var search dto.RuleSearch 41 | helper.PanicErr(c.BindParse(&search)) 42 | sess := params.(*xorm.Session) 43 | if search.Param.Appid != "" { 44 | sess.Where("appid = ?", search.Param.Appid) 45 | } 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/menu_controller.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "fmt" 5 | "github.com/silenceper/wechat/v2/officialaccount/menu" 6 | "github.com/xiusin/pinecms/src/application/controllers/backend" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | ) 9 | 10 | type WechatMenuController struct { 11 | backend.BaseController 12 | key string 13 | p menuParam 14 | } 15 | 16 | func (c *WechatMenuController) Construct() { 17 | c.BaseController.Construct() 18 | c.p = menuParam{} 19 | } 20 | 21 | func (c *WechatMenuController) PostEdit() { 22 | c.Ctx().BindJSON(&c.p) 23 | if len(c.p.Appid) == 0 || len(c.p.Menu.Button) == 0 { 24 | helper.Ajax("发布参数错误", 1, c.Ctx()) 25 | return 26 | } 27 | account, _ := GetOfficialAccount(c.p.Appid) 28 | if err := account.GetMenu().SetMenu(c.p.Menu.Button); err != nil { 29 | helper.Ajax("设置菜单失败: "+err.Error(), 1, c.Ctx()) 30 | } else { 31 | helper.Ajax("发布菜单成功", 0, c.Ctx()) 32 | } 33 | } 34 | 35 | func (c *WechatMenuController) GetInfo() { 36 | appid, _ := c.Ctx().Input().GetString("appid") 37 | if len(appid) == 0 { 38 | helper.Ajax("请选择公众号", 1, c.Ctx()) 39 | return 40 | } 41 | c.key = fmt.Sprintf(CacheKeyWechatMenu, appid) 42 | account, _ := GetOfficialAccount(appid) 43 | retMenu, err := account.GetMenu().GetMenu() 44 | if err != nil { 45 | helper.Ajax(err, 1, c.Ctx()) 46 | return 47 | } 48 | for i, btn := range retMenu.Menu.Button { 49 | if len(btn.SubButtons) == 0 { 50 | btn.SubButtons = []*menu.Button{} // 初始化子菜单结构 51 | } 52 | retMenu.Menu.Button[i] = btn 53 | } 54 | helper.Ajax(retMenu.Menu, 0, c.Ctx()) 55 | } 56 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/position.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/CloudyKit/jet" 6 | "github.com/xiusin/pine/contracts" 7 | "github.com/xiusin/pine/di" 8 | "github.com/xiusin/pinecms/src/application/controllers" 9 | "github.com/xiusin/pinecms/src/application/models" 10 | "github.com/xiusin/pinecms/src/application/models/tables" 11 | "reflect" 12 | "strings" 13 | ) 14 | 15 | /** 16 | * 标签:{{yield position() }} 17 | * 作用:获取当前页面面包屑只可适用于列表页和详情页 18 | */ 19 | func Position(args jet.Arguments) reflect.Value { 20 | if !checkArgType(&args) { 21 | return defaultArrReturnVal 22 | } 23 | typeid := getNumber(args.Get(0)) 24 | return reflect.ValueOf(getCategoryPos(typeid)) 25 | } 26 | 27 | func getCategoryPos(tid int64) string { 28 | var position []string 29 | var res string 30 | var data = struct { 31 | Arr []tables.Category 32 | Pos string 33 | }{} 34 | key := fmt.Sprintf(controllers.CacheCategoryPosPrefix, tid) 35 | icache := di.MustGet(controllers.ServiceICache).(contracts.Cache) 36 | err := icache.GetWithUnmarshal(key, &data) 37 | if err != nil { 38 | m := models.NewCategoryModel() 39 | data.Arr = m.GetPosArr(tid) 40 | for _, cat := range data.Arr { 41 | if cat.Type != 2 { 42 | position = append(position, ""+cat.Catname+"") 43 | } else { 44 | position = append(position, ""+cat.Catname+"") 45 | } 46 | } 47 | if len(data.Arr) > 0 { 48 | res = strings.Join(position, " > ") 49 | data.Pos = res 50 | _ = icache.SetWithMarshal(key, res) 51 | } 52 | } 53 | return data.Pos 54 | } 55 | -------------------------------------------------------------------------------- /src/application/models/link_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type LinkModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewLinkModel() *LinkModel { 15 | return &LinkModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (l *LinkModel) GetList(page, limit int64) ([]tables.Link, int64) { 19 | offset := (page - 1) * limit 20 | var list = []tables.Link{} 21 | var total int64 22 | var err error 23 | if total, err = l.orm.Desc("listorder").Limit(int(limit), int(offset)).FindAndCount(&list); err != nil { 24 | pine.Logger().Error(helper.GetCallerFuncName(), err) 25 | } 26 | return list, total 27 | } 28 | 29 | func (l *LinkModel) Add(data *tables.Link) int64 { 30 | id, err := l.orm.InsertOne(data) 31 | if err != nil { 32 | pine.Logger().Error(helper.GetCallerFuncName(), err) 33 | } 34 | return id 35 | } 36 | 37 | func (l *LinkModel) Delete(id int64) bool { 38 | res, err := l.orm.ID(id).Delete(&tables.Link{}) 39 | if err != nil { 40 | pine.Logger().Error(helper.GetCallerFuncName(), err) 41 | } 42 | return res > 0 43 | } 44 | 45 | func (l *LinkModel) Get(id int64) *tables.Link { 46 | var link = &tables.Link{} 47 | ok, _ := l.orm.ID(id).Get(link) 48 | if !ok { 49 | return nil 50 | } 51 | return link 52 | } 53 | 54 | func (l *LinkModel) Update(data *tables.Link) bool { 55 | id, err := l.orm.ID(data.Id).Update(data) 56 | if err != nil { 57 | pine.Logger().Error(helper.GetCallerFuncName(), err) 58 | } 59 | return id > 0 60 | } 61 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/common.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "errors" 5 | "github.com/xiusin/pine/contracts" 6 | 7 | "github.com/silenceper/wechat/v2" 8 | "github.com/silenceper/wechat/v2/officialaccount" 9 | offConfig "github.com/silenceper/wechat/v2/officialaccount/config" 10 | "github.com/xiusin/pine/di" 11 | "github.com/xiusin/pinecms/src/application/controllers" 12 | "github.com/xiusin/pinecms/src/application/models/tables" 13 | "github.com/xiusin/pinecms/src/common/helper" 14 | ) 15 | 16 | func GetOfficialAccount(appid string) (*officialaccount.OfficialAccount, *tables.WechatAccount) { 17 | accountData := &tables.WechatAccount{} 18 | orm := helper.GetORM() 19 | orm.Where("app_id = ?", appid).Get(accountData) 20 | if accountData.Id == 0 { 21 | panic(errors.New("公众号" + appid + "不存在")) 22 | } 23 | wc, memory := wechat.NewWechat(), &WechatTokenCacher{Cache: di.MustGet(controllers.ServiceICache).(contracts.Cache)} 24 | cfg := &offConfig.Config{ 25 | AppID: accountData.AppId, 26 | AppSecret: accountData.Secret, 27 | Token: accountData.Token, 28 | EncodingAESKey: accountData.AesKey, 29 | Cache: memory, 30 | } 31 | account := wc.GetOfficialAccount(cfg) 32 | return account, accountData 33 | } 34 | 35 | func SaveCacheMaterialListKey(key string, cacher contracts.Cache) { 36 | var keys []string 37 | cacher.GetWithUnmarshal(CacheKeyWechatMaterialListKeys, &keys) 38 | for _, cacheKey := range keys { 39 | if cacheKey == key { 40 | return 41 | } 42 | } 43 | keys = append(keys, key) 44 | cacher.SetWithMarshal(CacheKeyWechatMaterialListKeys, &keys, CacheTimeSecs) 45 | } 46 | -------------------------------------------------------------------------------- /src/application/controllers/backend/helper.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pine/di" 10 | "github.com/xiusin/pinecms/src/application/controllers" 11 | "github.com/xiusin/pinecms/src/common/storage" 12 | ) 13 | 14 | func getStorageEngine(settingData map[string]string) storage.Uploader { 15 | engine := settingData["UPLOAD_ENGINE"] 16 | var uploadEngine storage.Uploader 17 | uploader, err := di.Get(fmt.Sprintf(controllers.ServiceUploaderEngine, engine)) 18 | if err != nil { 19 | pine.Logger().Warn("缺少存储驱动, 自动转换为本地存储", err) 20 | uploadEngine = storage.NewFileUploader(settingData) 21 | } else { 22 | uploadEngine = uploader.(storage.Uploader) 23 | } 24 | return uploadEngine 25 | } 26 | 27 | func parseParam(ctx *pine.Context, param any) error { 28 | if ctx.Input().IsJson() && len(ctx.RequestCtx.PostBody()) > 0 { 29 | return ctx.BindJSON(param) 30 | } 31 | return nil 32 | } 33 | 34 | func ArrayCol(arr any, col string) []any { 35 | val := reflect.ValueOf(arr) 36 | if val.Kind() != reflect.Slice { 37 | panic(errors.New("ArrayCol第一个参数必须为切片类型")) 38 | } 39 | var cols []any 40 | for i := range val.Len() { 41 | cols = append(cols, val.Index(i).FieldByName(col).Interface()) 42 | } 43 | return cols 44 | } 45 | 46 | func ArrayColMap(arr any, col string) map[any]any { 47 | var maps = map[any]any{} 48 | val := reflect.ValueOf(arr) 49 | if val.Kind() != reflect.Slice { 50 | panic(errors.New("ArrayCol第一个参数必须为切片类型")) 51 | } 52 | for i := range val.Len() { 53 | maps[val.Index(i).FieldByName(col).Interface()] = val.Index(i).Interface() 54 | } 55 | return maps 56 | } 57 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/adlist.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/CloudyKit/jet" 9 | "github.com/xiusin/pinecms/src/application/models/tables" 10 | "github.com/xiusin/pinecms/src/common/helper" 11 | ) 12 | 13 | /** 14 | 广告标签 15 | {{yield adlist(id=3, pos="首页banner图")}} 16 | 返回一组广告 17 | */ 18 | func AdList(args jet.Arguments) reflect.Value { 19 | if !checkArgType(&args) { 20 | return defaultArrReturnVal 21 | } 22 | id := args.Get(0) 23 | var ids []string 24 | if isNumber(id) { 25 | ids = append(ids, strconv.Itoa(int(getNumber(id)))) 26 | } else if id.String() != "" { 27 | ids = strings.Split(id.String(), ",") 28 | } 29 | order := args.Get(2).String() 30 | if order == "" { 31 | order = "listorder desc" 32 | } 33 | orm := helper.GetORM().OrderBy(order) 34 | // 获取广告位信息 35 | var pos = args.Get(1) 36 | switch pos.Type().String() { 37 | case "string": 38 | if len(pos.String()) > 0 { 39 | advPos := &tables.AdvertSpace{} 40 | exists, _ := helper.GetORM().Table(advPos).Where("name = ?", pos.String()).Get(advPos) 41 | if !exists { 42 | return reflect.ValueOf([]tables.Advert{}) 43 | } 44 | orm.Where("space_id = ?", advPos.Id) 45 | } 46 | default: 47 | if pid := getNumber(pos); pid > 0 { 48 | orm.Where("space_id = ?", pid) 49 | } 50 | } 51 | if len(ids) > 0 { 52 | orm.In("id", ids) 53 | } 54 | 55 | now := helper.NowDate(helper.TimeFormat) 56 | orm.Where("status = 1").Where("start_time <= ?", now).Where("end_time >= ?", now).Select("id, name, image, link_url") 57 | var advs = []tables.Advert{} 58 | orm.Find(&advs) 59 | return reflect.ValueOf(advs) 60 | } 61 | -------------------------------------------------------------------------------- /src/application/controllers/backend/department_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | ) 9 | 10 | type DepartmentController struct { 11 | BaseController 12 | } 13 | 14 | func (c *DepartmentController) Construct() { 15 | c.Table = &tables.Department{} 16 | c.Entries = &[]tables.Department{} 17 | c.Group = "系统管理" 18 | c.SubGroup = "部门管理" 19 | c.ApiEntityName = "部门" 20 | c.BaseController.Construct() 21 | c.OpBefore = c.before 22 | } 23 | 24 | func (c *DepartmentController) before(act int, params any) error { 25 | if act == OpEdit || act == OpAdd { 26 | p := params.(*tables.Department) 27 | sess := c.Orm.NewSession() 28 | if act == OpEdit { 29 | if p.Id < 1 { 30 | return errors.New("ID不能为空") 31 | } 32 | if uint(p.Id) == p.ParentId { 33 | return errors.New("父级部门不能为自己") 34 | } else { 35 | ids := []any{p.Id} 36 | for len(ids) > 0 { 37 | var subs []tables.Department 38 | _ = c.Orm.In("parent_id", ids).Cols("id").Find(&subs) 39 | ids = ArrayCol(subs, "Id") 40 | for _, pid := range ids { 41 | if uint(pid.(int64)) == p.ParentId { 42 | return errors.New("不可设置自己下级作为父级部门") 43 | } 44 | } 45 | } 46 | } 47 | sess.Where("id <> ? and name = ?", p.Id, p.Name) 48 | } else { 49 | sess.Where("name = ?", p.Name) 50 | } 51 | if exist, _ := sess.Exist(&tables.Department{}); exist { 52 | return errors.New("部门已存在") 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (c *DepartmentController) GetSelect() { 60 | c.Orm.Find(c.Entries) 61 | helper.Ajax(c.Entries, 0, c.Ctx()) 62 | } 63 | -------------------------------------------------------------------------------- /src/application/controllers/tplfun/func.go: -------------------------------------------------------------------------------- 1 | package tplfun 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "time" 7 | "unicode/utf8" 8 | 9 | "github.com/CloudyKit/jet" 10 | "github.com/xiusin/pine" 11 | ) 12 | 13 | func format(str string) string { 14 | str = strings.Replace(str, "Y", "2006", 1) 15 | str = strings.Replace(str, "m", "01", 1) 16 | str = strings.Replace(str, "d", "02", 1) 17 | str = strings.Replace(str, "H", "13", 1) 18 | str = strings.Replace(str, "i", "04", 1) 19 | str = strings.Replace(str, "s", "05", 1) 20 | return str 21 | } 22 | 23 | func FormatTime(args jet.Arguments) reflect.Value { 24 | arg := strings.ReplaceAll("T", " ", args.Get(0).String()) 25 | arg = strings.ReplaceAll("Z", "", arg) 26 | t, err := time.Parse(time.DateTime, arg) 27 | if err != nil { 28 | return reflect.ValueOf("") 29 | } 30 | format := time.DateTime 31 | if args.NumOfArguments() > 1 { 32 | format = args.Get(1).String() 33 | } 34 | return reflect.ValueOf(t.Format(format)) 35 | } 36 | 37 | func MyDate(args jet.Arguments) reflect.Value { 38 | t, err := time.Parse("2006-01-02T15:04:05Z", args.Get(1).String()) 39 | if err != nil { 40 | pine.Logger().Error("解析时间错误", err) 41 | return reflect.ValueOf("") 42 | } 43 | format := format(args.Get(0).String()) 44 | return reflect.ValueOf(t.Format(format)) 45 | } 46 | 47 | func GetDateTimeMK(args jet.Arguments) reflect.Value { 48 | return FormatTime(args) 49 | } 50 | 51 | func CnSubstr(args jet.Arguments) reflect.Value { 52 | me := args.Get(0).String() 53 | length := int(args.Get(1).Float()) 54 | 55 | if utf8.RuneCountInString(me) > length { 56 | titleRune := []rune(me) 57 | me = string(titleRune[:length]) 58 | } 59 | return reflect.ValueOf(me) 60 | } 61 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/verify_token.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gbrlsnchs/jwt/v3" 6 | "github.com/xiusin/pine" 7 | "github.com/xiusin/pine/di" 8 | "github.com/xiusin/pinecms/src/application/controllers" 9 | "github.com/xiusin/pinecms/src/application/models/tables" 10 | "github.com/xiusin/pinecms/src/config" 11 | "strings" 12 | "xorm.io/xorm" 13 | ) 14 | 15 | // VerifyJwtToken 验证jwt 16 | // excludePrefix 需要排除认证的路由前缀 17 | func VerifyJwtToken() pine.Handler { 18 | return func(ctx *pine.Context) { 19 | uri := ctx.Path() 20 | if !strings.Contains(uri, "login") && 21 | !strings.Contains(uri, "captcha") && 22 | !strings.Contains(uri, "thumb") { 23 | token := ctx.Header("Authorization") 24 | if token == "" { 25 | token, _ = ctx.Input().GetString("token") 26 | } 27 | var hs = jwt.NewHS256([]byte(config.App().JwtKey)) 28 | // 验证token 29 | var pl controllers.LoginAdminPayload 30 | _, err := jwt.Verify([]byte(token), hs, &pl) 31 | if err != nil { 32 | _ = ctx.Render().JSON(pine.H{"code": 1, "msg": "授权失败, 请重新登录"}) 33 | return 34 | } 35 | 36 | ctx.SetUserValue("adminid", pl.AdminId) 37 | ctx.SetUserValue("roleid", pl.RoleID) 38 | 39 | if strings.Contains(uri, "user/info") { 40 | ctx.QueryArgs().Set("id", fmt.Sprintf("%d", pl.AdminId)) 41 | } 42 | if !strings.Contains(uri, "/log/list") { 43 | di.MustGet(&xorm.Engine{}).(*xorm.Engine).Insert(&tables.RequestLog{ 44 | Uri: string(ctx.RequestURI()), 45 | Userid: pl.AdminId, 46 | Params: string(ctx.PostBody()), 47 | Username: pl.AdminName, 48 | Ip: ctx.ClientIP(), 49 | Method: string(ctx.Method()), 50 | }) 51 | } 52 | } 53 | ctx.Next() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/common/search/zincsearch.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/xiusin/pinecms/src/config" 8 | client "github.com/zinclabs/sdk-go-zincsearch" 9 | ) 10 | 11 | type PineZincSearch struct { 12 | client *client.APIClient 13 | ctx context.Context 14 | } 15 | 16 | func (p *PineZincSearch) Search(index string, _query any) (any, error) { 17 | var query client.MetaZincQuery 18 | var ok bool 19 | if query, ok = _query.(client.MetaZincQuery); !ok { 20 | return nil, errors.New("invalid query type: expect client.MetaQuery") 21 | } 22 | 23 | resp, _, err := p.client.Search.Search(p.ctx, index).Query(query).Execute() 24 | return resp, err 25 | } 26 | 27 | func (p *PineZincSearch) Update(index, id string, doc map[string]any) error { 28 | _, _, err := p.client.Document.Update(p.ctx, index, id).Document(doc).Execute() 29 | return err 30 | } 31 | 32 | func (p *PineZincSearch) Delete(index, id string) error { 33 | _, _, err := p.client.Document.Delete(p.ctx, index, id).Execute() 34 | return err 35 | } 36 | 37 | func (p *PineZincSearch) Index(index string, doc map[string]any) (string, error) { 38 | resp, _, err := p.client.Document.Index(p.ctx, index).Document(doc).Execute() 39 | if err != nil { 40 | return "", err 41 | } 42 | return resp.GetId(), nil 43 | } 44 | 45 | func NewZincSearch() ISearch { 46 | cfg := config.DB() 47 | ctx := context.WithValue(context.Background(), client.ContextBasicAuth, client.BasicAuth{ 48 | UserName: cfg.Elastic.UserName, 49 | Password: cfg.Elastic.Password, 50 | }) 51 | configuration := client.NewConfiguration() 52 | configuration.Servers = client.ServerConfigurations{ 53 | client.ServerConfiguration{URL: cfg.Elastic.Url}, 54 | } 55 | return &PineZincSearch{client: client.NewAPIClient(configuration), ctx: ctx} 56 | } 57 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/account_controller.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "github.com/xiusin/pinecms/src/application/controllers" 5 | "github.com/xiusin/pinecms/src/application/controllers/backend" 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | ) 9 | 10 | type WechatAccountController struct { 11 | backend.BaseController 12 | } 13 | 14 | func (c *WechatAccountController) Construct() { 15 | c.Table = &tables.WechatAccount{} 16 | c.Entries = &[]tables.WechatAccount{} 17 | c.BaseController.Construct() 18 | } 19 | 20 | func (c *WechatAccountController) PostClear() { 21 | appid, _ := c.Input().GetString("appid") 22 | account, _ := GetOfficialAccount(appid) 23 | 24 | if err := account.GetBasic().ClearQuota(); err != nil { 25 | helper.Ajax(err, 1, c.Ctx()) 26 | } else { 27 | helper.Ajax("清除请求限制成功", 0, c.Ctx()) 28 | } 29 | } 30 | 31 | func (c *WechatAccountController) PostSelect() { 32 | _ = c.Orm.Find(c.Entries) 33 | m := c.Entries.(*[]tables.WechatAccount) 34 | var kv []tables.KV 35 | for _, model := range *m { 36 | kv = append(kv, tables.KV{ 37 | Label: model.Name, 38 | Value: model.AppId, 39 | }) 40 | } 41 | helper.Ajax(kv, 0, c.Ctx()) 42 | } 43 | 44 | // PostDistribution 会员分布 45 | func (c *WechatAccountController) PostDistribution() { 46 | appid, _ := c.Input().GetString("appid") 47 | if len(appid) == 0 { 48 | helper.Ajax("请选择公众号", 1, c.Ctx()) 49 | return 50 | } 51 | 52 | typ, _ := c.Input().GetInt("type") 53 | field := "province" 54 | if typ == 2 { 55 | field = "city" 56 | } 57 | 58 | data, _ := c.Orm.QueryString("SELECT " + field + ", COUNT(*) AS total FROM " + 59 | controllers.GetTableName("wechat_member") + " WHERE appid='" + appid + "' GROUP BY " + field) 60 | 61 | helper.Ajax(data, 0, c.Ctx()) 62 | } 63 | -------------------------------------------------------------------------------- /src/common/logger/pinecms_logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "time" 7 | 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pinecms/src/application/models/tables" 10 | "github.com/xiusin/pinecms/src/common/helper" 11 | "xorm.io/xorm" 12 | ) 13 | 14 | var firstLineChar = "pine.(*routerWrapper).result" 15 | var _sp = "\n" 16 | 17 | type pineCmsLoggerWriter struct { 18 | orm *xorm.Engine 19 | logCh chan []byte 20 | closed bool 21 | errorFlag []byte 22 | } 23 | 24 | func NewPineCmsLogger(orm *xorm.Engine, len uint) *pineCmsLoggerWriter { 25 | l := &pineCmsLoggerWriter{orm: orm, logCh: make(chan []byte, len), errorFlag: []byte("[ERRO]")} 26 | go l.BeginConsume() 27 | return l 28 | } 29 | 30 | func (p *pineCmsLoggerWriter) BeginConsume() { 31 | for { 32 | log, isCloser := <-p.logCh 33 | if !isCloser { 34 | return 35 | } 36 | if _, err := p.orm.InsertOne(p.parseLog(log)); err != nil { 37 | pine.Logger().Warn("日志入库失败", err) 38 | } 39 | } 40 | } 41 | 42 | func (p *pineCmsLoggerWriter) Write(data []byte) (int, error) { 43 | defer func() { 44 | if err := recover(); err != nil { 45 | p.closed = true 46 | } 47 | }() 48 | if !p.closed && bytes.Contains(data, p.errorFlag) { 49 | p.logCh <- data 50 | } 51 | return 0, nil 52 | } 53 | 54 | func (p *pineCmsLoggerWriter) parseLog(log []byte) *tables.Log { 55 | lines := strings.Split(*helper.Bytes2String(log), _sp) 56 | var index = -1 57 | for i, v := range lines { 58 | if strings.Contains(v, firstLineChar) { 59 | index = i 60 | break 61 | } 62 | } 63 | if index-3 > 3 { 64 | lines = append(lines[0:1], lines[3:index-3]...) 65 | } 66 | return &tables.Log{Level: 3, Message: strings.Join(lines, _sp), Time: tables.LocalTime(time.Now())} 67 | } 68 | 69 | func (p *pineCmsLoggerWriter) Close() { 70 | close(p.logCh) 71 | } 72 | -------------------------------------------------------------------------------- /src/common/message/email.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "io" 7 | "strconv" 8 | 9 | "github.com/kataras/go-mailer" 10 | "github.com/xiusin/pine/di" 11 | "github.com/xiusin/pinecms/src/config" 12 | "golang.org/x/text/encoding/simplifiedchinese" 13 | "golang.org/x/text/transform" 14 | ) 15 | 16 | type EmailMessage struct { 17 | client *mailer.Mailer 18 | } 19 | 20 | var ServiceEmailMessage = "pinecms.message.service.email" 21 | 22 | func (n *EmailMessage) Init() error { 23 | conf, err := config.SiteConfig() 24 | if err != nil { 25 | return err 26 | } 27 | port, err := strconv.Atoi(conf.Get("EMAIL_PORT")) 28 | if err != nil { 29 | port = 25 30 | } 31 | cfg := mailer.Config{ 32 | Host: conf.Get("EMAIL_SMTP"), 33 | Username: conf.Get("EMAIL_USER"), 34 | Password: conf.Get("EMAIL_PWD"), 35 | Port: port, 36 | FromAddr: conf.Get("EMAIL_EMAIL"), 37 | FromAlias: conf.Get("EMAIL_SEND_NAME"), 38 | } 39 | 40 | n.client = mailer.New(cfg) 41 | return nil 42 | } 43 | 44 | func (n *EmailMessage) Notice(receiver []string, params []any, templateId int) error { 45 | return nil 46 | } 47 | 48 | func (n *EmailMessage) Send(receiver []string, subject string, body string) error { 49 | subject = "=?UTF-8?B?" + base64.StdEncoding.EncodeToString([]byte(subject)) + "?=" 50 | 51 | data, _ := io.ReadAll(transform.NewReader(bytes.NewReader([]byte(body)), simplifiedchinese.GBK.NewEncoder())) 52 | return n.client.SendWithBytes(subject, []byte(data), receiver...) 53 | } 54 | 55 | func (n *EmailMessage) UpdateCfg() error { return n.Init() } 56 | 57 | func init() { 58 | di.Set(ServiceEmailMessage, func(builder di.AbstractBuilder) (any, error) { 59 | email := &EmailMessage{} 60 | if err := email.Init(); err != nil { 61 | return nil, err 62 | } 63 | return email, nil 64 | }, true) 65 | } 66 | -------------------------------------------------------------------------------- /src/application/models/adspace_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/models/tables" 6 | "github.com/xiusin/pinecms/src/common/helper" 7 | "xorm.io/xorm" 8 | ) 9 | 10 | type AdSpaceModel struct { 11 | orm *xorm.Engine 12 | } 13 | 14 | func NewAdSpaceModel() *AdSpaceModel { 15 | return &AdSpaceModel{orm: helper.GetORM()} 16 | } 17 | 18 | func (l *AdSpaceModel) All() []tables.AdvertSpace { 19 | var list = []tables.AdvertSpace{} 20 | if err := l.orm.Desc("id").Find(&list); err != nil { 21 | pine.Logger().Error(err.Error()) 22 | } 23 | return list 24 | } 25 | 26 | func (l *AdSpaceModel) GetList(page, limit int64) ([]tables.AdvertSpace, int64) { 27 | offset := (page - 1) * limit 28 | var list = []tables.AdvertSpace{} 29 | var total int64 30 | var err error 31 | if total, err = l.orm.Desc("id").Limit(int(limit), int(offset)).FindAndCount(&list); err != nil { 32 | pine.Logger().Error(err.Error()) 33 | } 34 | return list, total 35 | } 36 | 37 | func (l *AdSpaceModel) Add(data *tables.AdvertSpace) int64 { 38 | id, err := l.orm.Insert(data) 39 | if err != nil { 40 | pine.Logger().Error(err.Error()) 41 | } 42 | return id 43 | } 44 | 45 | func (l *AdSpaceModel) Delete(id int64) bool { 46 | res, err := l.orm.ID(id).Delete(&tables.AdvertSpace{}) 47 | if err != nil { 48 | pine.Logger().Error(err.Error()) 49 | } 50 | return res > 0 51 | } 52 | 53 | func (l *AdSpaceModel) Get(id int64) *tables.AdvertSpace { 54 | var link = &tables.AdvertSpace{} 55 | ok, _ := l.orm.ID(id).Get(link) 56 | if !ok { 57 | return nil 58 | } 59 | return link 60 | } 61 | 62 | func (l *AdSpaceModel) Update(data *tables.AdvertSpace) bool { 63 | id, err := l.orm.ID(data.Id).Update(data) 64 | if err != nil { 65 | pine.Logger().Error(err.Error()) 66 | } 67 | 68 | return id > 0 69 | } 70 | -------------------------------------------------------------------------------- /src/application/plugins/task/table/table.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type TaskInfo struct { 8 | Id int64 `json:"id" xorm:"autoincr"` 9 | EntityId int `json:"entity_id" xorm:"comment('任务ID')"` 10 | RepeatConf string `json:"repeatConf" xorm:"comment('任务配置') text"` 11 | Name string `json:"name" xorm:"comment('任务名称') varchar(50)"` 12 | Cron string `json:"cron" xorm:"comment('cron配置') varchar(50)"` 13 | Every uint `json:"every" xorm:"comment('间隔执行时间,task_type=1时生效')"` 14 | Limit uint `json:"limit" xorm:"comment('最大执行次数 不传为无限次')"` 15 | Remark string `json:"remark" xorm:"comment('备注') text"` 16 | Status uint `json:"status" xorm:"comment('状态 0:停止 1:运行')"` 17 | StartDate *time.Time `json:"startDate" xorm:"comment('开始时间')"` 18 | EndDate *time.Time `json:"endDate" xorm:"comment('结束时间')"` 19 | Data string `json:"data" xorm:"comment('数据') text"` 20 | Service string `json:"service" xorm:"comment('执行的service实例ID') varchar(100)"` 21 | Type uint8 `json:"type" xorm:"comment('类型 0:系统 1:用户')"` 22 | NextRunTime *time.Time `json:"nextRunTime" xorm:"comment('下一次执行时间')"` 23 | TaskType uint `json:"taskType" xorm:"comment('状态 0:cron 1:时间间隔')"` 24 | Error string `json:"error"` 25 | UpdatedAt time.Time `json:"updated_at"` 26 | } 27 | 28 | type TaskLog struct { 29 | Id int64 `json:"id" xorm:"autoincr"` 30 | TaskId int64 `json:"taskId" xorm:"comment('任务ID')"` 31 | Status bool `json:"status" xorm:"comment('状态 0:失败 1:成功')"` 32 | Detail string `json:"detail" xorm:"text comment('详情')"` 33 | ExecTime int64 `json:"exec_time" xorm:"int(11) default 0 comment('执行时长')"` 34 | CreatedAt time.Time `json:"created_at"` 35 | UpdatedAt time.Time `json:"updated_at"` 36 | } 37 | -------------------------------------------------------------------------------- /src/application/models/tables/member.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | type Member struct { 4 | Id int64 `xorm:"pk autoincr" json:"id"` 5 | Account string `json:"account" xorm:"comment('账号') varchar(40)"` 6 | Password string `json:"password" xorm:"comment('密码') varchar(32)"` 7 | Avatar string `json:"avatar" xorm:"comment('头像') varchar(40)"` 8 | Nickname string `json:"nickname" xorm:"comment('昵称') varchar(40)"` 9 | Integral uint `json:"integral" xorm:"comment('积分') int(11)"` 10 | Telphone string `json:"telphone" xorm:"comment('电话') varchar(30)"` 11 | Qq string `json:"qq" xorm:"comment('QQ') varchar(15)"` 12 | Description string `json:"description" xorm:"comment('个人简介')"` 13 | CreatedAt LocalTime `json:"created" xorm:"created"` 14 | UpdatedAt *LocalTime `json:"updated" xorm:"updated"` 15 | LoginTime LocalTime `json:"login_time" xorm:"datetime comment('最后登录时间')"` 16 | LoginIp string `json:"login_ip" xorm:"varchar(15) comment('最后登录IP')"` 17 | Email string `json:"email" xorm:"comment('邮箱') varchar(30)"` 18 | Status uint `json:"status" xorm:"comment('状态: 0=禁用 1=待验证 2=正常')"` 19 | Sex uint `json:"sex" xorm:"comment('性别: 0=保密 1=男 2=女') tinyint(3)"` 20 | GroupId uint `json:"group_id" xorm:"comment('分组ID') int(6)"` 21 | VerifyToken string `json:"-" xorm:"comment('验证token')"` 22 | } 23 | 24 | type MemberGroup struct { 25 | Id int64 `xorm:"pk autoincr" json:"id"` 26 | Name string `json:"name" xorm:"comment('名称') varchar(40)"` 27 | Description string `json:"description" xorm:"comment('介绍')"` 28 | Status uint `json:"status" xorm:"comment('状态: 0=禁用 1=正常')"` 29 | Listorder uint `json:"listorder"` 30 | CreatedAt LocalTime `json:"created" xorm:"created"` 31 | UpdatedAt *LocalTime `json:"updated" xorm:"updated"` 32 | } 33 | -------------------------------------------------------------------------------- /src/application/models/tables/define.go: -------------------------------------------------------------------------------- 1 | package tables 2 | 3 | import ( 4 | "database/sql/driver" 5 | "time" 6 | ) 7 | 8 | const localDateTimeFormat string = time.DateTime 9 | 10 | type LocalTime time.Time 11 | 12 | func (l LocalTime) MarshalJSON() ([]byte, error) { 13 | if time.Time(l).IsZero() { 14 | return []byte(`""`), nil 15 | } 16 | b := make([]byte, 0, len(localDateTimeFormat)+2) 17 | b = append(b, '"') 18 | b = time.Time(l).AppendFormat(b, localDateTimeFormat) 19 | b = append(b, '"') 20 | return b, nil 21 | } 22 | 23 | func (l *LocalTime) UnmarshalJSON(b []byte) error { 24 | now, err := time.ParseInLocation(`"`+localDateTimeFormat+`"`, string(b), time.Local) 25 | *l = LocalTime(now) 26 | return err 27 | } 28 | 29 | func (l LocalTime) String() string { 30 | return time.Time(l).Format(localDateTimeFormat) 31 | } 32 | 33 | func (l LocalTime) Now() LocalTime { 34 | return LocalTime(time.Now()) 35 | } 36 | 37 | func (l LocalTime) ParseTime(t time.Time) LocalTime { 38 | return LocalTime(t) 39 | } 40 | 41 | func (l LocalTime) format() string { 42 | return time.Time(l).Format(localDateTimeFormat) 43 | } 44 | 45 | func (l LocalTime) MarshalText() ([]byte, error) { 46 | return []byte(l.format()), nil 47 | } 48 | 49 | func (l *LocalTime) FromDB(b []byte) error { 50 | if nil == b || len(b) == 0 { 51 | return nil 52 | } 53 | var now time.Time 54 | var err error 55 | if now, err = time.ParseInLocation(localDateTimeFormat, string(b), time.Local); err != nil { 56 | return err 57 | } 58 | *l = LocalTime(now) 59 | return nil 60 | } 61 | 62 | func (l *LocalTime) ToDB() ([]byte, error) { 63 | if nil == l { 64 | return nil, nil 65 | } 66 | return []byte(time.Time(*l).Format(localDateTimeFormat)), nil 67 | } 68 | 69 | func (l *LocalTime) Value() (driver.Value, error) { 70 | if nil == l { 71 | return nil, nil 72 | } 73 | return time.Time(*l).Format(localDateTimeFormat), nil 74 | } 75 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/qrcode_ controller.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/silenceper/wechat/v2/officialaccount/basic" 7 | "github.com/xiusin/pine" 8 | "github.com/xiusin/pinecms/src/application/controllers/backend" 9 | "github.com/xiusin/pinecms/src/application/models/tables" 10 | "xorm.io/xorm" 11 | ) 12 | 13 | type WechatQrcodeController struct { 14 | backend.BaseController 15 | } 16 | 17 | func (c *WechatQrcodeController) Construct() { 18 | c.Table = &tables.WechatQrcode{} 19 | c.Entries = &[]tables.WechatQrcode{} 20 | c.BaseController.Construct() 21 | c.SearchFields = []backend.SearchFieldDsl{ 22 | {Field: "appid"}, 23 | } 24 | 25 | c.OpBefore = func(act int, intf any) error { 26 | if act == backend.OpList { 27 | sess := intf.(*xorm.Session) 28 | 29 | if appid, ok := c.P.Param["appid"]; ok && len(appid.(string)) > 0 { 30 | sess.Where("appid = ?", appid) 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | c.OpAfter = c.after 37 | } 38 | 39 | func (c *WechatQrcodeController) after(act int, params any) error { 40 | if act == backend.OpAdd { 41 | account, _ := GetOfficialAccount(params.(*tables.WechatQrcode).AppId) 42 | 43 | data := c.Table.(*tables.WechatQrcode) 44 | var req *basic.Request 45 | if data.IsTemp { 46 | var t time.Duration 47 | if time.Time(data.ExpireTime).Before(time.Now()) { 48 | t = 0 49 | } else { 50 | t = time.Until(time.Time(data.ExpireTime)) 51 | } 52 | req = basic.NewTmpQrRequest(t, data.SceneStr) 53 | } else { 54 | req = basic.NewLimitQrRequest(data.SceneStr) 55 | } 56 | ticket, err := account.GetBasic().GetQRTicket(req) 57 | if err != nil { 58 | pine.Logger().Error("获取传参二维码失败", err) 59 | return err 60 | } 61 | data.Ticket = ticket.Ticket 62 | 63 | data.Url = ticket.URL 64 | 65 | c.Orm.Where("id = ?", data.Id).Update(data) 66 | 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /src/application/controllers/backend/attachment_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "xorm.io/xorm" 5 | 6 | "github.com/xiusin/pinecms/src/application/controllers/middleware/apidoc" 7 | "github.com/xiusin/pinecms/src/application/models/tables" 8 | "github.com/xiusin/pinecms/src/common/helper" 9 | ) 10 | 11 | // @Rest(path = "/api/v1/11/{container_id}") 12 | type AttachmentController struct { 13 | BaseController 14 | } 15 | 16 | func (c *AttachmentController) Construct() { 17 | c.SearchFields = []SearchFieldDsl{ 18 | //{Field: "`type`", Op: "="}, 19 | } 20 | c.Table = &tables.Attachments{} 21 | c.Entries = &[]*tables.Attachments{} 22 | c.Group = "系统配置" 23 | c.SubGroup = "附件管理" 24 | 25 | c.apiEntities = map[string]apidoc.Entity{ 26 | "list": {Title: "附件列表", Desc: "查询已上传系统的附件列表"}, 27 | "add": {Title: "新增配置", Desc: "新增上传附件"}, 28 | "del": {Title: "删除配置", Desc: "删除一个附件"}, 29 | } 30 | c.OpBefore = c.before 31 | c.BaseController.Construct() 32 | } 33 | 34 | func (c *AttachmentController) before(act int, params any) error { 35 | if act == OpList { 36 | cid, _ := c.Input().GetInt64("classifyId") 37 | if cid > 0 { 38 | params.(*xorm.Session).Where("classify_id = ?", cid) 39 | } 40 | params.(*xorm.Session).Desc("id") 41 | } 42 | return nil 43 | } 44 | 45 | // @Rest(method = "GET", route = "/gaa") 46 | func (c *AttachmentController) PostAdd() { 47 | if err := c.BindParse(); err != nil { 48 | helper.Ajax(err.Error(), 1, c.Ctx()) 49 | return 50 | } 51 | 52 | md5, _ := c.Input().GetString("md5") 53 | if len(md5) == 0 { 54 | helper.Ajax("缺少必要的MD5参数", 1, c.Ctx()) 55 | return 56 | } 57 | data := &tables.Attachments{} 58 | 59 | if exist, _ := c.Orm.Where("md5 = ?", md5).Get(data); exist { 60 | helper.Ajax(data, 0, c.Ctx()) 61 | return 62 | } else if err := c.add(); err == nil { 63 | helper.Ajax(c.Table, 0, c.Ctx()) 64 | } else { 65 | helper.Ajax(err, 1, c.Ctx()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/application/controllers/backend/dict_category_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | ) 9 | 10 | type DictCategoryController struct { 11 | BaseController 12 | } 13 | 14 | func (c *DictCategoryController) Construct() { 15 | c.AppId = "admin" 16 | c.Group = "字典管理" 17 | c.KeywordsSearch = []SearchFieldDsl{ 18 | {Field: "key", Op: "LIKE", DataExp: "%$?%"}, 19 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 20 | } 21 | c.SearchFields = []SearchFieldDsl{ 22 | {Field: "status"}, 23 | } 24 | c.Table = &tables.DictCategory{} 25 | c.Entries = &[]*tables.DictCategory{} 26 | c.ApiEntityName = "字典分类" 27 | c.BaseController.Construct() 28 | c.OpBefore = c.before 29 | } 30 | 31 | func (c *DictCategoryController) before(act int, param any) error { 32 | switch act { 33 | case OpAdd, OpEdit: 34 | key := param.(*tables.DictCategory).Key 35 | id := param.(*tables.DictCategory).Id 36 | if len(key) == 0 { 37 | return errors.New("字典分类标识不存在") 38 | } 39 | sess := c.Orm.Table(c.Table).Where("`key` = ?", key) 40 | if OpEdit == act { 41 | sess = sess.Where("id <> ?", id) 42 | } 43 | exist, err := sess.Exist() 44 | if err != nil { 45 | return err 46 | } 47 | if exist { 48 | return errors.New("该字典分类标识已经存在") 49 | } 50 | case OpDel: 51 | exist, err := c.Orm.Table(&tables.Dict{}).In("cid", param.(*idParams).Ids).Exist() 52 | if err != nil { 53 | return err 54 | } 55 | if exist { 56 | return errors.New("分类下存在字典数据,无法删除") 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | func (c *DictCategoryController) GetSelect() { 63 | _ = c.Orm.Where("status = 1").Find(c.Entries) 64 | m := c.Entries.(*[]*tables.DictCategory) 65 | var kv []tables.KV 66 | for _, model := range *m { 67 | kv = append(kv, tables.KV{Label: model.Name, Value: model.Key}) 68 | } 69 | helper.Ajax(kv, 0, c.Ctx()) 70 | } 71 | -------------------------------------------------------------------------------- /src/application/controllers/backend/database_backup_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/xiusin/pine" 7 | "github.com/xiusin/pinecms/src/application/controllers" 8 | "github.com/xiusin/pinecms/src/common/helper" 9 | ) 10 | 11 | type DatabaseBackupController struct { 12 | BaseController 13 | } 14 | 15 | // 定时备份任务功能 16 | 17 | func (c *DatabaseBackupController) RegisterRoute(b pine.IRouterWrapper) { 18 | b.ANY("/backup/list", "BackupList") 19 | b.POST("/backup/delete", "BackupDelete") 20 | b.POST("/backup/download", "BackupDownload") 21 | } 22 | 23 | func (c *DatabaseBackupController) BackupList() { 24 | settingData := c.Ctx().Value(controllers.CacheSetting).(map[string]string) 25 | uploader := getStorageEngine(settingData) 26 | list, err := uploader.List(baseBackupDir) 27 | if err != nil { 28 | c.Logger().Error(err.Error()) 29 | } 30 | helper.Ajax(list, 0, c.Ctx()) 31 | } 32 | 33 | func (c *DatabaseBackupController) BackupDownload() { 34 | settingData := c.Ctx().Value(controllers.CacheSetting).(map[string]string) 35 | name, _ := c.Input().GetString("name") 36 | if len(name) == 0 { 37 | helper.Ajax("参数错误", 1, c.Ctx()) 38 | return 39 | } 40 | relName := filepath.Join(baseBackupDir, name) 41 | uploader := getStorageEngine(settingData) 42 | exists, _ := uploader.Exists(relName) 43 | if !exists { 44 | helper.Ajax("文件不存在或已经被删除", 1, c.Ctx()) 45 | return 46 | } 47 | helper.Ajax(uploader.GetFullUrl(relName), 0, c.Ctx()) 48 | } 49 | 50 | func (c *DatabaseBackupController) BackupDelete() { 51 | settingData := c.Ctx().Value(controllers.CacheSetting).(map[string]string) 52 | names := c.Input().Get("ids") 53 | if len(names.([]any)) == 0 { 54 | helper.Ajax("参数错误", 1, c.Ctx()) 55 | return 56 | } 57 | relName := names.([]any)[0].(string) 58 | uploader := getStorageEngine(settingData) 59 | if err := uploader.Remove(relName); err != nil { 60 | helper.Ajax(err, 1, c.Ctx()) 61 | return 62 | } 63 | helper.Ajax("删除文件成功", 0, c.Ctx()) 64 | } 65 | -------------------------------------------------------------------------------- /src/common/message/sms.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | // import ( 4 | // 5 | // "sync" 6 | // 7 | // openapi "github.com/alibabacloud-go/darabonba-openapi/client" 8 | // dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v2/client" 9 | // "github.com/alibabacloud-go/tea/tea" 10 | // "xorm.io/xorm" 11 | // "github.com/xiusin/pine/di" 12 | // 13 | // ) 14 | const ServiceSmsMessage = "pinecms.message.service.sms" 15 | 16 | // 17 | //type SmsMessage struct { 18 | // client *dysmsapi20170525.Client 19 | // orm *xorm.Engine 20 | // sync.Mutex 21 | //} 22 | // 23 | //func (n *SmsMessage) UpdateCfg() error { 24 | // return n.Init() 25 | //} 26 | // 27 | //func (n *SmsMessage) Init() error { 28 | // n.Lock() 29 | // defer n.Unlock() 30 | // var err error 31 | // var client *dysmsapi20170525.Client 32 | // config := &openapi.Config{ 33 | // AccessKeyId: nil, 34 | // AccessKeySecret: nil, 35 | // } 36 | // config.Endpoint = tea.String("dysmsapi.aliyuncs.com") 37 | // if client, err = dysmsapi20170525.NewClient(config); err == nil { 38 | // n.client = client 39 | // } 40 | // _orm , err := di.Get(&xorm.Engine{}) 41 | // if err != nil { 42 | // return err 43 | // } 44 | // n.orm = _orm.(*xorm.Engine) 45 | // return err 46 | //} 47 | // 48 | //func (n *SmsMessage) Notice(receiver []string, params []any, templateId int) error { 49 | // return nil 50 | //} 51 | // 52 | //func (n *SmsMessage) Send(receiver []string, msg string, typo int) error { 53 | // 54 | // for _, v := range receiver { 55 | // sendSmsRequest := &dysmsapi20170525.SendSmsRequest{ 56 | // PhoneNumbers: tea.String(v), 57 | // } 58 | // if _, err := n.client.SendSms(sendSmsRequest); err != nil { 59 | // return err 60 | // } 61 | // } 62 | // 63 | // return nil 64 | //} 65 | // 66 | //func init() { 67 | // di.Set(ServiceSmsMessage, func(builder di.AbstractBuilder) (any, error) { 68 | // msgService := &SmsMessage{} 69 | // err := msgService.Init() 70 | // if err != nil { 71 | // return nil, err 72 | // } 73 | // return msgService, nil 74 | // }, true) 75 | //} 76 | -------------------------------------------------------------------------------- /src/application/models/admin_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | "xorm.io/xorm" 9 | ) 10 | 11 | type AdminModel struct { 12 | orm *xorm.Engine 13 | } 14 | 15 | func NewAdminModel() *AdminModel { 16 | return &AdminModel{orm: helper.GetORM()} 17 | } 18 | 19 | // Login 登录用户 20 | func (a *AdminModel) Login(username, password, ip string) (tables.Admin, error) { 21 | admin := tables.Admin{Username: username} 22 | exist, _ := a.orm.Get(&admin) 23 | if !exist { 24 | return admin, errors.New("管理员不存在") 25 | } 26 | //password = helper.Password(password, admin.Encrypt) 27 | //if password != admin.Password { 28 | // return admin, errors.New("密码错误,请再次尝试!") 29 | //} 30 | admin.Lastloginip = ip 31 | a.orm.ID(admin.Userid).Update(admin) 32 | return admin, nil 33 | } 34 | 35 | //获取用户信息 36 | func (a *AdminModel) GetUserInfo(userid int64) (tables.Admin, error) { 37 | admin := tables.Admin{Userid: userid} 38 | has, _ := a.orm.Get(&admin) 39 | if has { 40 | 41 | return admin, nil 42 | } 43 | return admin, errors.New("没有找到用户") 44 | } 45 | 46 | //编辑密码 47 | func (a *AdminModel) EditPassword(userid int64, password string) bool { 48 | admin := tables.Admin{Userid: userid} 49 | res, _ := a.orm.Get(&admin) 50 | encrypt := string(helper.Krand(8, 3)) 51 | if res { 52 | admin.Password = helper.Password(password, encrypt) 53 | admin.Encrypt = encrypt 54 | result, _ := a.orm.ID(admin.Userid).Update(&admin) 55 | if result == 0 { 56 | return false 57 | } else { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | 64 | //获取管理员列表 65 | func (a *AdminModel) GetList(where string, page, rows int, order string, sortType string) []tables.Admin { 66 | start := (page - 1) * rows 67 | admins := []tables.Admin{} 68 | if sortType == "asc" { 69 | a.orm.Where(where).Asc(order).Limit(rows, start).Find(&admins) 70 | } else { 71 | a.orm.Where(where).Desc(order).Limit(rows, start).Find(&admins) 72 | } 73 | return admins 74 | } 75 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/likearticle.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/CloudyKit/jet" 9 | "github.com/xiusin/pine" 10 | "github.com/xiusin/pinecms/src/application/controllers" 11 | "github.com/xiusin/pinecms/src/application/models" 12 | "github.com/xiusin/pinecms/src/common/helper" 13 | ) 14 | 15 | func LikeArticle(args jet.Arguments) reflect.Value { 16 | defer func() { 17 | if err := recover(); err != nil { 18 | pine.Logger().Error(fmt.Sprintf("likearticle Failed %s", err)) 19 | } 20 | }() 21 | if !checkArgType(&args) { 22 | return defaultArrReturnVal 23 | } 24 | var kws = []string{args.Get(1).String(), args.Get(2).String(), args.Get(3).String()} 25 | limit := int(getNumber(args.Get(0))) 26 | catid := getNumber(args.Get(4)) 27 | titlelen := int(getNumber(args.Get(6))) 28 | var keywords []any 29 | if limit < 1 { 30 | limit = 10 31 | } 32 | var cond []string 33 | m := models.NewCategoryModel() 34 | category, _ := m.GetCategoryFByIdForBE(catid) 35 | modelTable := controllers.GetTableName(category.Model.Table) 36 | sess := getOrmSess(category.Model.Table).Where(modelTable+".id <> ?", getNumber(args.Get(5))) 37 | for _, kw := range kws { 38 | splitKeywords := strings.Split(kw, ",") 39 | for _, keyword := range splitKeywords { 40 | if keyword == "" { 41 | continue 42 | } 43 | cond = append(cond, fmt.Sprintf("%s.keywords LIKE ? OR %s.title LIKE ? OR %s.tags LIKE ?", modelTable, modelTable, modelTable)) 44 | keywords = append(keywords, "%"+strings.Trim(keyword, "")+"%", "%"+strings.Trim(keyword, "")+"%", "%"+strings.Trim(keyword, "")+"%") 45 | } 46 | } 47 | categoryTable := getCategoryTable() 48 | sess.And(strings.Join(cond, " OR "), keywords...) 49 | sess.Join("LEFT", categoryTable, categoryTable+".id = "+modelTable+".catid") 50 | sess.Select(fmt.Sprintf("%s.*, %s.catname as typename", modelTable, categoryTable)) 51 | list, _ := sess.Limit(limit).Desc(modelTable + ".id").QueryString() 52 | helper.HandleArtListInfo(list, titlelen) 53 | return reflect.ValueOf(list) 54 | } 55 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/middleware/Auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/xiusin/pine" 5 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/Apiform" 6 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/common" 7 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/errcode" 8 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/tables" 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func Auth() pine.Handler { 15 | return func(c *pine.Context) { 16 | for _, s := range []string{"ui", "login", "sftp", "term"} { 17 | if strings.Contains(c.Path(), "/" + s) { 18 | c.Next() 19 | return 20 | } 21 | } 22 | 23 | var resp Apiform.Resp 24 | jwtToken := c.Header("Authorization") 25 | if jwtToken == "" || !strings.HasPrefix(jwtToken, "Bearer ") { 26 | resp.Code = errcode.S_auth_fmt_err 27 | resp.Msg = "Token不正确" 28 | c.Render().JSON(resp) 29 | return 30 | } 31 | jwtToken = jwtToken[7:] 32 | claims, err := common.ParseToken(jwtToken) 33 | if err != nil { 34 | resp.Code = errcode.S_auth_err 35 | resp.Msg = "Token错误,请重新登录" 36 | c.Render().JSON(resp) 37 | return 38 | } 39 | valid := claims.Valid() 40 | if valid != nil { 41 | resp.Code = errcode.S_auth_err 42 | resp.Msg = "用户登录超时,请重新登录" 43 | c.Render().JSON(resp) 44 | return 45 | } 46 | var userInfo tables.SSHUser 47 | 48 | helper.GetORM().Where("id = ?", claims.Userid).Get(&userInfo) 49 | if userInfo.Phone == 0 { 50 | resp.Code = errcode.S_auth_err 51 | resp.Msg = "用户不存在,请重新登录" 52 | c.Render().JSON(resp) 53 | return 54 | } 55 | c.Set("uid", claims.Userid) 56 | c.Set("token", "") 57 | newToken, err := common.ReleaseToken(claims.Userid) 58 | if err != nil { 59 | resp.Code = errcode.S_auth_err 60 | resp.Msg = err.Error() 61 | c.Render().JSON(resp) 62 | return 63 | } 64 | if time.Now().Add(24*time.Hour).Unix() > claims.ExpiresAt { 65 | c.Set("token", newToken) 66 | } 67 | c.Next() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/common/enum/enum.go: -------------------------------------------------------------------------------- 1 | // Package enum 枚举值类型 2 | package enum 3 | 4 | import ( 5 | "reflect" 6 | "strings" 7 | "unsafe" 8 | ) 9 | 10 | // TypeContract 枚举值类型接口 11 | type TypeContract[T any] interface { 12 | Value() T 13 | } 14 | 15 | // Type 枚举值类型 16 | type Type[T any] struct { 17 | value *T 18 | } 19 | 20 | func (t Type[T]) Value() T { 21 | return *t.value 22 | } 23 | 24 | // New 枚举值类型 25 | func New[T any](t *T) *T { 26 | if t == nil { 27 | t = new(T) 28 | } 29 | value := reflect.ValueOf(t).Elem() 30 | typ := reflect.TypeOf(t).Elem() 31 | 32 | for i := 0; i < value.NumField(); i++ { 33 | field := typ.Field(i) 34 | fv := value.Field(i) 35 | 36 | // 非枚举定类型忽略 37 | if !strings.Contains(field.Type.String(), "enum.Type") { 38 | continue 39 | } 40 | 41 | // 判断具体反射类型 42 | valueType, ok := field.Type.FieldByName("value") 43 | if !ok { 44 | continue 45 | } 46 | 47 | rf := reflect.Indirect(fv).FieldByName("value") 48 | ptr := unsafe.Pointer(rf.UnsafeAddr()) 49 | 50 | fieldValue := field.Tag.Get("enum") 51 | switch valueType.Type.Elem().Kind() { 52 | case reflect.Int: 53 | *(**int)(ptr) = toPtr(i + 1) 54 | case reflect.Int8: 55 | *(**int8)(ptr) = toPtr(int8(i + 1)) 56 | case reflect.Int16: 57 | *(**int16)(ptr) = toPtr(int16(i + 1)) 58 | case reflect.Int32: 59 | *(**int32)(ptr) = toPtr(int32(i + 1)) 60 | case reflect.Int64: 61 | *(**int64)(ptr) = toPtr(int64(i + 1)) 62 | case reflect.Uint: 63 | *(**uint)(ptr) = toPtr(uint(i + 1)) 64 | case reflect.Uint8: 65 | *(**uint8)(ptr) = toPtr(uint8(i + 1)) 66 | case reflect.Uint16: 67 | *(**uint16)(ptr) = toPtr(uint16(i + 1)) 68 | case reflect.Uint32: 69 | *(**uint32)(ptr) = toPtr(uint32(i + 1)) 70 | case reflect.Uint64: 71 | *(**uint64)(ptr) = toPtr(uint64(i + 1)) 72 | case reflect.String: 73 | if fieldValue == "" { 74 | fieldValue = field.Name 75 | } 76 | *(**string)(ptr) = toPtr(fieldValue) 77 | default: 78 | panic("enum value type not support: " + valueType.Type.String()) 79 | } 80 | } 81 | return t 82 | 83 | } 84 | 85 | func toPtr[T any](v T) *T { 86 | return &v 87 | } 88 | -------------------------------------------------------------------------------- /src/application/controllers/middleware/apidoc/parse.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/fatih/structs" 8 | ) 9 | 10 | func parseInterface(reqParams any) ([]apiParam, []apiReturn) { 11 | if reqParams != nil { 12 | s := structs.Fields(reqParams) 13 | var apiReqParams []apiParam 14 | var apiReturns []apiReturn 15 | for _, field := range s { 16 | apiData := field.Tag("api") 17 | if len(apiData) == 0 { 18 | continue 19 | } 20 | fieldType := "any" 21 | switch field.Kind() { 22 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 23 | fieldType = "number" 24 | case reflect.Bool: 25 | fieldType = "bool" 26 | case reflect.String: 27 | fieldType = "string" 28 | case reflect.Struct, reflect.Map: 29 | fieldType = "object" 30 | case reflect.Slice, reflect.Array: 31 | fieldType = "array" 32 | case reflect.Float32, reflect.Float64: 33 | fieldType = "float" 34 | } 35 | apiDatas := strings.Split(apiData, "|") 36 | name := field.Tag("json") 37 | if len(name) == 0 { 38 | name = field.Name() 39 | } 40 | apiReqParam := apiParam{Type: fieldType, Name: name} 41 | apiReturn := apiReturn{Type: fieldType, Name: name, Params: nil} 42 | for _, data := range apiDatas { 43 | kv := strings.Split(strings.ToLower(data), ":") 44 | v := "" 45 | if len(kv) > 1 { 46 | v = kv[1] 47 | } 48 | switch kv[0] { 49 | case "require": 50 | if strings.ToLower(v) == "true" { 51 | apiReqParam.Require = true 52 | } 53 | case "remark": 54 | apiReqParam.Desc = v 55 | apiReturn.Desc = v 56 | case "main": 57 | if strings.ToLower(v) == "true" { 58 | apiReturn.Main = true 59 | } 60 | case "default": 61 | apiReqParam.Default = v 62 | apiReturn.Default = v 63 | } 64 | } 65 | apiReqParams = append(apiReqParams, apiReqParam) 66 | apiReturns = append(apiReturns, apiReturn) 67 | } 68 | return apiReqParams, apiReturns 69 | } 70 | return nil, nil 71 | } 72 | -------------------------------------------------------------------------------- /src/application/controllers/frontend/page.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pine/render/engine/pjet" 10 | "github.com/xiusin/pinecms/src/application/controllers" 11 | "github.com/xiusin/pinecms/src/application/models" 12 | "github.com/xiusin/pinecms/src/application/models/tables" 13 | ) 14 | 15 | func (c *IndexController) Page(pathname string) { 16 | c.setTemplateData() 17 | pageFilePath := GetStaticFile(pathname) 18 | tid, _ := c.Ctx().Params().GetInt64("tid") 19 | if tid < 1 { 20 | c.Ctx().Abort(404, "tid failed") 21 | return 22 | } 23 | category, err := models.NewCategoryModel().GetCategoryFByIdForBE(tid) 24 | if err != nil { 25 | pine.Logger().Error(err.Error()) 26 | c.Ctx().Abort(404) 27 | return 28 | } 29 | page := category.Page 30 | if page == nil { 31 | page = &tables.Page{Title: category.Catname} 32 | } 33 | if page.Keywords == "" { 34 | page.Keywords = category.Keywords 35 | } 36 | if len(page.Description) > 0 { 37 | page.Description = category.Description 38 | } 39 | _ = os.MkdirAll(filepath.Dir(pageFilePath), os.ModePerm) 40 | f, err := os.OpenFile(pageFilePath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.ModePerm) 41 | if err != nil { 42 | pine.Logger().Error(err.Error()) 43 | c.Ctx().Abort(http.StatusNotFound) 44 | return 45 | } 46 | defer f.Close() 47 | pineJet := pine.Make(controllers.ServiceJetEngine).(*pjet.PineJet) 48 | tpl := "page.jet" 49 | if len(category.DetailTpl) > 0 { 50 | tpl = category.DetailTpl 51 | } 52 | temp, err := pineJet.GetTemplate(template(tpl)) 53 | if err != nil { 54 | _ = c.Ctx().WriteString(err.Error()) 55 | return 56 | } 57 | err = temp.Execute(f, viewDataToJetMap(c.Render().GetViewData()), struct { 58 | Field *tables.Page // 单页信息 59 | Position string 60 | TypeID int64 61 | ArtID int64 62 | TopCategory *tables.Category // 顶级栏目信息 63 | }{ 64 | Field: page, 65 | TypeID: tid, 66 | }) 67 | if err != nil { 68 | _ = c.Ctx().WriteString(err.Error()) 69 | return 70 | } 71 | data, _ := os.ReadFile(pageFilePath) 72 | _ = c.Ctx().WriteHTMLBytes(data) 73 | } 74 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/helper.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/CloudyKit/jet" 12 | "github.com/xiusin/pine" 13 | "github.com/xiusin/pinecms/src/application/controllers" 14 | "github.com/xiusin/pinecms/src/application/models/tables" 15 | "github.com/xiusin/pinecms/src/common/helper" 16 | "xorm.io/xorm" 17 | ) 18 | 19 | var defaultArrReturnVal = reflect.ValueOf([]any{}) 20 | 21 | var defaultSignalVal = reflect.ValueOf(nil) 22 | 23 | func checkArgType(args *jet.Arguments) bool { 24 | l := args.NumOfArguments() 25 | for i := 0; i < l; i++ { 26 | t := args.Get(i) 27 | if !isNumber(t) && t.Type().String() != "string" && t.Type().String() != "bool" { 28 | pine.Logger().Error(fmt.Sprintf("参数类型不支持: idx: %d -> type: %s -> val: %s", i, t, args.Get(i))) 29 | return false 30 | } 31 | } 32 | return true 33 | } 34 | 35 | func isNumber(val reflect.Value) bool { 36 | return strings.Contains(val.String(), "float") || strings.Contains(val.String(), "int") 37 | } 38 | 39 | func getNumber(val reflect.Value) int64 { 40 | t := val.Type().String() 41 | if strings.Contains(t, "float") { 42 | return int64(val.Float()) 43 | } else if strings.Contains(t, "int") { 44 | return val.Int() 45 | } else if t == "string" { 46 | v, _ := strconv.Atoi(val.String()) 47 | return int64(v) 48 | } 49 | return 0 50 | } 51 | 52 | func getOrmSess(table ...string) *xorm.Session { 53 | if len(table) == 0 { 54 | table = []string{"articles"} 55 | } 56 | return helper.GetORM().Table(controllers.GetTableName(table[0])) 57 | } 58 | 59 | func getCategoryOrm() *xorm.Session { 60 | return helper.GetORM().Table(&tables.Category{}).Where("ismenu = 1") 61 | } 62 | 63 | func getCategoryTable() string { 64 | return controllers.GetTableName("category") 65 | } 66 | 67 | func getTagHash(args jet.Arguments) string { 68 | var arr []any 69 | for i := 0; i < args.NumOfArguments(); i++ { 70 | arr = append(arr, args.Get(i).Interface()) 71 | } 72 | byts, _ := json.Marshal(&arr) 73 | md := md5.New() 74 | md.Write(byts) 75 | 76 | return fmt.Sprintf("%x", md.Sum(nil)) 77 | } 78 | -------------------------------------------------------------------------------- /src/application/models/admin_role_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "log" 5 | 6 | "xorm.io/xorm" 7 | 8 | "github.com/xiusin/pinecms/src/application/models/tables" 9 | "github.com/xiusin/pinecms/src/common/helper" 10 | ) 11 | 12 | type AdminRoleModel struct { 13 | orm *xorm.Engine 14 | } 15 | 16 | func NewAdminRoleModel() *AdminRoleModel { 17 | return &AdminRoleModel{orm: helper.GetORM()} 18 | } 19 | 20 | func (a *AdminRoleModel) List(page, rows int) ([]tables.AdminRole, int64) { 21 | start := (page - 1) * rows 22 | roles := []tables.AdminRole{} 23 | total, _ := a.orm.Limit(rows, start).FindAndCount(&roles) 24 | return roles, total 25 | } 26 | 27 | func (a *AdminRoleModel) All() map[int64]*tables.AdminRole { 28 | var roles = map[int64]*tables.AdminRole{} 29 | var _roles []*tables.AdminRole 30 | a.orm.Find(&_roles) 31 | for _, role := range _roles { 32 | roles[role.Id] = role 33 | } 34 | return roles 35 | } 36 | 37 | func (a *AdminRoleModel) CheckRoleName(id int64, rolename string) bool { 38 | exists, _ := a.orm.Where("roleid <> ?", id).Where("rolename = ?", rolename).Exist() 39 | return exists 40 | } 41 | 42 | func (a *AdminRoleModel) AddRole(rolename, description string, disabled, listorder int64) bool { 43 | newRole := tables.AdminRole{ 44 | Rolename: rolename, 45 | Description: description, 46 | Disabled: disabled, 47 | Listorder: listorder, 48 | } 49 | insertId, err := a.orm.Insert(&newRole) 50 | if err != nil || insertId == 0 { 51 | log.Println(err, insertId) 52 | return false 53 | } 54 | return true 55 | } 56 | 57 | func (a *AdminRoleModel) GetRoleById(id int64) (tables.AdminRole, error) { 58 | role := tables.AdminRole{Id: id} 59 | _, err := a.orm.Get(&role) 60 | return role, err 61 | } 62 | 63 | func (a *AdminRoleModel) UpdateRole(role tables.AdminRole) bool { 64 | res, err := a.orm.Where("id=?", role.Id).MustCols("disabled").Update(&role) 65 | if err != nil || res == 0 { 66 | return false 67 | } 68 | return true 69 | } 70 | 71 | func (a *AdminRoleModel) DeleteRole(role tables.AdminRole) bool { 72 | res, err := a.orm.Delete(&role) 73 | if err != nil || res == 0 { 74 | return false 75 | } 76 | return true 77 | } 78 | -------------------------------------------------------------------------------- /src/application/controllers/backend/login_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gbrlsnchs/jwt/v3" 7 | 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pinecms/src/application/controllers" 10 | "github.com/xiusin/pinecms/src/application/controllers/middleware/apidoc" 11 | "github.com/xiusin/pinecms/src/application/models" 12 | "github.com/xiusin/pinecms/src/common/helper" 13 | "github.com/xiusin/pinecms/src/config" 14 | ) 15 | 16 | type LoginController struct { 17 | pine.Controller 18 | } 19 | 20 | func (c *LoginController) RegisterRoute(b pine.IRouterWrapper) { 21 | b.ANY("/login", "Login") 22 | } 23 | 24 | func (c *LoginController) Login() { 25 | var p loginUserParam 26 | apidoc.SetApiEntity(c.Ctx(), &apidoc.Entity{ 27 | ApiParam: &p, 28 | AppId: "admin", 29 | Group: "登录模块", 30 | SubGroup: "系统登录", 31 | Title: "登录系统", 32 | Desc: "账号密码登录系统, 并且返回JWT凭证", 33 | }) 34 | 35 | helper.PanicErr(parseParam(c.Ctx(), &p)) 36 | 37 | if p.Password == "" || p.Username == "" { 38 | helper.Ajax("参数不能为空", 1, c.Ctx()) 39 | return 40 | } 41 | 42 | // if !captcha.Verify(p.CaptchaId, p.CaptchaValue) { 43 | // helper.Ajax("验证码错误, 请重新输入", 1, c.Ctx()) 44 | // return 45 | // } 46 | 47 | // 读取登录人信息 48 | admin, err := models.NewAdminModel().Login(p.Username, p.Password, c.Ctx().ClientIP()) 49 | if err != nil { 50 | helper.Ajax(err, 1, c.Ctx()) 51 | return 52 | } 53 | var hs = jwt.NewHS256([]byte(config.App().JwtKey)) 54 | now := time.Now() 55 | pl := controllers.LoginAdminPayload{ 56 | Payload: jwt.Payload{ 57 | Subject: "PineCMS", 58 | ExpirationTime: jwt.NumericDate(now.Add(24 * 30 * 12 * time.Hour)), 59 | }, 60 | Id: admin.Userid, 61 | AdminId: admin.Userid, 62 | RoleID: admin.RoleIdList, 63 | AdminName: admin.Username, 64 | } 65 | if token, err := jwt.Sign(pl, hs); err != nil { 66 | helper.Ajax("登录失败", 1, c.Ctx()) 67 | } else { 68 | helper.Ajax(pine.H{ 69 | "role_id": admin.RoleIdList, 70 | "admin_id": admin.Userid, 71 | "id": admin.Userid, 72 | "admin_name": admin.Username, 73 | "token": string(token), 74 | }, 0, c.Ctx()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/application/controllers/backend/wechat/define.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "github.com/silenceper/wechat/v2/officialaccount/menu" 5 | "github.com/silenceper/wechat/v2/officialaccount/message" 6 | ) 7 | 8 | const CacheKeyWechatMaterialCount = "pinecms.wechat.material.count.%s" 9 | 10 | const CacheKeyWechatMaterialListKeys = "pinecms.wechat.material.list.key" 11 | 12 | const CacheKeyWechatUserTags = "pinecms.wechat.user.tags.%s" 13 | 14 | const CacheKeyWechatMenu = "pinecms.wechat.menu.%s" 15 | 16 | const CacheTimeSecs = 30 * 24 * 3600 17 | 18 | // Plugin TODO 回复插件 19 | type Plugin struct { 20 | } 21 | 22 | // WechatMsg 微信自动回复混合消息结构 23 | type WechatMsg struct { 24 | Title string `json:"title"` 25 | // 小程序 26 | AppID string `json:"appid"` 27 | PagePath string `json:"pagePath"` 28 | ThumbMediaID string `json:"thumb_media_id"` 29 | 30 | // 音乐 视频 31 | Description string `json:"description"` 32 | MusicURL string `json:"music_url"` 33 | HQMusicURL string `json:"hq_music_url"` 34 | 35 | MediaID string `json:"media_id"` 36 | 37 | KfAccount string `json:"kf_account"` 38 | 39 | Articles []*message.Article 40 | } 41 | 42 | // MaterialUploadForm 素材上传表单结构 43 | type MaterialUploadForm struct { 44 | Appid string `json:"appid"` 45 | MediaID string `json:"mediaId"` 46 | FileName string `json:"fileName"` 47 | Title string `json:"title"` 48 | Introduction string `json:"introduction"` 49 | MediaType string `json:"mediaType"` 50 | } 51 | 52 | // menuParam 菜单编辑参数 53 | type menuParam struct { 54 | Appid string `json:"appid"` 55 | Menu struct { 56 | Button []*menu.Button `json:"button"` 57 | MenuID int64 `json:"menuid"` 58 | } `json:"menu"` 59 | } 60 | 61 | // replyMsg 回复消息结构体 62 | type replyMsg struct { 63 | AppId string `json:"appid"` 64 | OpenId string `json:"openid"` 65 | ReplyContent string `json:"replyContent"` 66 | ReplyType string `json:"replyType"` 67 | } 68 | 69 | // UserTags 用户标签结构 70 | type UserTags struct { 71 | Appid string `json:"appid"` 72 | Id int64 `json:"id"` 73 | Name string `json:"name"` 74 | Openids []string `json:"openids"` 75 | Action string `json:"action"` 76 | } 77 | -------------------------------------------------------------------------------- /src/application/models/menu_model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | "xorm.io/xorm" 9 | ) 10 | 11 | type MenuModel struct { 12 | orm *xorm.Engine 13 | } 14 | 15 | func NewMenuModel() *MenuModel { 16 | return &MenuModel{orm: helper.GetORM()} 17 | } 18 | 19 | func (m MenuModel) GetTree(menus []tables.Menu, parentid int64) []tables.Menu { 20 | res := []tables.Menu{} 21 | if len(menus) != 0 { 22 | for _, menu := range menus { 23 | if parentid == menu.Parentid { 24 | menu.Children = m.GetTree(menus, menu.Id) 25 | res = append(res, menu) 26 | } 27 | } 28 | } 29 | return res 30 | } 31 | 32 | func (m MenuModel) GetAll(menuIdList []int64) []tables.Menu { 33 | menus := []tables.Menu{} 34 | if len(menuIdList) > 0 { 35 | m.orm.Asc("listorder").In("id", menuIdList).Desc("id").Find(&menus) 36 | } 37 | return menus 38 | } 39 | 40 | // 获取菜单父级id 41 | func (m MenuModel) GetParentIds(id int64, result string) string { 42 | menu := tables.Menu{Id: id} 43 | has, _ := m.orm.Get(&menu) 44 | var parentid int64 = 0 45 | if has { 46 | parentid = menu.Parentid 47 | } 48 | res := "" 49 | if parentid != 0 { 50 | if result == "" { 51 | res = strconv.Itoa(int(parentid)) 52 | } else { 53 | res = "," + result 54 | } 55 | res = m.GetParentIds(parentid, res) 56 | } 57 | return res 58 | } 59 | 60 | // 检查菜单名称是否存在 61 | func (m MenuModel) CheckName(name string) bool { 62 | has, _ := m.orm.Get(&tables.Menu{Name: name}) 63 | return has 64 | } 65 | 66 | func (m MenuModel) GetInfo(id int64) (*tables.Menu, bool) { 67 | im := &tables.Menu{Id: id} 68 | has, _ := m.orm.Get(im) 69 | return im, has 70 | } 71 | 72 | // 获取selectTree 73 | func (m MenuModel) GetSelectTree(menus []tables.Menu, parentid int64) []map[string]any { 74 | res := []map[string]any{} 75 | if len(menus) != 0 { 76 | for _, v := range menus { 77 | if parentid == v.Parentid { 78 | menu := map[string]any{ 79 | "value": v.Id, 80 | "label": v.Name, 81 | "children": m.GetSelectTree(menus, v.Id), 82 | } 83 | res = append(res, menu) 84 | } 85 | } 86 | } 87 | return res 88 | } 89 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/channel.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "time" 7 | 8 | "github.com/CloudyKit/jet" 9 | "github.com/xiusin/pinecms/src/application/models" 10 | "github.com/xiusin/pinecms/src/application/models/tables" 11 | "github.com/xiusin/pinecms/src/common/helper" 12 | ) 13 | 14 | /* 15 | * 16 | typeid = "son | top | self" 17 | top: 顶级栏目 : parentid 为 0 18 | son: 父id 为 reid的下级分类 19 | self: 同级 父ID为reid的同级栏目 20 | 21 | row = "10" 调用数量 22 | 23 | channel(typeid, reid, type, row) 24 | */ 25 | func Channel(args jet.Arguments) reflect.Value { 26 | var arr = []tables.Category{} 27 | helper.Cache().Remember("pine:tag:channel:"+getTagHash(args), &arr, func() (any, error) { 28 | if !checkArgType(&args) { 29 | return &arr, nil 30 | } 31 | startTime := time.Now() 32 | 33 | _typeid := getNumber(args.Get(0)) 34 | _reid := getNumber(args.Get(1)) 35 | _type := args.Get(2).String() 36 | _row := int(getNumber(args.Get(3))) 37 | _noself := args.Get(4).String() 38 | if _row == 0 { 39 | _row = 10 40 | } 41 | m := models.NewCategoryModel() 42 | if _typeid != 0 { 43 | cat := m.GetCategory(_typeid) 44 | if cat == nil { 45 | return &arr, nil 46 | } 47 | _reid = cat.Parentid 48 | } 49 | orm := getCategoryOrm().Limit(_row).Asc("listorder") 50 | switch _type { 51 | case "top": 52 | _reid = 0 53 | orm.Where("parentid = 0") 54 | case "son": 55 | if _typeid == 0 { // 没有设置typeid 返回空 56 | return &arr, nil 57 | } 58 | orm.Where("parentid = ?", _typeid) 59 | case "self": 60 | orm.Where("parentid = ?", _reid) 61 | if _noself == "yes" { 62 | orm.Where("catid <> ?", _typeid) 63 | } 64 | } 65 | 66 | orm.Find(&arr) 67 | 68 | if len(arr) == 0 && _type == "son" && _reid != 0 { 69 | //如果用子栏目模式,当没有子栏目时显示同级栏目 70 | getCategoryOrm().Limit(_row).Asc("listorder").Where("parentid = ?", _reid).Find(&arr) 71 | } 72 | for k, v := range arr { 73 | if v.Type != 2 { 74 | cat1s := m.GetPosArr(v.Catid) 75 | arr[k].Url = fmt.Sprintf("/%s/", m.GetUrlPrefixWithCategoryArr(cat1s)) 76 | } 77 | } 78 | fmt.Println("channel 耗时", time.Now().Sub(startTime)) 79 | 80 | return &arr, nil 81 | }) 82 | return reflect.ValueOf(arr) 83 | } 84 | -------------------------------------------------------------------------------- /src/application/controllers/backend/const.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | type MenuV2 struct { 4 | Path string `json:"path"` 5 | Label string `json:"label"` 6 | NodePath string `json:"nodePath"` 7 | Exact bool `json:"exact"` 8 | Icon string `json:"icon"` 9 | PathToComponent string `json:"pathToComponent"` 10 | SideVisible bool `json:"sideVisible"` 11 | Children []MenuV2 `json:"children"` 12 | } 13 | 14 | type ThemeConfig struct { 15 | Name string `json:"name"` 16 | Author string `json:"author"` 17 | Description string `json:"description"` 18 | Extra map[string]any `json:"extra"` 19 | IsDefault bool `json:"is_default"` 20 | Dir string `json:"dir"` 21 | } 22 | 23 | type FieldShowInPageList struct { 24 | Forms []KV `json:"forms"` 25 | List []KV `json:"list"` 26 | } 27 | 28 | type KV struct { 29 | Label string `json:"label"` 30 | Value any `json:"value"` 31 | Name string `json:"-"` 32 | Checked bool `json:"-"` 33 | } 34 | 35 | type TabsSchema struct { 36 | Title string `json:"title"` 37 | Hash string `json:"hash"` 38 | Body FormController `json:"body"` 39 | } 40 | 41 | type FormController struct { 42 | Title string `json:"title"` 43 | Api string `json:"api"` 44 | Type string `json:"type"` 45 | Mode string `json:"mode"` 46 | Controls []FormControl `json:"controls"` 47 | } 48 | 49 | type FormControl struct { 50 | Type string `json:"type"` 51 | Name string `json:"name"` 52 | Label string `json:"label"` 53 | Value any `json:"value"` 54 | Options []KV `json:"options"` 55 | Validations string `json:"validations"` 56 | Required bool `json:"required"` 57 | Description string `json:"description"` 58 | Placeholder string `json:"placeholder"` 59 | ValidationErrors string `json:"validationErrors"` 60 | Multiple bool `json:"multiple"` 61 | Precision int `json:"precision"` 62 | Inline bool `json:"inline"` 63 | Buttons []any `json:"buttons"` 64 | Limits []string `json:"limits"` 65 | LimitsLogic string `json:"limitsLogic"` 66 | } 67 | -------------------------------------------------------------------------------- /resources/themes/default/head2.jet: -------------------------------------------------------------------------------- 1 | {{ import "tags.jet" }} 2 | 3 |
    4 | 5 | 33 |
    34 |
    35 |

    内容字号:默认大号超大号

    段落设置:段首缩进取消段首缩进

    字体设置:切换到微软雅黑切换到宋体

    36 | 37 |
    38 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/channelartlist.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/CloudyKit/jet" 9 | "github.com/xiusin/pinecms/src/application/models" 10 | "github.com/xiusin/pinecms/src/application/models/tables" 11 | "xorm.io/xorm" 12 | ) 13 | 14 | /* 15 | * 16 | sons 是否附带直属子分类数据, 可用于下级数据的迭代, 可取代channel的sun功能 17 | active 是否附带当前活动状态逻辑, 一般用于下级菜单激活上级菜单的高亮样式 18 | topid 记录当前所处页面的tid 19 | {{channellist = channelartlist(typeid, row, topid, sons, active)}} 20 | */ 21 | func ChannelArtList(args jet.Arguments) reflect.Value { 22 | if !checkArgType(&args) { 23 | return defaultArrReturnVal 24 | } 25 | _row := int(getNumber(args.Get(1))) 26 | if _row <= 0 { 27 | _row = 20 28 | } 29 | var _typeid string 30 | if isNumber(args.Get(0)) { 31 | _typeid = fmt.Sprintf("%d", getNumber(args.Get(0))) 32 | } else { 33 | _typeid = args.Get(0).String() 34 | } 35 | 36 | var orm *xorm.Session 37 | if _typeid == "0" || _typeid == "top" { 38 | orm = getCategoryOrm().Where("parentid = 0") 39 | } else if strings.Contains(_typeid, ",") { 40 | orm = getCategoryOrm().In("catid", strings.Split(_typeid, ",")) 41 | } else { 42 | orm = getCategoryOrm().Where("parentid = ?", _typeid) 43 | } 44 | 45 | _topId := getNumber(args.Get(2)) // 当前页面的ID 46 | 47 | var cats []tables.Category 48 | 49 | orm.Limit(_row).Select("id, parentid, catname, type, model_id, description, thumb, url").Asc("listorder").Find(&cats) 50 | 51 | if len(cats) == 0 { 52 | return defaultArrReturnVal 53 | } 54 | m := models.NewCategoryModel() 55 | 56 | withSons := args.Get(3).Bool() 57 | withActive := args.Get(4).Bool() 58 | 59 | for k, v := range cats { 60 | if v.Type != 2 { 61 | if withSons { 62 | var sons tables.Category 63 | count, _ := getCategoryOrm().Where("parentid = ?", v.Catid).Count(&sons) 64 | if count > 0 { 65 | cats[k].HasSon = true 66 | } 67 | } 68 | if withActive { 69 | cat1s := m.GetPosArr(v.Catid) 70 | cats[k].Url = fmt.Sprintf("/%s/", m.GetUrlPrefixWithCategoryArr(cat1s)) 71 | for _, v := range cat1s { 72 | if v.Catid == _topId { 73 | cats[k].Active = true 74 | break 75 | } 76 | } 77 | } 78 | cats[k].Url = fmt.Sprintf("/%s/", m.GetUrlPrefixWithCategoryArr(m.GetPosArr(v.Catid))) 79 | } 80 | } 81 | return reflect.ValueOf(cats) 82 | } 83 | -------------------------------------------------------------------------------- /src/application/controllers/backend/dict_controller.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xiusin/pinecms/src/application/models/tables" 7 | "github.com/xiusin/pinecms/src/common/helper" 8 | ) 9 | 10 | type DictController struct { 11 | BaseController 12 | } 13 | 14 | func (c *DictController) Construct() { 15 | c.Group = "字典管理" 16 | c.SubGroup = "字典列表管理" 17 | c.KeywordsSearch = []SearchFieldDsl{ 18 | {Field: "value", Op: "LIKE", DataExp: "%$?%"}, 19 | {Field: "name", Op: "LIKE", DataExp: "%$?%"}, 20 | } 21 | c.SearchFields = []SearchFieldDsl{{Field: "status"}, {Field: "cid"}} 22 | 23 | c.Table = &tables.Dict{} 24 | c.Entries = &[]*tables.Dict{} 25 | c.ApiEntityName = "字典属性" 26 | 27 | c.BaseController.Construct() 28 | c.OpBefore = c.before 29 | c.OpAfter = c.after 30 | } 31 | 32 | func (c *DictController) before(act int, param any) error { 33 | switch act { 34 | case OpAdd, OpEdit: 35 | p := param.(*tables.Dict) 36 | id, cid, name := p.Id, p.Cid, p.Name 37 | if cid == 0 { 38 | return errors.New("请选择所属字典分类") 39 | } 40 | sess := c.Orm.Table(c.Table).Where("name = ?", name).Where("cid = ?", cid) 41 | if OpEdit == act { 42 | sess = sess.Where("id <> ?", id) 43 | } 44 | exist, err := sess.Exist() 45 | if err != nil { 46 | return err 47 | } 48 | if exist { 49 | return errors.New("该分类下已经存在此标识") 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | func (c *DictController) after(act int, param any) error { 56 | switch act { 57 | case OpList: 58 | entities := c.Entries.(*[]*tables.Dict) 59 | var ids []uint 60 | for _, dict := range *entities { 61 | ids = append(ids, dict.Cid) 62 | } 63 | if len(ids) > 0 { 64 | var cats []tables.DictCategory 65 | c.Orm.In("id", ids).Find(&cats) 66 | var m = map[uint]string{} 67 | for _, cat := range cats { 68 | m[cat.Id] = cat.Name 69 | } 70 | for _, dict := range *entities { 71 | dict.CatName, _ = m[dict.Cid] 72 | } 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | func (c *DictController) GetSelect() { 79 | cid, _ := c.Ctx().Input().GetInt64("cid") 80 | if cid == 0 { 81 | helper.Ajax("请传入字典分类ID", 1, c.Ctx()) 82 | return 83 | } 84 | var dicts []tables.KV 85 | _ = c.Orm.Where("status = 1").Where("cid = ?", cid).Find(c.Entries) 86 | m := c.Entries.(*[]tables.Dict) 87 | for _, model := range *m { 88 | dicts = append(dicts, tables.KV{Label: model.Name, Value: model.Value}) 89 | } 90 | helper.Ajax(dicts, 0, c.Ctx()) 91 | } 92 | -------------------------------------------------------------------------------- /src/application/controllers/taglibs/list.go: -------------------------------------------------------------------------------- 1 | package taglibs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/CloudyKit/jet" 10 | "github.com/xiusin/pine" 11 | "github.com/xiusin/pinecms/src/application/controllers" 12 | "github.com/xiusin/pinecms/src/application/models" 13 | "github.com/xiusin/pinecms/src/common/helper" 14 | "github.com/xiusin/pinecms/src/config" 15 | ) 16 | 17 | /** 18 | list(typeid, page, pagesize, modelname, titlelen, orderby, orderway) 19 | */ 20 | 21 | func List(args jet.Arguments) reflect.Value { 22 | if !checkArgType(&args) { 23 | return defaultArrReturnVal 24 | } 25 | defer func() { 26 | if err := recover(); err != nil { 27 | pine.Logger().Error(fmt.Sprintf("ArcList Failed %s", err)) 28 | } 29 | }() 30 | catid := getNumber(args.Get(0)) 31 | page := getNumber(args.Get(1)) 32 | pagesize := getNumber(args.Get(2)) 33 | if pagesize == 0 { 34 | conf, _ := config.SiteConfig() 35 | ps, _ := strconv.Atoi(conf["SITE_PAGE_SIZE"]) 36 | if ps == 0 { 37 | pagesize = 15 38 | } else { 39 | pagesize = int64(ps) 40 | } 41 | } 42 | if page < 1 { 43 | page = 1 44 | } 45 | offset := (page - 1) * pagesize 46 | tableName := args.Get(3).String() 47 | if tableName == "" || catid < 1 { 48 | return defaultArrReturnVal 49 | } 50 | titlelen := int(getNumber(args.Get(4))) 51 | orderBy := args.Get(5).String() 52 | orderWay := args.Get(6).String() 53 | if orderWay == "" { 54 | orderWay = "desc" 55 | } 56 | if orderBy == "" { 57 | orderBy = "listorder" 58 | } 59 | if orderBy == "rand" { 60 | if strings.ToLower(config.DB().Db.DbDriver) == "mysql" { 61 | orderBy = "RAND()" 62 | } else { 63 | orderBy = "RANDOM()" 64 | } 65 | } else { 66 | orderBy = fmt.Sprintf("%s %s", orderBy, orderWay) 67 | } 68 | categoryTable := getCategoryTable() 69 | modelTable := controllers.GetTableName(tableName) 70 | m := models.NewCategoryModel() 71 | ids := m.GetNextCategoryOnlyCatids(catid, true) 72 | sess := getOrmSess(tableName).Where(modelTable+".deleted_time IS NULL").Where(modelTable+".status = 1").In(modelTable+".catid", ids).Limit(int(pagesize), int(offset)).OrderBy(orderBy) 73 | defer sess.Close() 74 | sess.Join("LEFT", categoryTable, categoryTable+".id = "+modelTable+".catid") 75 | sess.Select(fmt.Sprintf("%s.*, %s.catname as typename", modelTable, categoryTable)) 76 | list, _ := sess.QueryString() 77 | helper.HandleArtListInfo(list, titlelen) 78 | return reflect.ValueOf(list) 79 | } 80 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/Apiform/Api.go: -------------------------------------------------------------------------------- 1 | package Apiform 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | uuid "github.com/satori/go.uuid" 8 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/common" 9 | "github.com/xiusin/pinecms/src/application/controllers/backend/webssh/tables" 10 | "github.com/xiusin/pinecms/src/common/helper" 11 | ) 12 | 13 | type Login struct { 14 | Phone int64 `form:"phone" binding:"required"` 15 | Code string `form:"code" binding:"required"` 16 | } 17 | 18 | type Send struct { 19 | Phone string `form:"phone" binding:"required"` 20 | } 21 | 22 | type Slist struct { 23 | Page int `form:"page" binding:"required"` 24 | Limit int `form:"limit" binding:"required"` 25 | } 26 | 27 | type List_resp struct { 28 | List []tables.SSHServer 29 | Count uint 30 | } 31 | 32 | type GetTerm struct { 33 | ID int64 `form:"id" binding:"required"` 34 | Password string `form:"password"` 35 | Setpass string `from:"setpass"` 36 | } 37 | 38 | type WsAuth struct { 39 | Sid string `uri:"sid" binding:"required,uuid"` 40 | } 41 | 42 | type Edit struct { 43 | ID int64 `form:"id" binding:"required"` 44 | Nickname string `form:"nickname"` 45 | Ip string `form:"ip"` 46 | Port int `form:"port"` 47 | Username string `form:"username"` 48 | } 49 | 50 | type EditPass struct { 51 | ID int64 `form:"id" binding:"required"` 52 | Password string `form:"password" binding:"required"` 53 | } 54 | 55 | type Addser struct { 56 | Nickname string `form:"nickname"` 57 | Ip string `form:"ip" binding:"required"` 58 | Port int `form:"port" binding:"required"` 59 | Username string `form:"username" binding:"required"` 60 | Password string `form:"password" binding:"required"` 61 | Desc string `form:"desc"` 62 | } 63 | 64 | type SerInfo struct { 65 | ID int64 66 | Ip string 67 | Port int 68 | Username string 69 | Password string 70 | BindUser int64 71 | } 72 | 73 | func (t *GetTerm) Decode(server tables.SSHServer) (sid string, err error) { 74 | sid = uuid.Must(uuid.NewV4(), nil).String() 75 | sPass, err := common.AesDecryptCBC(server.Password, []byte(t.Setpass)) 76 | if err != nil { 77 | return "", err 78 | } 79 | if sPass == "" { 80 | return "", errors.New("秘钥验证失败") 81 | } else { 82 | var serinfo = SerInfo{server.Id, server.Ip, server.Port, server.Username, sPass, server.BindUser} 83 | sInfo, _ := json.Marshal(serinfo) 84 | helper.Cache().Set(sid, sInfo, 10) 85 | } 86 | 87 | return sid, nil 88 | } 89 | -------------------------------------------------------------------------------- /cmd/crud/crud_stub.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | const ( 4 | controllerDir = "src/application/controllers/backend/" 5 | tableDir = "src/application/models/tables/" 6 | feModuleDir = "src/cool/modules/" 7 | routerFile = "src/router/module.go" 8 | theme = "vim" 9 | goExt = ".go" 10 | controllerTpl = `package backend 11 | import ( 12 | "github.com/xiusin/pinecms/src/application/models/tables" 13 | ) 14 | 15 | type [ctrl] struct { 16 | BaseController 17 | } 18 | 19 | func (c *[ctrl]) Construct() { 20 | c.SearchFields = map[string]searchFieldDsl{ 21 | [searchFieldDsl] 22 | } 23 | c.Group = "[table]管理" 24 | c.ApiEntityName = "[table]" 25 | 26 | c.Table = &tables.[table]{} 27 | c.Entries = &[]tables.[table]{} 28 | }` 29 | tableTpl = `package tables 30 | 31 | [struct] 32 | ` 33 | 34 | indexVueTpl = ` 56 | 57 | ` 91 | 92 | serviceTsTpl = `import Router from "./router"; export default {[table]: new Router()};` 93 | 94 | serviceRouterTpl = `import { BaseService, Service, Permission } from "/@/core"; 95 | @Service("[table]") 96 | class Sys[table] extends BaseService {} 97 | export default Sys[table];` 98 | 99 | serviceIndexTsTpl = `import service from "./service"; export default { service };` 100 | ) 101 | -------------------------------------------------------------------------------- /resources/themes/default/head.jet: -------------------------------------------------------------------------------- 1 | {{ import "tags.jet" }} 2 |
    3 | 4 | 33 |
    34 | 36 | 37 |
    38 | 39 |
    40 |
    41 | 42 | {{yield myad(name="top_all")}} 43 |
    44 |
    -------------------------------------------------------------------------------- /src/application/controllers/frontend/list.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/xiusin/pine" 9 | "github.com/xiusin/pine/render/engine/pjet" 10 | "github.com/xiusin/pinecms/src/application/controllers" 11 | "github.com/xiusin/pinecms/src/application/models" 12 | "github.com/xiusin/pinecms/src/application/models/tables" 13 | ) 14 | 15 | func (c *IndexController) List(pageFilePath string) { 16 | c.setTemplateData() 17 | pageFilePath = GetStaticFile(pageFilePath) 18 | queryTid, _ := c.Ctx().Input().GetInt64("tid") 19 | tid, _ := c.Ctx().Params().GetInt64("tid", queryTid) 20 | if tid < 1 { 21 | c.Ctx().Abort(404) 22 | return 23 | } 24 | category, err := models.NewCategoryModel().GetCategoryFByIdForBE(tid) 25 | if err != nil || category.Model.Enabled == 0 { 26 | if err == nil { 27 | pine.Logger().Error("模型禁用,无可查看", c.Ctx().Path()) 28 | } else { 29 | pine.Logger().Error(err.Error()) 30 | } 31 | c.Ctx().Abort(404) 32 | return 33 | } 34 | page, _ := c.Ctx().Params().GetInt("page", 1) 35 | if page < 1 { 36 | page = 1 37 | } 38 | total, _ := getOrmSess(category.Model).In("id", models.NewCategoryModel().GetNextCategoryOnlyCatids(tid, true)).Count() 39 | tpl := "list_" + category.Model.Table + ".jet" // default tpl 40 | if len(category.Model.FeTplList) > 0 { // model tpl 41 | tpl = category.Model.FeTplList 42 | } 43 | if len(category.ListTpl) > 0 { // category tpl 44 | tpl = category.ListTpl 45 | } 46 | _ = os.MkdirAll(filepath.Dir(pageFilePath), os.ModePerm) 47 | f, err := os.Create(pageFilePath) 48 | if err != nil { 49 | c.Ctx().Abort(http.StatusInternalServerError, err.Error()) 50 | return 51 | } 52 | defer f.Close() 53 | jet := pine.Make(controllers.ServiceJetEngine).(*pjet.PineJet) 54 | temp, err := jet.GetTemplate(template(tpl)) 55 | if err != nil { 56 | c.Ctx().Abort(http.StatusInternalServerError, err.Error()) 57 | return 58 | } 59 | 60 | err = temp.Execute(f, viewDataToJetMap(c.Render().GetViewData()), struct { 61 | Field *tables.Category 62 | TypeID int64 63 | ArtCount int64 64 | ModelName string 65 | QP map[string]any 66 | PageNum int64 67 | }{ 68 | Field: category, 69 | TypeID: tid, 70 | ArtCount: total, 71 | PageNum: int64(page), 72 | ModelName: category.Model.Table, 73 | QP: c.Ctx().Input().All(), 74 | }) 75 | if err != nil { 76 | c.Ctx().Abort(http.StatusInternalServerError, err.Error()) 77 | return 78 | } 79 | data, _ := os.ReadFile(pageFilePath) 80 | _ = c.Ctx().WriteHTMLBytes(data) 81 | } 82 | -------------------------------------------------------------------------------- /cmd/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "slices" 6 | "strings" 7 | ) 8 | 9 | func AppPath() string { 10 | curPath, _ := os.Getwd() 11 | return curPath 12 | } 13 | 14 | func ScanDir(dir string, ignoreDirs []string) (list []struct { 15 | Path string 16 | IsDir bool 17 | }, err error) { 18 | fs, err := os.ReadDir(dir) 19 | if err != nil { 20 | return 21 | } 22 | list = append(list, struct { 23 | Path string 24 | IsDir bool 25 | }{Path: dir, IsDir: true}) 26 | 27 | for _, f := range fs { 28 | if f.IsDir() { 29 | if InSlice(f.Name(), ignoreDirs) { 30 | continue 31 | } 32 | l, err := ScanDir(dir+"/"+f.Name(), ignoreDirs) 33 | if err != nil { 34 | return nil, err 35 | } 36 | list = append(list, l...) 37 | } 38 | // 添加目录和文件到列表, 提供给监听器 39 | list = append(list, struct { 40 | Path string 41 | IsDir bool 42 | }{Path: dir + "/" + f.Name(), IsDir: f.IsDir()}) 43 | } 44 | return 45 | } 46 | 47 | func InSlice(needle string, haystacks []string) bool { 48 | return slices.Contains(haystacks, needle) 49 | } 50 | func SnakeString(s string) string { 51 | data := make([]byte, 0, len(s)*2) 52 | j := false 53 | num := len(s) 54 | for i := range num { 55 | d := s[i] 56 | if i > 0 && d >= 'A' && d <= 'Z' && j { 57 | data = append(data, '_') 58 | } 59 | if d != '_' { 60 | j = true 61 | } 62 | data = append(data, d) 63 | } 64 | return strings.ToLower(string(data[:])) 65 | } 66 | 67 | func StrFirstToUpper(str string) string { 68 | temp := strings.Split(strings.ReplaceAll(str, "_", "-"), "-") 69 | var upperStr string 70 | for y := 0; y < len(temp); y++ { 71 | vv := []rune(temp[y]) 72 | if y != 0 { 73 | for i := 0; i < len(vv); i++ { 74 | if i == 0 { 75 | vv[i] -= 32 76 | upperStr += string(vv[i]) // + string(vv[i+1]) 77 | } else { 78 | upperStr += string(vv[i]) 79 | } 80 | } 81 | } 82 | } 83 | return temp[0] + upperStr 84 | } 85 | 86 | func CamelString(s string) string { 87 | data := make([]byte, 0, len(s)) 88 | j := false 89 | k := false 90 | num := len(s) - 1 91 | for i := 0; i <= num; i++ { 92 | d := s[i] 93 | if k == false && d >= 'A' && d <= 'Z' { 94 | k = true 95 | } 96 | if d >= 'a' && d <= 'z' && (j || k == false) { 97 | d = d - 32 98 | j = false 99 | k = true 100 | } 101 | if k && d == '_' && num > i && s[i+1] >= 'a' && s[i+1] <= 'z' { 102 | j = true 103 | continue 104 | } 105 | data = append(data, d) 106 | } 107 | return string(data[:]) 108 | } 109 | -------------------------------------------------------------------------------- /src/application/controllers/backend/webssh/common/aes.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/md5" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "errors" 11 | ) 12 | 13 | func AesEncryptCBC(origData []byte, key []byte) string { 14 | // 分组秘钥 15 | // NewCipher该函数限制了输入k的长度必须为16, 24或者32 16 | key = Md5(key)[:24] 17 | block, _ := aes.NewCipher(key) 18 | blockSize := block.BlockSize() // 获取秘钥块的长度 19 | origData = pkcs7Padding(origData, blockSize) // 补全码 20 | //fmt.Println(blockSize) 21 | blockMode := cipher.NewCBCEncrypter(block, key[:blockSize]) // 加密模式 22 | //fmt.Println(string(Md5(key)[:blockSize])) 23 | encrypted := make([]byte, len(origData)) // 创建数组 24 | blockMode.CryptBlocks(encrypted, origData) // 加密 25 | return base64.StdEncoding.EncodeToString(encrypted) 26 | } 27 | func AesDecryptCBC(enc string, key []byte) (string, error) { 28 | key = Md5(key)[:24] 29 | encrypted, _ := base64.StdEncoding.DecodeString(enc) 30 | block, _ := aes.NewCipher(key) // 分组秘钥 31 | blockSize := block.BlockSize() // 获取秘钥块的长度 32 | blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式 33 | decrypted := make([]byte, len(encrypted)) // 创建数组 34 | blockMode.CryptBlocks(decrypted, encrypted) // 解密 35 | decrypted, err := pkcs7UnPadding(decrypted) // 去除补全码 36 | if err != nil { 37 | return "", err 38 | } 39 | return string(decrypted), nil 40 | } 41 | func pkcs5Padding(ciphertext []byte, blockSize int) []byte { 42 | padding := blockSize - len(ciphertext)%blockSize 43 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 44 | return append(ciphertext, padtext...) 45 | } 46 | func pkcs5UnPadding(origData []byte) []byte { 47 | length := len(origData) 48 | unpadding := int(origData[length-1]) 49 | return origData[:(length - unpadding)] 50 | } 51 | 52 | func pkcs7Padding(ciphertext []byte, blockSize int) []byte { 53 | padding := blockSize - len(ciphertext)%blockSize 54 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 55 | return append(ciphertext, padtext...) 56 | } 57 | 58 | func pkcs7UnPadding(origData []byte) ([]byte, error) { 59 | length := len(origData) 60 | unpadding := int(origData[length-1]) 61 | if length-unpadding < 0 { 62 | return []byte{}, errors.New("解密失败") 63 | } 64 | return origData[:(length - unpadding)], nil 65 | } 66 | 67 | func Md5(str []byte) []byte { 68 | h := md5.New() 69 | h.Write(str) 70 | //fmt.Println(hex.EncodeToString(h.Sum(nil))) 71 | return []byte(hex.EncodeToString(h.Sum(nil))) 72 | } 73 | --------------------------------------------------------------------------------