feat(notice+job): 通知模块(渠道/规则/日志/测试) + 自动任务管理(DB驱动gcron) + 用户表加Bark/pushplus字段
This commit is contained in:
parent
e18b25f32a
commit
0569c70690
@ -1,6 +1,8 @@
|
||||
# service.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-08-27 | FIX | 菜单种子主键冲突修复(743683e):recruitment/003_menu.sql 招聘中心一级菜单 id=95 与 014_house_presale_menu.sql 新房预售 id=95(挂看房中心 90)ON DUPLICATE 互相覆盖、后导者赢,本地实际发生(新房预售菜单被顶掉致 /house/presale 404、生产库同样风险);改为 id=96 后发现 015_server_security.sql(并行会话,生产已部署)也用 96=安全日志(挂系统监控5),再改 **97**:95=新房预售(挂90)、96=安全日志(挂5)、97=招聘中心,三模块错开;本地按 014→015→003 重放,routes 接口 22 节点验证全部正确(e18b25f)。⚠️ 生产库需按 014→015→003 顺序重放修复(当前生产 95=招聘中心、新房预售菜单丢失)
|
||||
|
||||
2026-08-27 | CHG | 服务器安全日志模块:015 SQL 建 server_security_log 表 + 菜单(96安全日志/960查询/961统计);api/serversecurity/v1 三接口全 POST——open 上报 /api/service/open/security/log/report(body token+list,校验 internalToken=INTERNAL_TOKEN env)+ admin 查询 /server-security/log/list、统计 /server-security/log/stats(时间/IP/类型/端口筛选+TOP攻击源);entity/do/dao(主库)/dto/service/controller(ReportController+ManageController 双控制器);cmd.go 注入 INTERNAL_TOKEN→internalToken、open 组绑 NewReport、protected 组绑 NewManage;go build 通过,交叉编译上传 + 服务器 docker build + 容器重建(复用 env 追加 INTERNAL_TOKEN),路由/上报验证 OK(错误 token 返回 10002)
|
||||
2026-08-27 | FIX | 本地登录 500 修复:根因=config.dev.yaml 的 ${RECRUITMENT_DB_DSN} 占位符在本地无对应 env(injectEnv 只回填已有变量),gdb 配置解析失败致 g.DB() 全局报 invalid link configuration;本地补建 recruitment 库(导入 001_init+002_seed_sources,6 表 5 源)+.env.dev 与 .run/service-dev.run.xml 补 RECRUITMENT_DB_DSN(两文件 gitignore 不入库);附带修复 gcron 5 段式表达式注册失败(invalid pattern "0 3 * * *")——改 6 段式 '0 0 3 * * *'/'0 0 8 * * *',此前线上每日抓取/推送 cron 实际从未执行(f81ce64);验证:重启后登录返回业务码 30002(非500)、无 cron 报错
|
||||
2026-08-27 | CHG | 看房新增新房预售证模块:api/house/presale + controller/service(POST /api/service/admin/house/presale/list,筛选 region/purpose/keyword,id 倒序);dto 加 HousePresaleVO;house_presale 加 publish_date 字段(013 SQL 同步 + hack/config.yaml tables 补 house_presale + gen dao 更新);权限种子 014(菜单 95 新房预售 + 950 查询权限);go build 通过、接口返回 487 条干净数据(presaleNo/publishDate 正确)
|
||||
|
||||
73
api/job/v1/job.go
Normal file
73
api/job/v1/job.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Package job_v1 自动任务管理模块接口契约(DB 驱动的 gcron 调度 + 运行记录)。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含参数;入参一律 body。
|
||||
package job_v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ---------- 任务列表 ----------
|
||||
type JobItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
JobType string `json:"jobType"`
|
||||
CronExpr string `json:"cronExpr"`
|
||||
Enabled int `json:"enabled"`
|
||||
Remark string `json:"remark"`
|
||||
LastRunAt string `json:"lastRunAt"`
|
||||
LastResult int `json:"lastResult"` // 0未运行 1成功 2失败
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
type AutoJobListReq struct {
|
||||
g.Meta `path:"/auto-job/list" method:"post" tags:"Admin/AutoJob" summary:"自动任务列表"`
|
||||
}
|
||||
|
||||
type AutoJobListRes struct {
|
||||
List []*JobItem `json:"list"`
|
||||
}
|
||||
|
||||
// ---------- 保存任务(改 cron/启用/备注) ----------
|
||||
type AutoJobSaveReq struct {
|
||||
g.Meta `path:"/auto-job/save" method:"post" tags:"Admin/AutoJob" summary:"保存自动任务(改cron/启用)"`
|
||||
Id uint64 `json:"id" v:"required"`
|
||||
CronExpr string `json:"cronExpr"`
|
||||
Enabled int `json:"enabled" d:"1"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type AutoJobSaveRes struct{}
|
||||
|
||||
// ---------- 手动触发 ----------
|
||||
type AutoJobTriggerReq struct {
|
||||
g.Meta `path:"/auto-job/trigger" method:"post" tags:"Admin/AutoJob" summary:"手动触发任务"`
|
||||
Id uint64 `json:"id" v:"required"`
|
||||
}
|
||||
|
||||
type AutoJobTriggerRes struct {
|
||||
Summary string `json:"summary"`
|
||||
Ok bool `json:"ok"`
|
||||
}
|
||||
|
||||
// ---------- 任务运行日志 ----------
|
||||
type JobLogItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
JobId uint64 `json:"jobId"`
|
||||
JobName string `json:"jobName"`
|
||||
RunAt string `json:"runAt"`
|
||||
Result int `json:"result"`
|
||||
Error string `json:"error"`
|
||||
Summary string `json:"summary"`
|
||||
DurationMs int `json:"durationMs"`
|
||||
}
|
||||
|
||||
type AutoJobLogListReq struct {
|
||||
g.Meta `path:"/auto-job/log/list" method:"post" tags:"Admin/AutoJob" summary:"任务运行日志分页"`
|
||||
Page int `json:"page" d:"1" v:"min:1"`
|
||||
Size int `json:"size" d:"10" v:"min:1|max:100"`
|
||||
JobId uint64 `json:"jobId"` // 0=全部
|
||||
}
|
||||
|
||||
type AutoJobLogListRes struct {
|
||||
List []*JobLogItem `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
128
api/notice/v1/notice.go
Normal file
128
api/notice/v1/notice.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Package notice_v1 通知模块接口契约(统一推送管理:渠道/规则/日志/测试)。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含参数;入参一律 body。
|
||||
package notice_v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ---------- 通知渠道 ----------
|
||||
type NoticeChannelItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Enabled int `json:"enabled"`
|
||||
Config string `json:"config"` // JSON 字符串
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type NoticeChannelListReq struct {
|
||||
g.Meta `path:"/notice/channel/list" method:"post" tags:"Admin/Notice" summary:"通知渠道列表"`
|
||||
}
|
||||
|
||||
type NoticeChannelListRes struct {
|
||||
List []*NoticeChannelItem `json:"list"`
|
||||
}
|
||||
|
||||
type NoticeChannelSaveReq struct {
|
||||
g.Meta `path:"/notice/channel/save" method:"post" tags:"Admin/Notice" summary:"保存通知渠道(新增/更新)"`
|
||||
Id uint64 `json:"id"` // 0=新增
|
||||
Code string `json:"code" v:"required"`
|
||||
Name string `json:"name" v:"required"`
|
||||
Enabled int `json:"enabled" d:"1"`
|
||||
Config string `json:"config"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type NoticeChannelSaveRes struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
// ---------- 通知规则 ----------
|
||||
type NoticeRuleItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
EventType string `json:"eventType"`
|
||||
ChannelCodes []string `json:"channelCodes"`
|
||||
UserIds []uint64 `json:"userIds"`
|
||||
TitleTemplate string `json:"titleTemplate"`
|
||||
BodyTemplate string `json:"bodyTemplate"`
|
||||
Enabled int `json:"enabled"`
|
||||
}
|
||||
|
||||
type NoticeRuleListReq struct {
|
||||
g.Meta `path:"/notice/rule/list" method:"post" tags:"Admin/Notice" summary:"通知规则列表"`
|
||||
EventType string `json:"eventType"` // 可选过滤
|
||||
}
|
||||
|
||||
type NoticeRuleListRes struct {
|
||||
List []*NoticeRuleItem `json:"list"`
|
||||
}
|
||||
|
||||
type NoticeRuleSaveReq struct {
|
||||
g.Meta `path:"/notice/rule/save" method:"post" tags:"Admin/Notice" summary:"保存通知规则(新增/更新)"`
|
||||
Id uint64 `json:"id"` // 0=新增
|
||||
Name string `json:"name" v:"required"`
|
||||
EventType string `json:"eventType" v:"required"`
|
||||
ChannelCodes []string `json:"channelCodes" v:"required|min-length:1"`
|
||||
UserIds []uint64 `json:"userIds" v:"required|min-length:1"`
|
||||
TitleTemplate string `json:"titleTemplate"`
|
||||
BodyTemplate string `json:"bodyTemplate"`
|
||||
Enabled int `json:"enabled" d:"1"`
|
||||
}
|
||||
|
||||
type NoticeRuleSaveRes struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
type NoticeRuleDeleteReq struct {
|
||||
g.Meta `path:"/notice/rule/delete" method:"post" tags:"Admin/Notice" summary:"删除通知规则"`
|
||||
Id uint64 `json:"id" v:"required"`
|
||||
}
|
||||
|
||||
type NoticeRuleDeleteRes struct{}
|
||||
|
||||
// ---------- 通知日志 ----------
|
||||
type NoticeLogItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
RuleId uint64 `json:"ruleId"`
|
||||
EventType string `json:"eventType"`
|
||||
ChannelCode string `json:"channelCode"`
|
||||
UserId uint64 `json:"userId"`
|
||||
Target string `json:"target"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Result int `json:"result"`
|
||||
Error string `json:"error"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type NoticeLogListReq struct {
|
||||
g.Meta `path:"/notice/log/list" method:"post" tags:"Admin/Notice" summary:"通知发送记录分页"`
|
||||
Page int `json:"page" d:"1" v:"min:1"`
|
||||
Size int `json:"size" d:"10" v:"min:1|max:100"`
|
||||
EventType string `json:"eventType"`
|
||||
ChannelCode string `json:"channelCode"`
|
||||
Result int `json:"result"` // 0全部 1成功 2失败
|
||||
DateFrom string `json:"dateFrom"`
|
||||
DateTo string `json:"dateTo"`
|
||||
}
|
||||
|
||||
type NoticeLogListRes struct {
|
||||
List []*NoticeLogItem `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ---------- 测试发送 ----------
|
||||
type NoticeTestReq struct {
|
||||
g.Meta `path:"/notice/test" method:"post" tags:"Admin/Notice" summary:"测试通知(按规则或直接指定)"`
|
||||
RuleId uint64 `json:"ruleId"` // 按规则测试(0=手动指定)
|
||||
ChannelCode string `json:"channelCode"` // 手动:渠道编码
|
||||
Target string `json:"target"` // 手动:设备key/token
|
||||
UserIds []uint64 `json:"userIds"` // 手动:或按用户
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type NoticeTestRes struct {
|
||||
Result bool `json:"result"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@ -4,15 +4,19 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
"github.com/gogf/gf/v2/os/genv"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
|
||||
adminctl "service.xpcool.com/internal/controller/admin"
|
||||
housectl "service.xpcool.com/internal/controller/house"
|
||||
openctl "service.xpcool.com/internal/controller/open"
|
||||
recruitmentctl "service.xpcool.com/internal/controller/recruitment"
|
||||
noticectl "service.xpcool.com/internal/controller/notice"
|
||||
jobctl "service.xpcool.com/internal/controller/job"
|
||||
serversecurityctl "service.xpcool.com/internal/controller/serversecurity"
|
||||
userctl "service.xpcool.com/internal/controller/user"
|
||||
"service.xpcool.com/internal/library/jwt"
|
||||
@ -32,40 +36,59 @@ import (
|
||||
housetransaction "service.xpcool.com/internal/service/house/transaction"
|
||||
housepresale "service.xpcool.com/internal/service/house/presale"
|
||||
recruitmentsvc "service.xpcool.com/internal/service/recruitment"
|
||||
noticesvc "service.xpcool.com/internal/service/notice"
|
||||
jobsvc "service.xpcool.com/internal/service/job"
|
||||
serversecuritysvc "service.xpcool.com/internal/service/serversecurity"
|
||||
)
|
||||
|
||||
// 开发环境默认值:任何启动方式(GoLand / 命令行 / go run)漏注入环境变量时兜底,
|
||||
// 避免 ${DB_DSN} 占位符原样传入 gdb 导致全站接口 500。
|
||||
// 生产(GF_GCFG_ENV=prod)不启用兜底,配置缺失应显性报错。
|
||||
const (
|
||||
devDefaultDBDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/service_xpcool_com?loc=Local"
|
||||
devDefaultRecruitmentDSN = "mysql:root:root123@tcp(127.0.0.1:3306)/recruitment?loc=Local"
|
||||
devDefaultJWTSecret = "dev-only-secret-not-for-production"
|
||||
)
|
||||
|
||||
// setIfEmpty 仅当 env 存在时写入配置(优先级:env > 配置文件默认值)。
|
||||
func setIfEmpty(adapter *gcfg.AdapterFile, key, envName string) {
|
||||
if v := genv.Get(envName); !v.IsEmpty() {
|
||||
_ = adapter.Set(key, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
// injectEnv 手动把关键环境变量写入配置系统。
|
||||
// 注意:gf v2.10.2 起配置不再自动替换 ${ENV} 占位符,需在此显式注入,
|
||||
// 否则 config.dev.yaml 中的 ${DB_DSN}/${JWT_SECRET} 会原样传给数据库与 JWT。
|
||||
// 开发环境下若 env 缺失,回填本地默认值,保证「零配置可启动」。
|
||||
func injectEnv(ctx context.Context) {
|
||||
adapter, ok := g.Cfg().GetAdapter().(*gcfg.AdapterFile)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v := genv.Get("DB_DSN"); !v.IsEmpty() {
|
||||
_ = adapter.Set("database.default.link", v.String())
|
||||
isProd := genv.Get("GF_GCFG_ENV").String() == "prod"
|
||||
if !isProd {
|
||||
// 开发兜底:仅当配置项仍是未解析的 ${...} 占位符时才写默认值,
|
||||
// 不覆盖 .env.dev / GoLand 运行配置注入的真实值。
|
||||
if v, _ := adapter.Get(ctx, "database.default.link"); v != nil && gstr.Contains(gconv.String(v), "${") {
|
||||
_ = adapter.Set("database.default.link", devDefaultDBDSN)
|
||||
}
|
||||
if v := genv.Get("JWT_SECRET"); !v.IsEmpty() {
|
||||
_ = adapter.Set("jwt.secret", v.String())
|
||||
if v, _ := adapter.Get(ctx, "database.recruitment.link"); v != nil && gstr.Contains(gconv.String(v), "${") {
|
||||
_ = adapter.Set("database.recruitment.link", devDefaultRecruitmentDSN)
|
||||
}
|
||||
if v, _ := adapter.Get(ctx, "jwt.secret"); v != nil && gstr.Contains(gconv.String(v), "${") {
|
||||
_ = adapter.Set("jwt.secret", devDefaultJWTSecret)
|
||||
}
|
||||
}
|
||||
setIfEmpty(adapter, "database.default.link", "DB_DSN")
|
||||
setIfEmpty(adapter, "jwt.secret", "JWT_SECRET")
|
||||
// 招聘模块独立数据库与 Bark 推送配置(自建 Bark 服务)。
|
||||
if v := genv.Get("RECRUITMENT_DB_DSN"); !v.IsEmpty() {
|
||||
_ = adapter.Set("database.recruitment.link", v.String())
|
||||
}
|
||||
if v := genv.Get("BARK_BASE_URL"); !v.IsEmpty() {
|
||||
_ = adapter.Set("bark.baseUrl", v.String())
|
||||
}
|
||||
if v := genv.Get("BARK_DEVICE_KEY"); !v.IsEmpty() {
|
||||
_ = adapter.Set("bark.deviceKey", v.String())
|
||||
}
|
||||
if v := genv.Get("BARK_PUSH_TIME"); !v.IsEmpty() {
|
||||
_ = adapter.Set("bark.pushTime", v.String())
|
||||
}
|
||||
setIfEmpty(adapter, "database.recruitment.link", "RECRUITMENT_DB_DSN")
|
||||
setIfEmpty(adapter, "bark.baseUrl", "BARK_BASE_URL")
|
||||
setIfEmpty(adapter, "bark.deviceKey", "BARK_DEVICE_KEY")
|
||||
setIfEmpty(adapter, "bark.pushTime", "BARK_PUSH_TIME")
|
||||
// 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。
|
||||
if v := genv.Get("INTERNAL_TOKEN"); !v.IsEmpty() {
|
||||
_ = adapter.Set("internalToken", v.String())
|
||||
}
|
||||
setIfEmpty(adapter, "internalToken", "INTERNAL_TOKEN")
|
||||
}
|
||||
|
||||
var (
|
||||
@ -92,6 +115,8 @@ var (
|
||||
housetransaction.RegisterTransaction(housetransaction.NewTransaction())
|
||||
housepresale.RegisterPresale(housepresale.NewPresale())
|
||||
recruitmentsvc.RegisterRecruitment(recruitmentsvc.New())
|
||||
noticesvc.RegisterNotice(noticesvc.New())
|
||||
jobsvc.RegisterJob(jobsvc.New())
|
||||
serversecuritysvc.RegisterServerSecurity(serversecuritysvc.New())
|
||||
s.Group("/api/service/open", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
|
||||
@ -120,11 +145,14 @@ var (
|
||||
protected.Bind(adminctl.New())
|
||||
protected.Bind(housectl.New())
|
||||
protected.Bind(recruitmentctl.New())
|
||||
protected.Bind(noticectl.New())
|
||||
protected.Bind(jobctl.New())
|
||||
protected.Bind(serversecurityctl.NewManage())
|
||||
})
|
||||
})
|
||||
// 启动招聘模块定时任务(每日增量抓取 + 早报推送)。
|
||||
recruitmentsvc.StartScheduler(ctx)
|
||||
// 启动自动任务调度器:招聘模块先把任务注册进来,再由 job 模块按 DB 配置统一调度。
|
||||
recruitmentsvc.RegisterTasks()
|
||||
jobsvc.Job().StartScheduler(ctx)
|
||||
s.Run()
|
||||
return nil
|
||||
},
|
||||
|
||||
@ -27,7 +27,8 @@ func (c *Controller) AdminList(ctx context.Context, req *adminv1.AdminListReq) (
|
||||
// AdminCreate 创建管理员。
|
||||
func (c *Controller) AdminCreate(ctx context.Context, req *adminv1.AdminCreateReq) (res *adminv1.AdminCreateRes, err error) {
|
||||
id, err := admin.AdminManage().Create(ctx, dto.AdminCreateInput{
|
||||
Username: req.Username, Password: req.Password, Nickname: req.Nickname, RoleIds: req.RoleIds,
|
||||
Username: req.Username, Password: req.Password, Nickname: req.Nickname,
|
||||
BarkDeviceId: req.BarkDeviceId, PushplusToken: req.PushplusToken, RoleIds: req.RoleIds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -37,7 +38,8 @@ func (c *Controller) AdminCreate(ctx context.Context, req *adminv1.AdminCreateRe
|
||||
|
||||
// AdminUpdate 更新管理员。
|
||||
func (c *Controller) AdminUpdate(ctx context.Context, req *adminv1.AdminUpdateReq) (res *adminv1.AdminUpdateRes, err error) {
|
||||
if err = admin.AdminManage().Update(ctx, dto.AdminUpdateInput{Id: req.Id, Nickname: req.Nickname, Status: req.Status, RoleIds: req.RoleIds}); err != nil {
|
||||
if err = admin.AdminManage().Update(ctx, dto.AdminUpdateInput{Id: req.Id, Nickname: req.Nickname, Status: req.Status,
|
||||
BarkDeviceId: req.BarkDeviceId, PushplusToken: req.PushplusToken, RoleIds: req.RoleIds}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adminv1.AdminUpdateRes{}, nil
|
||||
|
||||
68
internal/controller/job/job.go
Normal file
68
internal/controller/job/job.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Package job 自动任务模块控制器(绑定 admin 受权限保护组)。
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
jobv1 "service.xpcool.com/api/job/v1"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
"service.xpcool.com/internal/service/job"
|
||||
)
|
||||
|
||||
// Controller 实现自动任务端点。
|
||||
type Controller struct{}
|
||||
|
||||
// New 创建自动任务控制器。
|
||||
func New() *Controller { return &Controller{} }
|
||||
|
||||
// List 自动任务列表。
|
||||
func (c *Controller) List(ctx context.Context, req *jobv1.AutoJobListReq) (res *jobv1.AutoJobListRes, err error) {
|
||||
list, err := job.Job().List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*jobv1.JobItem, 0, len(list))
|
||||
for i := range list {
|
||||
v := &list[i]
|
||||
out = append(out, &jobv1.JobItem{
|
||||
Id: v.Id, Name: v.Name, Code: v.Code, JobType: v.JobType, CronExpr: v.CronExpr,
|
||||
Enabled: v.Enabled, Remark: v.Remark, LastRunAt: v.LastRunAt,
|
||||
LastResult: v.LastResult, LastError: v.LastError,
|
||||
})
|
||||
}
|
||||
return &jobv1.AutoJobListRes{List: out}, nil
|
||||
}
|
||||
|
||||
// Save 保存自动任务(cron/启用/备注)。
|
||||
func (c *Controller) Save(ctx context.Context, req *jobv1.AutoJobSaveReq) (res *jobv1.AutoJobSaveRes, err error) {
|
||||
if err = job.Job().Save(ctx, dto.JobInput{Id: req.Id, CronExpr: req.CronExpr, Enabled: req.Enabled, Remark: req.Remark}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &jobv1.AutoJobSaveRes{}, nil
|
||||
}
|
||||
|
||||
// Trigger 手动触发任务。
|
||||
func (c *Controller) Trigger(ctx context.Context, req *jobv1.AutoJobTriggerReq) (res *jobv1.AutoJobTriggerRes, err error) {
|
||||
summary, ok, err := job.Job().Trigger(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &jobv1.AutoJobTriggerRes{Summary: summary, Ok: ok}, nil
|
||||
}
|
||||
|
||||
// LogList 任务运行日志分页。
|
||||
func (c *Controller) LogList(ctx context.Context, req *jobv1.AutoJobLogListReq) (res *jobv1.AutoJobLogListRes, err error) {
|
||||
list, total, err := job.Job().LogList(ctx, dto.JobLogFilter{Page: req.Page, Size: req.Size, JobId: req.JobId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*jobv1.JobLogItem, 0, len(list))
|
||||
for i := range list {
|
||||
v := &list[i]
|
||||
out = append(out, &jobv1.JobLogItem{
|
||||
Id: v.Id, JobId: v.JobId, JobName: v.JobName, RunAt: v.RunAt,
|
||||
Result: v.Result, Error: v.Error, Summary: v.Summary, DurationMs: v.DurationMs,
|
||||
})
|
||||
}
|
||||
return &jobv1.AutoJobLogListRes{List: out, Total: total}, nil
|
||||
}
|
||||
108
internal/controller/notice/notice.go
Normal file
108
internal/controller/notice/notice.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Package notice 通知模块控制器(绑定 admin 受权限保护组)。
|
||||
package notice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
noticev1 "service.xpcool.com/api/notice/v1"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
"service.xpcool.com/internal/service/notice"
|
||||
)
|
||||
|
||||
// Controller 实现通知模块端点。
|
||||
type Controller struct{}
|
||||
|
||||
// New 创建通知控制器。
|
||||
func New() *Controller { return &Controller{} }
|
||||
|
||||
// ChannelList 通知渠道列表。
|
||||
func (c *Controller) ChannelList(ctx context.Context, req *noticev1.NoticeChannelListReq) (res *noticev1.NoticeChannelListRes, err error) {
|
||||
list, err := notice.Notice().ChannelList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*noticev1.NoticeChannelItem, 0, len(list))
|
||||
for i := range list {
|
||||
v := &list[i]
|
||||
out = append(out, ¬icev1.NoticeChannelItem{Id: v.Id, Code: v.Code, Name: v.Name, Enabled: v.Enabled, Config: v.Config, Remark: v.Remark})
|
||||
}
|
||||
return ¬icev1.NoticeChannelListRes{List: out}, nil
|
||||
}
|
||||
|
||||
// ChannelSave 保存通知渠道。
|
||||
func (c *Controller) ChannelSave(ctx context.Context, req *noticev1.NoticeChannelSaveReq) (res *noticev1.NoticeChannelSaveRes, err error) {
|
||||
id, err := notice.Notice().ChannelSave(ctx, dto.NoticeChannelInput{
|
||||
Id: req.Id, Code: req.Code, Name: req.Name, Enabled: req.Enabled, Config: req.Config, Remark: req.Remark,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬icev1.NoticeChannelSaveRes{Id: id}, nil
|
||||
}
|
||||
|
||||
// RuleList 通知规则列表。
|
||||
func (c *Controller) RuleList(ctx context.Context, req *noticev1.NoticeRuleListReq) (res *noticev1.NoticeRuleListRes, err error) {
|
||||
list, err := notice.Notice().RuleList(ctx, req.EventType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*noticev1.NoticeRuleItem, 0, len(list))
|
||||
for i := range list {
|
||||
v := &list[i]
|
||||
out = append(out, ¬icev1.NoticeRuleItem{
|
||||
Id: v.Id, Name: v.Name, EventType: v.EventType, ChannelCodes: v.ChannelCodes,
|
||||
UserIds: v.UserIds, TitleTemplate: v.TitleTemplate, BodyTemplate: v.BodyTemplate, Enabled: v.Enabled,
|
||||
})
|
||||
}
|
||||
return ¬icev1.NoticeRuleListRes{List: out}, nil
|
||||
}
|
||||
|
||||
// RuleSave 保存通知规则。
|
||||
func (c *Controller) RuleSave(ctx context.Context, req *noticev1.NoticeRuleSaveReq) (res *noticev1.NoticeRuleSaveRes, err error) {
|
||||
id, err := notice.Notice().RuleSave(ctx, dto.NoticeRuleInput{
|
||||
Id: req.Id, Name: req.Name, EventType: req.EventType, ChannelCodes: req.ChannelCodes,
|
||||
UserIds: req.UserIds, TitleTemplate: req.TitleTemplate, BodyTemplate: req.BodyTemplate, Enabled: req.Enabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬icev1.NoticeRuleSaveRes{Id: id}, nil
|
||||
}
|
||||
|
||||
// RuleDelete 删除通知规则。
|
||||
func (c *Controller) RuleDelete(ctx context.Context, req *noticev1.NoticeRuleDeleteReq) (res *noticev1.NoticeRuleDeleteRes, err error) {
|
||||
if err = notice.Notice().RuleDelete(ctx, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬icev1.NoticeRuleDeleteRes{}, nil
|
||||
}
|
||||
|
||||
// LogList 通知发送记录分页。
|
||||
func (c *Controller) LogList(ctx context.Context, req *noticev1.NoticeLogListReq) (res *noticev1.NoticeLogListRes, err error) {
|
||||
list, total, err := notice.Notice().LogList(ctx, dto.NoticeLogFilter{
|
||||
Page: req.Page, Size: req.Size, EventType: req.EventType,
|
||||
ChannelCode: req.ChannelCode, Result: req.Result, DateFrom: req.DateFrom, DateTo: req.DateTo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*noticev1.NoticeLogItem, 0, len(list))
|
||||
for i := range list {
|
||||
v := &list[i]
|
||||
out = append(out, ¬icev1.NoticeLogItem{
|
||||
Id: v.Id, RuleId: v.RuleId, EventType: v.EventType, ChannelCode: v.ChannelCode,
|
||||
UserId: v.UserId, Target: v.Target, Title: v.Title, Body: v.Body,
|
||||
Result: v.Result, Error: v.Error, CreatedAt: v.CreatedAt,
|
||||
})
|
||||
}
|
||||
return ¬icev1.NoticeLogListRes{List: out, Total: total}, nil
|
||||
}
|
||||
|
||||
// Test 测试通知发送。
|
||||
func (c *Controller) Test(ctx context.Context, req *noticev1.NoticeTestReq) (res *noticev1.NoticeTestRes, err error) {
|
||||
ok, msg, err := notice.Notice().Test(ctx, req.RuleId, req.ChannelCode, req.Target, req.UserIds, req.Title, req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬icev1.NoticeTestRes{Result: ok, Message: msg}, nil
|
||||
}
|
||||
51
internal/dao/notice.go
Normal file
51
internal/dao/notice.go
Normal file
@ -0,0 +1,51 @@
|
||||
// Package dao 通知模块与自动任务模块数据访问(主库默认分组,手写)。
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// noticeModel 主库通用模型构造。
|
||||
func noticeModel(table string) func(ctx context.Context) *gdb.Model {
|
||||
return func(ctx context.Context) *gdb.Model {
|
||||
return g.DB().Model(table).Safe()
|
||||
}
|
||||
}
|
||||
|
||||
type noticeChannelDao struct{ table string }
|
||||
|
||||
// NoticeChannel 通知渠道表访问对象。
|
||||
var NoticeChannel = noticeChannelDao{table: "notice_channel"}
|
||||
|
||||
func (d noticeChannelDao) Ctx(ctx context.Context) *gdb.Model { return noticeModel(d.table)(ctx) }
|
||||
|
||||
type noticeRuleDao struct{ table string }
|
||||
|
||||
// NoticeRule 通知规则表访问对象。
|
||||
var NoticeRule = noticeRuleDao{table: "notice_rule"}
|
||||
|
||||
func (d noticeRuleDao) Ctx(ctx context.Context) *gdb.Model { return noticeModel(d.table)(ctx) }
|
||||
|
||||
type noticeLogDao struct{ table string }
|
||||
|
||||
// NoticeLog 通知记录表访问对象。
|
||||
var NoticeLog = noticeLogDao{table: "notice_log"}
|
||||
|
||||
func (d noticeLogDao) Ctx(ctx context.Context) *gdb.Model { return noticeModel(d.table)(ctx) }
|
||||
|
||||
type autoJobDao struct{ table string }
|
||||
|
||||
// AutoJob 自动任务定义表访问对象。
|
||||
var AutoJob = autoJobDao{table: "auto_job"}
|
||||
|
||||
func (d autoJobDao) Ctx(ctx context.Context) *gdb.Model { return noticeModel(d.table)(ctx) }
|
||||
|
||||
type autoJobLogDao struct{ table string }
|
||||
|
||||
// AutoJobLog 自动任务运行日志表访问对象。
|
||||
var AutoJobLog = autoJobLogDao{table: "auto_job_log"}
|
||||
|
||||
func (d autoJobLogDao) Ctx(ctx context.Context) *gdb.Model { return noticeModel(d.table)(ctx) }
|
||||
@ -17,6 +17,8 @@ type AdminUser struct {
|
||||
PasswordHash any //
|
||||
Nickname any //
|
||||
Status any //
|
||||
BarkDeviceId any //
|
||||
PushplusToken any //
|
||||
LastLoginAt *gtime.Time //
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
|
||||
78
internal/model/do/notice.go
Normal file
78
internal/model/do/notice.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Package do 定义通知模块与自动任务模块的数据对象(手写)。
|
||||
package do
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// NoticeChannel 通知渠道 DO。
|
||||
type NoticeChannel struct {
|
||||
g.Meta `orm:"table:notice_channel, do:true"`
|
||||
Id any //
|
||||
Code any // 渠道编码
|
||||
Name any // 渠道名称
|
||||
Enabled any // 是否启用
|
||||
Config any // 配置JSON
|
||||
Remark any // 备注
|
||||
CreatedAt any //
|
||||
UpdatedAt any //
|
||||
}
|
||||
|
||||
// NoticeRule 通知规则 DO。
|
||||
type NoticeRule struct {
|
||||
g.Meta `orm:"table:notice_rule, do:true"`
|
||||
Id any //
|
||||
Name any // 规则名称
|
||||
EventType any // 事件类型
|
||||
ChannelCodes any // 渠道JSON数组
|
||||
UserIds any // 用户JSON数组
|
||||
TitleTemplate any // 标题模板
|
||||
BodyTemplate any // 内容模板
|
||||
Enabled any // 是否启用
|
||||
CreatedAt any //
|
||||
UpdatedAt any //
|
||||
}
|
||||
|
||||
// NoticeLog 通知发送记录 DO。
|
||||
type NoticeLog struct {
|
||||
g.Meta `orm:"table:notice_log, do:true"`
|
||||
Id any //
|
||||
RuleId any // 规则ID
|
||||
EventType any // 事件类型
|
||||
ChannelCode any // 渠道编码
|
||||
UserId any // 用户ID
|
||||
Target any // 发送目标
|
||||
Title any // 标题
|
||||
Body any // 内容
|
||||
Result any // 结果
|
||||
Error any // 错误
|
||||
CreatedAt any //
|
||||
}
|
||||
|
||||
// AutoJob 自动任务 DO。
|
||||
type AutoJob struct {
|
||||
g.Meta `orm:"table:auto_job, do:true"`
|
||||
Id any //
|
||||
Name any // 任务名称
|
||||
Code any // 任务编码
|
||||
JobType any // 任务类型
|
||||
CronExpr any // cron表达式
|
||||
Enabled any // 是否启用
|
||||
Remark any // 备注
|
||||
LastRunAt any // 上次运行
|
||||
LastResult any // 上次结果
|
||||
LastError any // 上次错误
|
||||
CreatedAt any //
|
||||
UpdatedAt any //
|
||||
}
|
||||
|
||||
// AutoJobLog 自动任务运行日志 DO。
|
||||
type AutoJobLog struct {
|
||||
g.Meta `orm:"table:auto_job_log, do:true"`
|
||||
Id any //
|
||||
JobId any // 任务ID
|
||||
RunAt any // 运行时间
|
||||
Result any // 结果
|
||||
Error any // 错误
|
||||
Summary any // 摘要
|
||||
DurationMs any // 耗时
|
||||
CreatedAt any //
|
||||
}
|
||||
@ -12,6 +12,8 @@ type AdminItem struct {
|
||||
Username string
|
||||
Nickname string
|
||||
Status int
|
||||
BarkDeviceId string
|
||||
PushplusToken string
|
||||
RoleIds []uint64
|
||||
RoleNames []string
|
||||
CreatedAt string
|
||||
@ -21,6 +23,8 @@ type AdminCreateInput struct {
|
||||
Username string
|
||||
Password string
|
||||
Nickname string
|
||||
BarkDeviceId string
|
||||
PushplusToken string
|
||||
RoleIds []uint64
|
||||
}
|
||||
|
||||
@ -28,6 +32,8 @@ type AdminUpdateInput struct {
|
||||
Id uint64
|
||||
Nickname string
|
||||
Status int
|
||||
BarkDeviceId string
|
||||
PushplusToken string
|
||||
RoleIds []uint64
|
||||
}
|
||||
|
||||
|
||||
43
internal/model/dto/job.go
Normal file
43
internal/model/dto/job.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Package dto 定义自动任务模块的服务边界对象。
|
||||
package dto
|
||||
|
||||
// JobVO 任务出参。
|
||||
type JobVO struct {
|
||||
Id uint64
|
||||
Name string
|
||||
Code string
|
||||
JobType string
|
||||
CronExpr string
|
||||
Enabled int
|
||||
Remark string
|
||||
LastRunAt string
|
||||
LastResult int
|
||||
LastError string
|
||||
}
|
||||
|
||||
// JobInput 任务更新入参(cron/启用/备注)。
|
||||
type JobInput struct {
|
||||
Id uint64
|
||||
CronExpr string
|
||||
Enabled int
|
||||
Remark string
|
||||
}
|
||||
|
||||
// JobLogVO 任务运行日志出参。
|
||||
type JobLogVO struct {
|
||||
Id uint64
|
||||
JobId uint64
|
||||
JobName string
|
||||
RunAt string
|
||||
Result int
|
||||
Error string
|
||||
Summary string
|
||||
DurationMs int
|
||||
}
|
||||
|
||||
// JobLogFilter 运行日志筛选。
|
||||
type JobLogFilter struct {
|
||||
Page int
|
||||
Size int
|
||||
JobId uint64
|
||||
}
|
||||
83
internal/model/dto/notice.go
Normal file
83
internal/model/dto/notice.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Package dto 定义通知模块的服务边界对象(入参/出参)。
|
||||
package dto
|
||||
|
||||
import "strings"
|
||||
|
||||
// NoticeChannelVO 渠道出参。
|
||||
type NoticeChannelVO struct {
|
||||
Id uint64
|
||||
Code string
|
||||
Name string
|
||||
Enabled int
|
||||
Config string
|
||||
Remark string
|
||||
}
|
||||
|
||||
// NoticeChannelInput 渠道入参。
|
||||
type NoticeChannelInput struct {
|
||||
Id uint64
|
||||
Code string
|
||||
Name string
|
||||
Enabled int
|
||||
Config string
|
||||
Remark string
|
||||
}
|
||||
|
||||
// NoticeRuleVO 规则出参。
|
||||
type NoticeRuleVO struct {
|
||||
Id uint64
|
||||
Name string
|
||||
EventType string
|
||||
ChannelCodes []string
|
||||
UserIds []uint64
|
||||
TitleTemplate string
|
||||
BodyTemplate string
|
||||
Enabled int
|
||||
}
|
||||
|
||||
// NoticeRuleInput 规则入参。
|
||||
type NoticeRuleInput struct {
|
||||
Id uint64
|
||||
Name string
|
||||
EventType string
|
||||
ChannelCodes []string
|
||||
UserIds []uint64
|
||||
TitleTemplate string
|
||||
BodyTemplate string
|
||||
Enabled int
|
||||
}
|
||||
|
||||
// NoticeLogVO 通知记录出参。
|
||||
type NoticeLogVO struct {
|
||||
Id uint64
|
||||
RuleId uint64
|
||||
EventType string
|
||||
ChannelCode string
|
||||
UserId uint64
|
||||
Target string
|
||||
Title string
|
||||
Body string
|
||||
Result int
|
||||
Error string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
// NoticeLogFilter 通知记录筛选。
|
||||
type NoticeLogFilter struct {
|
||||
Page int
|
||||
Size int
|
||||
EventType string
|
||||
ChannelCode string
|
||||
Result int // 0全部 1成功 2失败
|
||||
DateFrom string
|
||||
DateTo string
|
||||
}
|
||||
|
||||
// RenderTemplate 简单模板渲染:将 {{key}} 替换为 vars 中对应值。
|
||||
func RenderTemplate(tpl string, vars map[string]string) string {
|
||||
out := tpl
|
||||
for k, v := range vars {
|
||||
out = strings.ReplaceAll(out, "{{"+k+"}}", v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@ -15,6 +15,8 @@ type AdminUser struct {
|
||||
PasswordHash string `json:"passwordHash" orm:"password_hash" description:""` //
|
||||
Nickname string `json:"nickname" orm:"nickname" description:""` //
|
||||
Status int `json:"status" orm:"status" description:""` //
|
||||
BarkDeviceId string `json:"barkDeviceId" orm:"bark_device_id" description:"Bark设备ID"` //
|
||||
PushplusToken string `json:"pushplusToken" orm:"pushplus_token" description:"pushplus token"` //
|
||||
LastLoginAt *gtime.Time `json:"lastLoginAt" orm:"last_login_at" description:""` //
|
||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` //
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` //
|
||||
|
||||
71
internal/model/entity/notice.go
Normal file
71
internal/model/entity/notice.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Package entity 定义通知模块与自动任务模块的表结构(手写,未跑 gf gen)。
|
||||
package entity
|
||||
|
||||
// NoticeChannel 通知渠道配置。
|
||||
type NoticeChannel struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
Code string `json:"code" orm:"code" description:"渠道编码"`
|
||||
Name string `json:"name" orm:"name" description:"渠道名称"`
|
||||
Enabled int `json:"enabled" orm:"enabled" description:"是否启用"`
|
||||
Config string `json:"config" orm:"config" description:"渠道配置JSON"`
|
||||
Remark string `json:"remark" orm:"remark" description:"备注"`
|
||||
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// NoticeRule 通知规则(事件→渠道→人员→启用→模板)。
|
||||
type NoticeRule struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
Name string `json:"name" orm:"name" description:"规则名称"`
|
||||
EventType string `json:"eventType" orm:"event_type" description:"事件类型"`
|
||||
ChannelCodes string `json:"channelCodes" orm:"channel_codes" description:"渠道编码JSON数组"`
|
||||
UserIds string `json:"userIds" orm:"user_ids" description:"接收用户ID JSON数组"`
|
||||
TitleTemplate string `json:"titleTemplate" orm:"title_template" description:"标题模板"`
|
||||
BodyTemplate string `json:"bodyTemplate" orm:"body_template" description:"内容模板"`
|
||||
Enabled int `json:"enabled" orm:"enabled" description:"是否启用"`
|
||||
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// NoticeLog 通知发送记录。
|
||||
type NoticeLog struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
RuleId uint64 `json:"ruleId" orm:"rule_id" description:"规则ID"`
|
||||
EventType string `json:"eventType" orm:"event_type" description:"事件类型"`
|
||||
ChannelCode string `json:"channelCode" orm:"channel_code" description:"渠道编码"`
|
||||
UserId uint64 `json:"userId" orm:"user_id" description:"接收用户ID"`
|
||||
Target string `json:"target" orm:"target" description:"发送目标"`
|
||||
Title string `json:"title" orm:"title" description:"标题"`
|
||||
Body string `json:"body" orm:"body" description:"内容"`
|
||||
Result int `json:"result" orm:"result" description:"1成功 0失败"`
|
||||
Error string `json:"error" orm:"error" description:"错误信息"`
|
||||
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
// AutoJob 自动任务定义。
|
||||
type AutoJob struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
Name string `json:"name" orm:"name" description:"任务名称"`
|
||||
Code string `json:"code" orm:"code" description:"任务编码"`
|
||||
JobType string `json:"jobType" orm:"job_type" description:"任务类型"`
|
||||
CronExpr string `json:"cronExpr" orm:"cron_expr" description:"cron表达式"`
|
||||
Enabled int `json:"enabled" orm:"enabled" description:"是否启用"`
|
||||
Remark string `json:"remark" orm:"remark" description:"备注"`
|
||||
LastRunAt string `json:"lastRunAt" orm:"last_run_at" description:"上次运行时间"`
|
||||
LastResult int `json:"lastResult" orm:"last_result" description:"0未运行 1成功 2失败"`
|
||||
LastError string `json:"lastError" orm:"last_error" description:"上次错误"`
|
||||
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// AutoJobLog 自动任务运行日志。
|
||||
type AutoJobLog struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
JobId uint64 `json:"jobId" orm:"job_id" description:"任务ID"`
|
||||
RunAt string `json:"runAt" orm:"run_at" description:"运行时间"`
|
||||
Result int `json:"result" orm:"result" description:"1成功 2失败"`
|
||||
Error string `json:"error" orm:"error" description:"错误信息"`
|
||||
Summary string `json:"summary" orm:"summary" description:"运行摘要"`
|
||||
DurationMs int `json:"durationMs" orm:"duration_ms" description:"耗时毫秒"`
|
||||
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||
}
|
||||
@ -60,6 +60,7 @@ func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminIt
|
||||
}
|
||||
items = append(items, &dto.AdminItem{
|
||||
Id: a.Id, Username: a.Username, Nickname: a.Nickname, Status: a.Status,
|
||||
BarkDeviceId: a.BarkDeviceId, PushplusToken: a.PushplusToken,
|
||||
RoleIds: roleIds, RoleNames: roleNames,
|
||||
CreatedAt: a.CreatedAt.Layout("2006-01-02 15:04:05"),
|
||||
})
|
||||
@ -104,6 +105,7 @@ func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint
|
||||
}
|
||||
id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{
|
||||
Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1,
|
||||
BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "insert admin")
|
||||
@ -115,7 +117,8 @@ func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint
|
||||
}
|
||||
|
||||
func (s *adminManage) Update(ctx context.Context, in dto.AdminUpdateInput) error {
|
||||
data := do.AdminUser{Nickname: in.Nickname, Status: in.Status}
|
||||
data := do.AdminUser{Nickname: in.Nickname, Status: in.Status,
|
||||
BarkDeviceId: in.BarkDeviceId, PushplusToken: in.PushplusToken}
|
||||
if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: in.Id}).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "update admin")
|
||||
}
|
||||
|
||||
232
internal/service/job/job.go
Normal file
232
internal/service/job/job.go
Normal file
@ -0,0 +1,232 @@
|
||||
// Package job 提供自动任务调度服务:DB 驱动的 gcron 管理 + 运行记录 + 完成/失败通知。
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcron"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"service.xpcool.com/internal/consts"
|
||||
"service.xpcool.com/internal/dao"
|
||||
"service.xpcool.com/internal/library/response"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
"service.xpcool.com/internal/model/entity"
|
||||
"service.xpcool.com/internal/service/notice"
|
||||
)
|
||||
|
||||
// TaskFunc 任务执行函数:返回运行摘要与错误。
|
||||
type TaskFunc func(context.Context) (string, error)
|
||||
|
||||
// IJob 自动任务服务接口。
|
||||
type IJob interface {
|
||||
List(context.Context) ([]dto.JobVO, error)
|
||||
Save(context.Context, dto.JobInput) error
|
||||
Trigger(context.Context, uint64) (string, bool, error)
|
||||
LogList(context.Context, dto.JobLogFilter) ([]dto.JobLogVO, int, error)
|
||||
Register(string, TaskFunc)
|
||||
StartScheduler(context.Context)
|
||||
}
|
||||
|
||||
type job struct {
|
||||
mu sync.Mutex
|
||||
registry map[string]TaskFunc // code -> 业务函数
|
||||
}
|
||||
|
||||
var localJob IJob
|
||||
|
||||
// New 创建自动任务服务实现。
|
||||
func New() IJob {
|
||||
return &job{registry: make(map[string]TaskFunc)}
|
||||
}
|
||||
|
||||
// Job 返回已注册的自动任务服务实现。
|
||||
func Job() IJob {
|
||||
if localJob == nil {
|
||||
panic("Job implementation not registered")
|
||||
}
|
||||
return localJob
|
||||
}
|
||||
|
||||
// RegisterJob 注册自动任务服务实现。
|
||||
func RegisterJob(i IJob) { localJob = i }
|
||||
|
||||
// Register 注册任务执行函数(按 code)。
|
||||
func (s *job) Register(code string, fn TaskFunc) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.registry[code] = fn
|
||||
}
|
||||
|
||||
// ---------------- 列表 / 保存 / 触发 / 日志 ----------------
|
||||
|
||||
func (s *job) List(ctx context.Context) ([]dto.JobVO, error) {
|
||||
var list []entity.AutoJob
|
||||
if err := dao.AutoJob.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil {
|
||||
return nil, gerror.Wrap(err, "query auto jobs")
|
||||
}
|
||||
out := make([]dto.JobVO, 0, len(list))
|
||||
for _, v := range list {
|
||||
out = append(out, dto.JobVO{
|
||||
Id: v.Id, Name: v.Name, Code: v.Code, JobType: v.JobType, CronExpr: v.CronExpr,
|
||||
Enabled: v.Enabled, Remark: v.Remark, LastRunAt: v.LastRunAt,
|
||||
LastResult: v.LastResult, LastError: v.LastError,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *job) Save(ctx context.Context, in dto.JobInput) error {
|
||||
var j entity.AutoJob
|
||||
if err := dao.AutoJob.Ctx(ctx).Where("id", in.Id).Scan(&j); err != nil || j.Id == 0 {
|
||||
return response.Error(consts.CodeInvalidParam, "任务不存在")
|
||||
}
|
||||
data := map[string]interface{}{"remark": in.Remark}
|
||||
if in.CronExpr != "" {
|
||||
data["cron_expr"] = in.CronExpr
|
||||
}
|
||||
if in.Enabled >= 0 {
|
||||
data["enabled"] = in.Enabled
|
||||
}
|
||||
if _, err := dao.AutoJob.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "update auto job")
|
||||
}
|
||||
// 动态重启调度:移除旧 cron,按新配置重新注册
|
||||
s.reschedule(ctx, j.Code)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *job) Trigger(ctx context.Context, id uint64) (string, bool, error) {
|
||||
var j entity.AutoJob
|
||||
if err := dao.AutoJob.Ctx(ctx).Where("id", id).Scan(&j); err != nil || j.Id == 0 {
|
||||
return "", false, response.Error(consts.CodeInvalidParam, "任务不存在")
|
||||
}
|
||||
summary, err := s.run(ctx, j)
|
||||
return summary, err == nil, err
|
||||
}
|
||||
|
||||
func (s *job) LogList(ctx context.Context, f dto.JobLogFilter) ([]dto.JobLogVO, int, error) {
|
||||
m := dao.AutoJobLog.Ctx(ctx)
|
||||
if f.JobId > 0 {
|
||||
m = m.Where("job_id", f.JobId)
|
||||
}
|
||||
total, err := m.Clone().Count()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "count job log")
|
||||
}
|
||||
var list []entity.AutoJobLog
|
||||
if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "query job log")
|
||||
}
|
||||
out := make([]dto.JobLogVO, 0, len(list))
|
||||
for _, v := range list {
|
||||
jobName := ""
|
||||
if f.JobId == 0 {
|
||||
var j entity.AutoJob
|
||||
_ = dao.AutoJob.Ctx(ctx).Where("id", v.JobId).Scan(&j)
|
||||
jobName = j.Name
|
||||
}
|
||||
out = append(out, dto.JobLogVO{
|
||||
Id: v.Id, JobId: v.JobId, JobName: jobName, RunAt: v.RunAt,
|
||||
Result: v.Result, Error: v.Error, Summary: v.Summary, DurationMs: v.DurationMs,
|
||||
})
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// ---------------- 调度 ----------------
|
||||
|
||||
// StartScheduler 启动调度:加载 DB 中启用的任务并注册 gcron。
|
||||
func (s *job) StartScheduler(ctx context.Context) {
|
||||
var jobs []entity.AutoJob
|
||||
if err := dao.AutoJob.Ctx(ctx).Where("enabled", 1).Scan(&jobs); err != nil {
|
||||
g.Log().Errorf(ctx, "load auto jobs failed: %v", err)
|
||||
return
|
||||
}
|
||||
for _, j := range jobs {
|
||||
s.addCron(ctx, j)
|
||||
}
|
||||
g.Log().Infof(ctx, "auto job scheduler started: %d jobs", len(jobs))
|
||||
}
|
||||
|
||||
// addCron 为任务注册 gcron(不存在的函数跳过)。
|
||||
func (s *job) addCron(ctx context.Context, j entity.AutoJob) {
|
||||
s.mu.Lock()
|
||||
_, ok := s.registry[j.Code]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
g.Log().Warningf(ctx, "job code %s has no handler, skip cron", j.Code)
|
||||
return
|
||||
}
|
||||
if _, err := gcron.Add(ctx, j.CronExpr, func(c context.Context) {
|
||||
_, _ = s.run(c, j)
|
||||
}, j.Code); err != nil {
|
||||
g.Log().Errorf(ctx, "add cron %s(%s) failed: %v", j.Code, j.CronExpr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// reschedule 按 DB 最新状态重启任务的调度(停用则移除 cron)。
|
||||
func (s *job) reschedule(ctx context.Context, code string) {
|
||||
gcron.Remove(code)
|
||||
var j entity.AutoJob
|
||||
if err := dao.AutoJob.Ctx(ctx).Where("code", code).Scan(&j); err != nil || j.Id == 0 {
|
||||
return
|
||||
}
|
||||
if j.Enabled == 1 {
|
||||
s.addCron(ctx, j)
|
||||
}
|
||||
}
|
||||
|
||||
// run 执行任务:调用注册函数 → 更新任务状态 → 写运行日志 → 通知完成/失败。
|
||||
func (s *job) run(ctx context.Context, j entity.AutoJob) (string, error) {
|
||||
s.mu.Lock()
|
||||
fn, ok := s.registry[j.Code]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
err := fmt.Errorf("no handler for job %s", j.Code)
|
||||
s.finish(ctx, j, 2, err.Error(), "", 0)
|
||||
return "", err
|
||||
}
|
||||
start := time.Now()
|
||||
summary, err := fn(ctx)
|
||||
duration := int(time.Since(start).Milliseconds())
|
||||
if err != nil {
|
||||
s.finish(ctx, j, 2, err.Error(), summary, duration)
|
||||
return summary, err
|
||||
}
|
||||
s.finish(ctx, j, 1, "", summary, duration)
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// finish 更新任务状态 + 写运行日志 + 发通知。
|
||||
func (s *job) finish(ctx context.Context, j entity.AutoJob, result int, errMsg, summary string, duration int) {
|
||||
// 更新任务状态
|
||||
if _, err := dao.AutoJob.Ctx(ctx).Where("id", j.Id).Data(map[string]interface{}{
|
||||
"last_run_at": gtime.Now().Format("Y-m-d H:i:s"),
|
||||
"last_result": result, "last_error": errMsg,
|
||||
}).Update(); err != nil {
|
||||
g.Log().Errorf(ctx, "update job %s status failed: %v", j.Code, err)
|
||||
}
|
||||
// 写运行日志
|
||||
if _, err := dao.AutoJobLog.Ctx(ctx).Data(map[string]interface{}{
|
||||
"job_id": j.Id, "result": result, "error": errMsg,
|
||||
"summary": summary, "duration_ms": duration,
|
||||
}).Insert(); err != nil {
|
||||
g.Log().Errorf(ctx, "insert job log failed: %v", err)
|
||||
}
|
||||
// 通知:完成/失败
|
||||
vars := map[string]string{"jobName": j.Name, "summary": summary, "error": errMsg}
|
||||
if result == 1 {
|
||||
if summary == "" {
|
||||
summary = "执行完成"
|
||||
}
|
||||
_ = notice.Notice().Send(ctx, "job_done", vars)
|
||||
} else {
|
||||
_ = notice.Notice().Send(ctx, "job_fail", vars)
|
||||
}
|
||||
}
|
||||
352
internal/service/notice/notice.go
Normal file
352
internal/service/notice/notice.go
Normal file
@ -0,0 +1,352 @@
|
||||
// Package notice 提供统一通知服务(渠道/规则/发送/日志)。
|
||||
// 发送实现:Bark 走路径式 key(POST {baseUrl}/{deviceKey} JSON);pushplus 走官方 send 接口。
|
||||
package notice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
"service.xpcool.com/internal/consts"
|
||||
"service.xpcool.com/internal/dao"
|
||||
"service.xpcool.com/internal/library/response"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
"service.xpcool.com/internal/model/entity"
|
||||
)
|
||||
|
||||
// INotice 统一通知服务接口。
|
||||
type INotice interface {
|
||||
ChannelList(context.Context) ([]dto.NoticeChannelVO, error)
|
||||
ChannelSave(context.Context, dto.NoticeChannelInput) (uint64, error)
|
||||
RuleList(context.Context, string) ([]dto.NoticeRuleVO, error)
|
||||
RuleSave(context.Context, dto.NoticeRuleInput) (uint64, error)
|
||||
RuleDelete(context.Context, uint64) error
|
||||
LogList(context.Context, dto.NoticeLogFilter) ([]dto.NoticeLogVO, int, error)
|
||||
Send(context.Context, string, map[string]string) error // 按启用规则发送通知(事件→渠道→人员)
|
||||
Test(context.Context, uint64, string, string, []uint64, string, string) (bool, string, error)
|
||||
}
|
||||
|
||||
type notice struct{}
|
||||
|
||||
var localNotice INotice
|
||||
|
||||
// New 创建通知服务实现。
|
||||
func New() INotice { return ¬ice{} }
|
||||
|
||||
// Notice 返回已注册的通知服务实现。
|
||||
func Notice() INotice {
|
||||
if localNotice == nil {
|
||||
panic("Notice implementation not registered")
|
||||
}
|
||||
return localNotice
|
||||
}
|
||||
|
||||
// RegisterNotice 注册通知服务实现。
|
||||
func RegisterNotice(i INotice) { localNotice = i }
|
||||
|
||||
// ---------------- 渠道 ----------------
|
||||
|
||||
func (s *notice) ChannelList(ctx context.Context) ([]dto.NoticeChannelVO, error) {
|
||||
var list []entity.NoticeChannel
|
||||
if err := dao.NoticeChannel.Ctx(ctx).OrderAsc("id").Scan(&list); err != nil {
|
||||
return nil, gerror.Wrap(err, "query notice channels")
|
||||
}
|
||||
out := make([]dto.NoticeChannelVO, 0, len(list))
|
||||
for _, v := range list {
|
||||
out = append(out, dto.NoticeChannelVO{Id: v.Id, Code: v.Code, Name: v.Name, Enabled: v.Enabled, Config: v.Config, Remark: v.Remark})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *notice) ChannelSave(ctx context.Context, in dto.NoticeChannelInput) (uint64, error) {
|
||||
data := map[string]interface{}{
|
||||
"code": in.Code, "name": in.Name, "enabled": in.Enabled,
|
||||
"config": in.Config, "remark": in.Remark,
|
||||
}
|
||||
if in.Id > 0 {
|
||||
if _, err := dao.NoticeChannel.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
|
||||
return 0, gerror.Wrap(err, "update notice channel")
|
||||
}
|
||||
return in.Id, nil
|
||||
}
|
||||
id, err := dao.NoticeChannel.Ctx(ctx).Data(data).InsertAndGetId()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "insert notice channel")
|
||||
}
|
||||
return uint64(id), nil
|
||||
}
|
||||
|
||||
// ---------------- 规则 ----------------
|
||||
|
||||
func (s *notice) RuleList(ctx context.Context, eventType string) ([]dto.NoticeRuleVO, error) {
|
||||
m := dao.NoticeRule.Ctx(ctx)
|
||||
if eventType != "" {
|
||||
m = m.Where("event_type", eventType)
|
||||
}
|
||||
var list []entity.NoticeRule
|
||||
if err := m.OrderAsc("id").Scan(&list); err != nil {
|
||||
return nil, gerror.Wrap(err, "query notice rules")
|
||||
}
|
||||
out := make([]dto.NoticeRuleVO, 0, len(list))
|
||||
for _, v := range list {
|
||||
out = append(out, dto.NoticeRuleVO{
|
||||
Id: v.Id, Name: v.Name, EventType: v.EventType,
|
||||
ChannelCodes: parseStrArray(v.ChannelCodes), UserIds: parseUintArray(v.UserIds),
|
||||
TitleTemplate: v.TitleTemplate, BodyTemplate: v.BodyTemplate, Enabled: v.Enabled,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *notice) RuleSave(ctx context.Context, in dto.NoticeRuleInput) (uint64, error) {
|
||||
data := map[string]interface{}{
|
||||
"name": in.Name, "event_type": in.EventType,
|
||||
"channel_codes": toJSON(in.ChannelCodes), "user_ids": toJSON(in.UserIds),
|
||||
"title_template": in.TitleTemplate, "body_template": in.BodyTemplate, "enabled": in.Enabled,
|
||||
}
|
||||
if in.Id > 0 {
|
||||
if _, err := dao.NoticeRule.Ctx(ctx).Where("id", in.Id).Data(data).Update(); err != nil {
|
||||
return 0, gerror.Wrap(err, "update notice rule")
|
||||
}
|
||||
return in.Id, nil
|
||||
}
|
||||
id, err := dao.NoticeRule.Ctx(ctx).Data(data).InsertAndGetId()
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "insert notice rule")
|
||||
}
|
||||
return uint64(id), nil
|
||||
}
|
||||
|
||||
func (s *notice) RuleDelete(ctx context.Context, id uint64) error {
|
||||
if _, err := dao.NoticeRule.Ctx(ctx).Where("id", id).Delete(); err != nil {
|
||||
return gerror.Wrap(err, "delete notice rule")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------- 日志 ----------------
|
||||
|
||||
func (s *notice) LogList(ctx context.Context, f dto.NoticeLogFilter) ([]dto.NoticeLogVO, int, error) {
|
||||
m := dao.NoticeLog.Ctx(ctx)
|
||||
if f.EventType != "" {
|
||||
m = m.Where("event_type", f.EventType)
|
||||
}
|
||||
if f.ChannelCode != "" {
|
||||
m = m.Where("channel_code", f.ChannelCode)
|
||||
}
|
||||
if f.Result > 0 {
|
||||
m = m.Where("result", f.Result)
|
||||
}
|
||||
if f.DateFrom != "" {
|
||||
m = m.WhereGTE("created_at", f.DateFrom)
|
||||
}
|
||||
if f.DateTo != "" {
|
||||
m = m.WhereLTE("created_at", f.DateTo)
|
||||
}
|
||||
total, err := m.Clone().Count()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "count notice log")
|
||||
}
|
||||
var list []entity.NoticeLog
|
||||
if err = m.Page(f.Page, f.Size).OrderDesc("id").Scan(&list); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "query notice log")
|
||||
}
|
||||
out := make([]dto.NoticeLogVO, 0, len(list))
|
||||
for _, v := range list {
|
||||
out = append(out, dto.NoticeLogVO{
|
||||
Id: v.Id, RuleId: v.RuleId, EventType: v.EventType, ChannelCode: v.ChannelCode,
|
||||
UserId: v.UserId, Target: v.Target, Title: v.Title, Body: v.Body,
|
||||
Result: v.Result, Error: v.Error, CreatedAt: v.CreatedAt,
|
||||
})
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
// ---------------- 发送 ----------------
|
||||
|
||||
// Send 按事件类型查启用规则,向每个渠道的每个接收用户发送,并记录日志。
|
||||
func (s *notice) Send(ctx context.Context, eventType string, vars map[string]string) error {
|
||||
var rules []entity.NoticeRule
|
||||
if err := dao.NoticeRule.Ctx(ctx).Where("event_type", eventType).Where("enabled", 1).Scan(&rules); err != nil {
|
||||
return gerror.Wrap(err, "query notice rules for send")
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return nil // 未配置规则 = 静默
|
||||
}
|
||||
for _, r := range rules {
|
||||
channels := parseStrArray(r.ChannelCodes)
|
||||
userIds := parseUintArray(r.UserIds)
|
||||
title := dto.RenderTemplate(r.TitleTemplate, vars)
|
||||
body := dto.RenderTemplate(r.BodyTemplate, vars)
|
||||
for _, channel := range channels {
|
||||
for _, uid := range userIds {
|
||||
s.deliver(ctx, r.Id, eventType, channel, uid, title, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deliver 单条投递:取用户渠道凭据 → 发送 → 记日志。
|
||||
func (s *notice) deliver(ctx context.Context, ruleId uint64, eventType, channel string, userId uint64, title, body string) {
|
||||
var user entity.AdminUser
|
||||
_ = dao.AdminUser.Ctx(ctx).Where("id", userId).Scan(&user)
|
||||
var target string
|
||||
switch channel {
|
||||
case "bark":
|
||||
target = user.BarkDeviceId
|
||||
case "pushplus":
|
||||
target = user.PushplusToken
|
||||
default:
|
||||
target = ""
|
||||
}
|
||||
ok, errMsg := s.sendByChannel(ctx, channel, target, title, body)
|
||||
result := 0
|
||||
if ok {
|
||||
result = 1
|
||||
}
|
||||
_, _ = dao.NoticeLog.Ctx(ctx).Data(map[string]interface{}{
|
||||
"rule_id": ruleId, "event_type": eventType, "channel_code": channel,
|
||||
"user_id": userId, "target": target, "title": title, "body": body,
|
||||
"result": result, "error": errMsg,
|
||||
}).Insert()
|
||||
}
|
||||
|
||||
// sendByChannel 按渠道发送单条;返回 (是否成功, 错误信息)。
|
||||
func (s *notice) sendByChannel(ctx context.Context, channel, target, title, body string) (bool, string) {
|
||||
if target == "" {
|
||||
return false, "目标凭据为空(用户未配置)"
|
||||
}
|
||||
client := g.Client().SetTimeout(10 * time.Second)
|
||||
switch channel {
|
||||
case "bark":
|
||||
// 渠道配置取 baseUrl(JSON config.baseUrl),默认容器网络内 bark:8080
|
||||
baseURL := "http://bark:8080"
|
||||
var ch entity.NoticeChannel
|
||||
if err := dao.NoticeChannel.Ctx(ctx).Where("code", "bark").Scan(&ch); err == nil && ch.Config != "" {
|
||||
var cfg struct {
|
||||
BaseURL string `json:"baseUrl"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(ch.Config), &cfg)
|
||||
if cfg.BaseURL != "" {
|
||||
baseURL = cfg.BaseURL
|
||||
}
|
||||
}
|
||||
// 路径式 key:POST {baseUrl}/{deviceKey},body JSON {title, body}
|
||||
url := strings.TrimRight(baseURL, "/") + "/" + target
|
||||
resp, err := client.Post(ctx, url, map[string]string{"title": title, "body": body})
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return false, "bark http " + resp.Status
|
||||
}
|
||||
var out struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
_ = json.Unmarshal(resp.ReadAll(), &out)
|
||||
if out.Code != 200 {
|
||||
return false, "bark code " + resp.ReadAllString()
|
||||
}
|
||||
return true, ""
|
||||
case "pushplus":
|
||||
// 官方接口:POST https://www.pushplus.plus/send {token,title,content}
|
||||
resp, err := client.Post(ctx, "https://www.pushplus.plus/send", map[string]string{
|
||||
"token": target, "title": title, "content": body,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return false, "pushplus http " + resp.Status
|
||||
}
|
||||
var out struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
_ = json.Unmarshal(resp.ReadAll(), &out)
|
||||
if out.Code != 200 {
|
||||
return false, "pushplus code " + resp.ReadAllString()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
return false, "未知渠道: " + channel
|
||||
}
|
||||
|
||||
// Test 测试发送:优先按规则;否则手动指定渠道+目标。
|
||||
func (s *notice) Test(ctx context.Context, ruleId uint64, channelCode, target string, userIds []uint64, title, body string) (bool, string, error) {
|
||||
if ruleId > 0 {
|
||||
var r entity.NoticeRule
|
||||
if err := dao.NoticeRule.Ctx(ctx).Where("id", ruleId).Scan(&r); err != nil || r.Id == 0 {
|
||||
return false, "", response.Error(consts.CodeInvalidParam, "规则不存在")
|
||||
}
|
||||
for _, ch := range parseStrArray(r.ChannelCodes) {
|
||||
for _, uid := range parseUintArray(r.UserIds) {
|
||||
s.deliver(ctx, r.Id, r.EventType, ch, uid, title, body)
|
||||
}
|
||||
}
|
||||
return true, "已按规则投递", nil
|
||||
}
|
||||
// 手动:渠道+目标(target 为空则按用户)
|
||||
if channelCode == "" {
|
||||
return false, "", response.Error(consts.CodeInvalidParam, "请指定渠道")
|
||||
}
|
||||
if target == "" && len(userIds) > 0 {
|
||||
var user entity.AdminUser
|
||||
_ = dao.AdminUser.Ctx(ctx).Where("id", userIds[0]).Scan(&user)
|
||||
if channelCode == "bark" {
|
||||
target = user.BarkDeviceId
|
||||
} else if channelCode == "pushplus" {
|
||||
target = user.PushplusToken
|
||||
}
|
||||
}
|
||||
if target == "" {
|
||||
return false, "", response.Error(consts.CodeInvalidParam, "目标凭据为空")
|
||||
}
|
||||
ok, msg := s.sendByChannel(ctx, channelCode, target, title, body)
|
||||
_, _ = dao.NoticeLog.Ctx(ctx).Data(map[string]interface{}{
|
||||
"rule_id": 0, "event_type": "test", "channel_code": channelCode,
|
||||
"user_id": 0, "target": target, "title": title, "body": body,
|
||||
"result": boolToInt(ok), "error": msg,
|
||||
}).Insert()
|
||||
return ok, msg, nil
|
||||
}
|
||||
|
||||
// ---------------- 工具 ----------------
|
||||
|
||||
func parseStrArray(s string) []string {
|
||||
var out []string
|
||||
_ = json.Unmarshal([]byte(s), &out)
|
||||
if out == nil {
|
||||
out = []string{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseUintArray(s string) []uint64 {
|
||||
var out []uint64
|
||||
_ = json.Unmarshal([]byte(s), &out)
|
||||
if out == nil {
|
||||
out = []uint64{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toJSON(v interface{}) string {
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// NowStr 当前时间字符串(供日志)。
|
||||
func NowStr() string { return gtime.Now().Format("Y-m-d H:i:s") }
|
||||
@ -3,27 +3,24 @@ package recruitment
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcron"
|
||||
"service.xpcool.com/internal/service/job"
|
||||
)
|
||||
|
||||
// StartScheduler 注册定时任务:每日 03:00 增量抓取全部启用源,08:00 推送招聘早报。
|
||||
// 调度器随 service 进程常驻,不额外引入系统 crontab。
|
||||
// 注意:gcron 表达式为 6 段式(秒 分 时 日 月 周),5 段标准 cron 会注册失败。
|
||||
func StartScheduler(ctx context.Context) {
|
||||
if _, err := gcron.Add(ctx, "0 0 3 * * *", func(c context.Context) {
|
||||
_, summary, e := Recruitment().Trigger(c, 0, false)
|
||||
// RegisterTasks 把招聘模块的定时任务注册到自动任务调度器(由 job 模块统一纳管)。
|
||||
// 原 StartScheduler 直接注册 gcron 的方式已废弃:任务定义存 auto_job 表(016 SQL 种子),
|
||||
// 由 job.StartScheduler 按 DB 配置统一调度,支持启停/改 cron/运行日志/完成通知。
|
||||
func RegisterTasks() {
|
||||
// 每日 03:00 增量抓取全部启用源
|
||||
job.Job().Register("recruit-crawl-daily", func(ctx context.Context) (string, error) {
|
||||
_, summary, e := Recruitment().Trigger(ctx, 0, false)
|
||||
if e != nil {
|
||||
g.Log().Errorf(c, "recruit daily crawl failed: %v", e)
|
||||
} else {
|
||||
g.Log().Infof(c, "recruit daily crawl done: %s", summary)
|
||||
}
|
||||
}, "recruit-crawl-daily"); err != nil {
|
||||
g.Log().Errorf(ctx, "add recruit crawl cron failed: %v", err)
|
||||
}
|
||||
if _, err := gcron.Add(ctx, "0 0 8 * * *", func(c context.Context) {
|
||||
sendDailyDigest(c)
|
||||
}, "recruit-push-daily"); err != nil {
|
||||
g.Log().Errorf(ctx, "add recruit push cron failed: %v", err)
|
||||
return summary, e
|
||||
}
|
||||
return summary, nil
|
||||
})
|
||||
// 每日 08:00 推送招聘早报(订阅列表)
|
||||
job.Job().Register("recruit-push-daily", func(ctx context.Context) (string, error) {
|
||||
sendDailyDigest(ctx)
|
||||
return "招聘早报推送完成", nil
|
||||
})
|
||||
}
|
||||
|
||||
152
manifest/sql/016_user_notice_job.sql
Normal file
152
manifest/sql/016_user_notice_job.sql
Normal file
@ -0,0 +1,152 @@
|
||||
-- 016_user_notice_job.sql
|
||||
-- 用户管理扩展 + 通知模块 + 自动任务模块(建表 + 菜单 + 种子)。
|
||||
-- 幂等:ALTER TABLE ADD COLUMN 用 IF NOT EXISTS(MySQL 8 不支持 COLUMN IF NOT EXISTS,用存储过程判断);
|
||||
-- 菜单用 INSERT ... ON DUPLICATE KEY UPDATE;种子用 INSERT IGNORE。
|
||||
-- 依赖:service 主库;admin_menu/admin_role_menu/admin_user 已存在。
|
||||
|
||||
-- ============ 1) 用户表扩展(Bark 设备ID / pushplus token) ============
|
||||
SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='admin_user' AND COLUMN_NAME='bark_device_id');
|
||||
SET @sql := IF(@has_col=0, 'ALTER TABLE admin_user ADD COLUMN bark_device_id VARCHAR(200) NOT NULL DEFAULT '''' COMMENT ''Bark设备ID'' AFTER status', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='admin_user' AND COLUMN_NAME='pushplus_token');
|
||||
SET @sql := IF(@has_col=0, 'ALTER TABLE admin_user ADD COLUMN pushplus_token VARCHAR(200) NOT NULL DEFAULT '''' COMMENT ''pushplus token'' AFTER bark_device_id', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ============ 2) 通知渠道表 ============
|
||||
CREATE TABLE IF NOT EXISTS `notice_channel` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '渠道编码 bark/pushplus/webhook',
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '渠道名称',
|
||||
`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用 0否 1是',
|
||||
`config` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '渠道配置JSON(如 bark 的 baseUrl)',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_code` (`code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知渠道配置';
|
||||
|
||||
-- ============ 3) 通知规则表 ============
|
||||
CREATE TABLE IF NOT EXISTS `notice_rule` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '规则名称',
|
||||
`event_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '事件类型 job_done/job_fail/security_alert/recruit_daily',
|
||||
`channel_codes` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '渠道编码 JSON 数组 ["bark","pushplus"]',
|
||||
`user_ids` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '接收用户ID JSON 数组 [1,2]',
|
||||
`title_template` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '标题模板(支持 {var} 占位符)',
|
||||
`body_template` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '内容模板',
|
||||
`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用 0否 1是',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_event` (`event_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知规则(事件→渠道→人员→启用)';
|
||||
|
||||
-- ============ 4) 通知发送记录 ============
|
||||
CREATE TABLE IF NOT EXISTS `notice_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`rule_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '规则ID',
|
||||
`event_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '事件类型',
|
||||
`channel_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '渠道编码',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '接收用户ID',
|
||||
`target` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '发送目标(设备key/token)',
|
||||
`title` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '标题',
|
||||
`body` VARCHAR(2000) NOT NULL DEFAULT '' COMMENT '内容',
|
||||
`result` TINYINT NOT NULL DEFAULT 0 COMMENT '1成功 0失败',
|
||||
`error` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '错误信息',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_event` (`event_type`),
|
||||
KEY `idx_created` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知发送记录';
|
||||
|
||||
-- ============ 5) 自动任务定义 ============
|
||||
CREATE TABLE IF NOT EXISTS `auto_job` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '任务名称',
|
||||
`code` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '任务编码(唯一,映射业务函数)',
|
||||
`job_type` VARCHAR(32) NOT NULL DEFAULT 'gcron' COMMENT '任务类型',
|
||||
`cron_expr` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'cron 表达式(6段: 秒 分 时 日 月 周)',
|
||||
`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用 0否 1是',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`last_run_at` DATETIME NULL DEFAULT NULL COMMENT '上次运行时间',
|
||||
`last_result` TINYINT NOT NULL DEFAULT 0 COMMENT '0未运行 1成功 2失败',
|
||||
`last_error` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '上次错误',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_code` (`code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自动任务定义';
|
||||
|
||||
-- ============ 6) 自动任务运行日志 ============
|
||||
CREATE TABLE IF NOT EXISTS `auto_job_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`job_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '任务ID',
|
||||
`run_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '运行时间',
|
||||
`result` TINYINT NOT NULL DEFAULT 0 COMMENT '1成功 2失败',
|
||||
`error` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '错误信息',
|
||||
`summary` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '运行摘要',
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT '耗时(毫秒)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_job` (`job_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自动任务运行日志';
|
||||
|
||||
-- ============ 7) 种子:通知渠道 ============
|
||||
INSERT INTO notice_channel (id, code, name, enabled, config, remark) VALUES
|
||||
(1, 'bark', 'Bark(iOS)', 1, '{"baseUrl":"http://bark:8080"}', 'Bark 自建服务,容器网络内访问 bark:8080'),
|
||||
(2, 'pushplus', 'pushplus', 1, '{}', '微信推送,token 在用户资料中维护')
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), enabled=VALUES(enabled), config=VALUES(config), remark=VALUES(remark);
|
||||
|
||||
-- ============ 8) 种子:通知规则 ============
|
||||
INSERT INTO notice_rule (id, name, event_type, channel_codes, user_ids, title_template, body_template, enabled) VALUES
|
||||
(1, '安全告警', 'security_alert', '["bark"]', '[1]', '服务器安全告警', '{{summary}}', 1),
|
||||
(2, '任务完成', 'job_done', '["bark"]', '[1]', '任务完成: {{jobName}}', '{{summary}}', 1),
|
||||
(3, '任务失败', 'job_fail', '["bark","pushplus"]', '[1]', '⚠️ 任务失败: {{jobName}}', '{{summary}}', 1)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), event_type=VALUES(event_type), channel_codes=VALUES(channel_codes),
|
||||
user_ids=VALUES(user_ids), title_template=VALUES(title_template), body_template=VALUES(body_template), enabled=VALUES(enabled);
|
||||
|
||||
-- ============ 9) 种子:自动任务(现有 gcron 任务纳管) ============
|
||||
INSERT INTO auto_job (id, name, code, job_type, cron_expr, enabled, remark) VALUES
|
||||
(1, '招聘公告每日抓取', 'recruit-crawl-daily', 'gcron', '0 0 3 * * *', 1, '每日03:00 增量抓取招聘公告'),
|
||||
(2, '招聘公告每日推送', 'recruit-push-daily', 'gcron', '0 0 8 * * *', 1, '每日08:00 推送订阅公告(按 push_subscription 配置)')
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), cron_expr=VALUES(cron_expr), enabled=VALUES(enabled), remark=VALUES(remark);
|
||||
|
||||
-- ============ 10) 菜单:人员管理改名 → 用户管理 ============
|
||||
UPDATE admin_menu SET name='用户管理' WHERE name='人员管理' AND type=1;
|
||||
-- 补充兜底:若没有名为人员管理的菜单(如叫管理员管理),把 component='system/admin/index' 的改名为用户管理
|
||||
UPDATE admin_menu SET name='用户管理' WHERE component='system/admin/index' AND type=1 AND name != '用户管理';
|
||||
|
||||
-- ============ 11) 菜单:通知中心 + 自动任务 ============
|
||||
INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES
|
||||
(98, 0, '通知中心', 'mdi:bell-ring-outline', 1, '/notice', '', 'notice:center', 8, 1, 0, NOW(), NOW()),
|
||||
(980, 98, '通知规则', 'mdi:bell-cog-outline', 1, 'rule', 'system/notice/rule/index', 'notice:rule', 1, 1, 0, NOW(), NOW()),
|
||||
(981, 98, '通知渠道', 'mdi:router-wireless', 1, 'channel', 'system/notice/channel/index', 'notice:channel', 2, 1, 0, NOW(), NOW()),
|
||||
(982, 98, '通知日志', 'mdi:history', 1, 'log', 'system/notice/log/index', 'notice:log', 3, 1, 0, NOW(), NOW()),
|
||||
(99, 0, '自动任务', 'mdi:clock-outline', 1, '/auto-job','', 'job:center', 9, 1, 0, NOW(), NOW()),
|
||||
(990, 99, '任务管理', 'mdi:calendar-clock', 1, 'list', 'system/auto-job/list/index', 'job:list', 1, 1, 0, NOW(), NOW()),
|
||||
(991, 99, '任务日志', 'mdi:file-clock-outline', 1, 'log', 'system/auto-job/log/index', 'job:log', 2, 1, 0, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id), name=VALUES(name), icon=VALUES(icon), type=VALUES(type),
|
||||
path=VALUES(path), component=VALUES(component), permission=VALUES(permission), sort=VALUES(sort),
|
||||
status=VALUES(status), hidden=VALUES(hidden), deleted_at=NULL;
|
||||
|
||||
-- ============ 12) type=2 API 权限 ============
|
||||
INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES
|
||||
(9801, 980, '查询', '', 2, 'POST /api/service/admin/notice/rule/list', '', 'notice:rule:list', 1, 1, 0, NOW(), NOW()),
|
||||
(9802, 980, '保存', '', 2, 'POST /api/service/admin/notice/rule/save', '', 'notice:rule:save', 2, 1, 0, NOW(), NOW()),
|
||||
(9803, 980, '删除', '', 2, 'POST /api/service/admin/notice/rule/delete', '', 'notice:rule:delete', 3, 1, 0, NOW(), NOW()),
|
||||
(9804, 980, '测试', '', 2, 'POST /api/service/admin/notice/test', '', 'notice:test', 4, 1, 0, NOW(), NOW()),
|
||||
(9811, 981, '查询', '', 2, 'POST /api/service/admin/notice/channel/list', '', 'notice:channel:list', 1, 1, 0, NOW(), NOW()),
|
||||
(9812, 981, '保存', '', 2, 'POST /api/service/admin/notice/channel/save', '', 'notice:channel:save', 2, 1, 0, NOW(), NOW()),
|
||||
(9821, 982, '查询', '', 2, 'POST /api/service/admin/notice/log/list', '', 'notice:log:list', 1, 1, 0, NOW(), NOW()),
|
||||
(9901, 990, '查询', '', 2, 'POST /api/service/admin/auto-job/list', '', 'job:list:query', 1, 1, 0, NOW(), NOW()),
|
||||
(9902, 990, '保存', '', 2, 'POST /api/service/admin/auto-job/save', '', 'job:list:save', 2, 1, 0, NOW(), NOW()),
|
||||
(9903, 990, '触发', '', 2, 'POST /api/service/admin/auto-job/trigger', '', 'job:list:trigger', 3, 1, 0, NOW(), NOW()),
|
||||
(9911, 991, '查询', '', 2, 'POST /api/service/admin/auto-job/log/list', '', 'job:log:list', 1, 1, 0, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id), name=VALUES(name), type=VALUES(type),
|
||||
path=VALUES(path), permission=VALUES(permission), sort=VALUES(sort), status=VALUES(status), hidden=VALUES(hidden), deleted_at=NULL;
|
||||
|
||||
-- ============ 13) 超管角色绑定 ============
|
||||
INSERT IGNORE INTO admin_role_menu (role_id, menu_id, created_at, updated_at)
|
||||
SELECT 1, id, NOW(), NOW() FROM admin_menu WHERE id IN (98,980,981,982,99,990,991,9801,9802,9803,9804,9811,9812,9821,9901,9902,9903,9911);
|
||||
Loading…
Reference in New Issue
Block a user