Compare commits
10 Commits
8721d01028
...
5e517a2d95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e517a2d95 | ||
|
|
abd81486ad | ||
|
|
3d24dbd8cb | ||
|
|
e868c1ef9c | ||
|
|
0569c70690 | ||
|
|
e18b25f32a | ||
|
|
743683efdd | ||
|
|
1d451499b9 | ||
|
|
a85a520af7 | ||
|
|
f81ce64fdd |
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,3 +31,5 @@ log/
|
||||
|
||||
# GoLand 项目级运行配置(含数据库口令),禁止提交
|
||||
.run/
|
||||
service_test.exe
|
||||
service_test2*
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
# service.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-08-27 | CHG | 通知模块+自动任务+用户渠道字段:016 SQL(admin_user 加 bark_device_id/pushplus_token;建 notice_channel/rule/log + auto_job/auto_job_log 5 表;菜单 98 通知中心(980规则/981渠道/982日志)/99 自动任务(990任务/991日志) + type=2 权限;渠道/规则/任务种子;人员管理菜单改名用户管理);api/notice + api/job(11 接口全 POST 无 URL 参数);service/notice(Send 统一发送:Bark 路径式 POST /{key} + pushplus 官方接口;渠道/规则/日志/测试);service/job(DB 驱动 gcron:List/Save/Trigger/LogList + Register 业务函数注册 + 完成/失败自动通知 job_done/job_fail);recruitment scheduler 改 RegisterTasks 纳管(原 StartScheduler 废弃);用户 admin 接口支持渠道字段;已部署:容器重建 api.json 200、11 路由全注册、调度器无 handler 缺失告警、种子数据验证 OK。
|
||||
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 正确)
|
||||
|
||||
2026-08-27 | CHG | 看房数据扩充:013_house_presale.sql 建预售证表(presale_no 唯一);house_community 加 avg_price 字段(010 SQL 同步 + gen dao 更新 entity/do 加 AvgPrice);go build 通过
|
||||
|
||||
@ -5,13 +5,15 @@ import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// AdminItem 管理员列表中的一行。
|
||||
type AdminItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
RoleNames []string `json:"roleNames"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Id uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
BarkDeviceId string `json:"barkDeviceId"`
|
||||
PushplusToken string `json:"pushplusToken"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
RoleNames []string `json:"roleNames"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// AdminListReq 分页查询管理员。
|
||||
@ -30,11 +32,13 @@ type AdminListRes struct {
|
||||
|
||||
// AdminCreateReq 创建管理员。
|
||||
type AdminCreateReq struct {
|
||||
g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"`
|
||||
Username string `json:"username" v:"required"`
|
||||
Password string `json:"password" v:"required|min-length:6#password required|password too short"`
|
||||
Nickname string `json:"nickname"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
g.Meta `path:"/admin/create" method:"post" tags:"Admin/Admin" summary:"创建管理员"`
|
||||
Username string `json:"username" v:"required"`
|
||||
Password string `json:"password" v:"required|min-length:6#password required|password too short"`
|
||||
Nickname string `json:"nickname"`
|
||||
BarkDeviceId string `json:"barkDeviceId"`
|
||||
PushplusToken string `json:"pushplusToken"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
}
|
||||
|
||||
// AdminCreateRes 是 AdminCreateReq 的响应。
|
||||
@ -44,11 +48,13 @@ type AdminCreateRes struct {
|
||||
|
||||
// AdminUpdateReq 更新管理员的资料、状态与角色。
|
||||
type AdminUpdateReq struct {
|
||||
g.Meta `path:"/admin/update/{id}" method:"post" tags:"Admin/Admin" summary:"更新管理员"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
g.Meta `path:"/admin/update/{id}" method:"post" tags:"Admin/Admin" summary:"更新管理员"`
|
||||
Id uint64 `json:"id" in:"path" v:"required"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
BarkDeviceId string `json:"barkDeviceId"`
|
||||
PushplusToken string `json:"pushplusToken"`
|
||||
RoleIds []uint64 `json:"roleIds"`
|
||||
}
|
||||
|
||||
// AdminUpdateRes 是 AdminUpdateReq 的响应。
|
||||
|
||||
@ -50,9 +50,11 @@ type RefreshReq struct {
|
||||
// RefreshRes 是 RefreshReq 的响应。
|
||||
type RefreshRes LoginRes
|
||||
|
||||
// LogoutReq 结束管理员会话。
|
||||
// LogoutReq 结束管理员会话。refreshToken 可选携带:
|
||||
// 传入时撤销对应刷新会话,阻止该设备继续续期(幂等)。
|
||||
type LogoutReq struct {
|
||||
g.Meta `path:"/system/auth/logout" method:"post" tags:"Admin/System/Auth" summary:"管理员登出"`
|
||||
g.Meta `path:"/system/auth/logout" method:"post" tags:"Admin/System/Auth" summary:"管理员登出"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
// LogoutRes 是 LogoutReq 的响应。
|
||||
|
||||
73
api/job/job.go
Normal file
73
api/job/job.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Package job_v1 自动任务管理模块接口契约(DB 驱动的 gcron 调度 + 运行记录)。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含参数;入参一律 body。
|
||||
package job
|
||||
|
||||
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/notice.go
Normal file
128
api/notice/notice.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Package notice_v1 通知模块接口契约(统一推送管理:渠道/规则/日志/测试)。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含参数;入参一律 body。
|
||||
package notice
|
||||
|
||||
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"`
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
// Package recruitment_v1 招聘考试聚合模块接口契约。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含任何参数(查询/路径参数均禁止);入参一律走 body。
|
||||
package recruitment_v1
|
||||
package recruitment
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
91
api/serversecurity/security.go
Normal file
91
api/serversecurity/security.go
Normal file
@ -0,0 +1,91 @@
|
||||
// Package serversecurity_v1 服务器安全监控日志模块接口契约。
|
||||
// 规范(2026-08-27):全部 POST;URL 不含任何参数(查询/路径参数均禁止);入参一律走 body。
|
||||
package serversecurity
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ---------- 上报(宿主机采集脚本调用,open 组 + 内部 token 校验) ----------
|
||||
// SecurityLogItem 单条安全日志。
|
||||
type SecurityLogItem struct {
|
||||
LogTime string `json:"logTime"` // 事件发生时间 YYYY-MM-DD HH:MM:SS
|
||||
SrcIp string `json:"srcIp"` // 来源 IP
|
||||
SrcPort int `json:"srcPort"` // 来源端口
|
||||
DestPort int `json:"destPort"` // 目标端口(如 22025)
|
||||
EventType string `json:"eventType"` // failed_ssh / accepted_ssh / banned / unbanned
|
||||
Detail string `json:"detail"` // 日志详情/原文
|
||||
}
|
||||
|
||||
// SecurityLogReportReq 上报请求。
|
||||
type SecurityLogReportReq struct {
|
||||
g.Meta `path:"/security/log/report" method:"post" tags:"Open/Security" summary:"上报服务器安全日志(内部脚本调用)"`
|
||||
Token string `json:"token" v:"required"` // 内部上报令牌
|
||||
List []SecurityLogItem `json:"list" v:"required|min-length:1"`
|
||||
}
|
||||
|
||||
// SecurityLogReportRes 上报结果。
|
||||
type SecurityLogReportRes struct {
|
||||
Accepted int `json:"accepted"` // 成功入库条数
|
||||
}
|
||||
|
||||
// ---------- 查询(admin 组,受权限保护) ----------
|
||||
// SecurityLogListReq 分页查询请求。
|
||||
type SecurityLogListReq struct {
|
||||
g.Meta `path:"/server-security/log/list" method:"post" tags:"Admin/Security/Log" summary:"安全日志分页查询"`
|
||||
Page int `json:"page" d:"1" v:"min:1"`
|
||||
Size int `json:"size" d:"10" v:"min:1|max:100"`
|
||||
SrcIp string `json:"srcIp"` // 来源 IP 精确/模糊
|
||||
EventType string `json:"eventType"` // 事件类型过滤(空=全部)
|
||||
DestPort int `json:"destPort"` // 目标端口(0=全部)
|
||||
DateFrom string `json:"dateFrom"` // 起始时间 YYYY-MM-DD HH:MM:SS
|
||||
DateTo string `json:"dateTo"` // 结束时间 YYYY-MM-DD HH:MM:SS
|
||||
}
|
||||
|
||||
// SecurityLogListItem 日志列表项。
|
||||
type SecurityLogListItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
LogTime string `json:"logTime"`
|
||||
SrcIp string `json:"srcIp"`
|
||||
SrcPort int `json:"srcPort"`
|
||||
DestPort int `json:"destPort"`
|
||||
EventType string `json:"eventType"`
|
||||
EventName string `json:"eventName"` // 派生:事件中文名
|
||||
Detail string `json:"detail"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// SecurityLogListRes 分页查询结果。
|
||||
type SecurityLogListRes struct {
|
||||
List []*SecurityLogListItem `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ---------- 统计(admin 组,受权限保护) ----------
|
||||
// SecurityLogStatsReq 统计请求。
|
||||
type SecurityLogStatsReq struct {
|
||||
g.Meta `path:"/server-security/log/stats" method:"post" tags:"Admin/Security/Log" summary:"安全日志统计(默认最近24h)"`
|
||||
DateFrom string `json:"dateFrom"` // 起始时间(空=24小时前)
|
||||
DateTo string `json:"dateTo"` // 结束时间(空=当前)
|
||||
}
|
||||
|
||||
// SecurityTopIp TOP 攻击来源 IP。
|
||||
type SecurityTopIp struct {
|
||||
SrcIp string `json:"srcIp"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// SecurityByType 按事件类型分布。
|
||||
type SecurityByType struct {
|
||||
EventType string `json:"eventType"`
|
||||
EventName string `json:"eventName"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// SecurityLogStatsRes 统计结果。
|
||||
type SecurityLogStatsRes struct {
|
||||
Total int `json:"total"` // 范围内总记录
|
||||
Failed int `json:"failed"` // SSH 爆破尝试
|
||||
Banned int `json:"banned"` // fail2ban 封禁
|
||||
Accepted int `json:"accepted"` // 成功登录
|
||||
TopIps []SecurityTopIp `json:"topIps"` // TOP 攻击源(按条数)
|
||||
ByType []SecurityByType `json:"byType"` // 按类型分布
|
||||
}
|
||||
@ -4,15 +4,20 @@ 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"
|
||||
"service.xpcool.com/internal/middleware"
|
||||
@ -31,35 +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())
|
||||
}
|
||||
if v := genv.Get("JWT_SECRET"); !v.IsEmpty() {
|
||||
_ = adapter.Set("jwt.secret", 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, _ := 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")
|
||||
// 服务器安全日志上报令牌(宿主机采集脚本携带,接口侧校验)。
|
||||
setIfEmpty(adapter, "internalToken", "INTERNAL_TOKEN")
|
||||
}
|
||||
|
||||
var (
|
||||
@ -86,9 +115,13 @@ 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)
|
||||
group.Bind(openctl.New()) // Open tools API for frontends, no auth required.
|
||||
group.Bind(openctl.New()) // Open tools API for frontends, no auth required.
|
||||
group.Bind(serversecurityctl.NewReport()) // 安全日志上报(宿主机脚本,内部令牌校验)。
|
||||
})
|
||||
s.Group("/api/service/user", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(middleware.Recover, middleware.CORS, ghttp.MiddlewareHandlerResponse)
|
||||
@ -112,10 +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
|
||||
|
||||
@ -46,8 +46,12 @@ func (c *AuthController) Refresh(ctx context.Context, req *authv1.RefreshReq) (r
|
||||
return &authv1.RefreshRes{AccessToken: p.AccessToken, RefreshToken: p.RefreshToken, ExpiresIn: p.ExpiresIn, AdminID: id}, nil
|
||||
}
|
||||
|
||||
// Logout 结束管理员会话。无状态 JWT 登出依赖客户端
|
||||
// 丢弃令牌,该端点对任意已登录管理员始终成功。
|
||||
// Logout 结束管理员会话。无状态 JWT 的 access token 无法立即作废(等自然过期),
|
||||
// 但携带 refreshToken 时可撤销刷新会话,阻止该设备继续续期。
|
||||
// 幂等设计:即使令牌无效也返回成功,保证前端登出流程不阻塞。
|
||||
func (c *AuthController) Logout(ctx context.Context, req *authv1.LogoutReq) (res *authv1.LogoutRes, err error) {
|
||||
if req.RefreshToken != "" {
|
||||
_ = auth.AdminAuth().Revoke(ctx, req.RefreshToken)
|
||||
}
|
||||
return &authv1.LogoutRes{}, 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"
|
||||
"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"
|
||||
"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
|
||||
}
|
||||
@ -3,7 +3,7 @@ package recruitment
|
||||
import (
|
||||
"context"
|
||||
|
||||
recruitmentv1 "service.xpcool.com/api/recruitment/v1"
|
||||
recruitmentv1 "service.xpcool.com/api/recruitment"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
"service.xpcool.com/internal/service/recruitment"
|
||||
)
|
||||
|
||||
21
internal/controller/serversecurity/controller.go
Normal file
21
internal/controller/serversecurity/controller.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Package serversecurity 服务器安全日志模块控制器。
|
||||
// 分为两个 Controller:ReportController 绑定 open 组(宿主机脚本上报,内部令牌鉴权);
|
||||
// ManageController 绑定 admin 受权限保护组(查询/统计)。
|
||||
package serversecurity
|
||||
|
||||
import (
|
||||
serversecurityv1 "service.xpcool.com/api/serversecurity"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
)
|
||||
|
||||
// toInput 将 API 上报项转为服务层入参。
|
||||
func toInput(items []serversecurityv1.SecurityLogItem) []dto.SecurityLogInput {
|
||||
out := make([]dto.SecurityLogInput, 0, len(items))
|
||||
for _, it := range items {
|
||||
out = append(out, dto.SecurityLogInput{
|
||||
LogTime: it.LogTime, SrcIp: it.SrcIp, SrcPort: it.SrcPort,
|
||||
DestPort: it.DestPort, EventType: it.EventType, Detail: it.Detail,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
57
internal/controller/serversecurity/manage.go
Normal file
57
internal/controller/serversecurity/manage.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Package serversecurity 管理控制器:绑定 admin 受权限保护组(查询/统计)。
|
||||
package serversecurity
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
serversecurityv1 "service.xpcool.com/api/serversecurity"
|
||||
"service.xpcool.com/internal/model/dto"
|
||||
"service.xpcool.com/internal/service/serversecurity"
|
||||
)
|
||||
|
||||
// ManageController 实现安全日志查询与统计端点。
|
||||
type ManageController struct{}
|
||||
|
||||
// NewManage 创建管理控制器。
|
||||
func NewManage() *ManageController { return &ManageController{} }
|
||||
|
||||
// ListLog 安全日志分页查询(时间倒序)。
|
||||
func (c *ManageController) ListLog(ctx context.Context, req *serversecurityv1.SecurityLogListReq) (res *serversecurityv1.SecurityLogListRes, err error) {
|
||||
list, total, err := serversecurity.Security().List(ctx, dto.SecurityLogFilter{
|
||||
Page: req.Page, Size: req.Size, SrcIp: req.SrcIp, EventType: req.EventType,
|
||||
DestPort: req.DestPort, DateFrom: req.DateFrom, DateTo: req.DateTo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*serversecurityv1.SecurityLogListItem, 0, len(list))
|
||||
for i := range list {
|
||||
v := &list[i]
|
||||
out = append(out, &serversecurityv1.SecurityLogListItem{
|
||||
Id: v.Id, LogTime: v.LogTime, SrcIp: v.SrcIp, SrcPort: v.SrcPort,
|
||||
DestPort: v.DestPort, EventType: v.EventType, EventName: v.EventName,
|
||||
Detail: v.Detail, CreatedAt: v.CreatedAt,
|
||||
})
|
||||
}
|
||||
return &serversecurityv1.SecurityLogListRes{List: out, Total: total}, nil
|
||||
}
|
||||
|
||||
// Stats 安全日志统计(默认最近24小时)。
|
||||
func (c *ManageController) Stats(ctx context.Context, req *serversecurityv1.SecurityLogStatsReq) (res *serversecurityv1.SecurityLogStatsRes, err error) {
|
||||
st, err := serversecurity.Security().Stats(ctx, req.DateFrom, req.DateTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
topIps := make([]serversecurityv1.SecurityTopIp, 0, len(st.TopIps))
|
||||
for _, v := range st.TopIps {
|
||||
topIps = append(topIps, serversecurityv1.SecurityTopIp{SrcIp: v.SrcIp, Count: v.Count})
|
||||
}
|
||||
byType := make([]serversecurityv1.SecurityByType, 0, len(st.ByType))
|
||||
for _, v := range st.ByType {
|
||||
byType = append(byType, serversecurityv1.SecurityByType{EventType: v.EventType, EventName: v.EventName, Count: v.Count})
|
||||
}
|
||||
return &serversecurityv1.SecurityLogStatsRes{
|
||||
Total: st.Total, Failed: st.Failed, Banned: st.Banned, Accepted: st.Accepted,
|
||||
TopIps: topIps, ByType: byType,
|
||||
}, nil
|
||||
}
|
||||
24
internal/controller/serversecurity/report.go
Normal file
24
internal/controller/serversecurity/report.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Package serversecurity 上报控制器:绑定 open 组(无登录态),仅宿主机采集脚本调用。
|
||||
package serversecurity
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
serversecurityv1 "service.xpcool.com/api/serversecurity"
|
||||
"service.xpcool.com/internal/service/serversecurity"
|
||||
)
|
||||
|
||||
// ReportController 实现安全日志上报端点。
|
||||
type ReportController struct{}
|
||||
|
||||
// NewReport 创建上报控制器。
|
||||
func NewReport() *ReportController { return &ReportController{} }
|
||||
|
||||
// ReportLog 接收宿主机采集脚本上报的安全日志并入库(内部令牌鉴权)。
|
||||
func (c *ReportController) ReportLog(ctx context.Context, req *serversecurityv1.SecurityLogReportReq) (res *serversecurityv1.SecurityLogReportRes, err error) {
|
||||
n, err := serversecurity.Security().Report(ctx, req.Token, toInput(req.List))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &serversecurityv1.SecurityLogReportRes{Accepted: n}, nil
|
||||
}
|
||||
@ -21,28 +21,32 @@ type AdminUserDao struct {
|
||||
|
||||
// AdminUserColumns 定义并存储表 admin_user 的列名。
|
||||
type AdminUserColumns struct {
|
||||
Id string //
|
||||
Username string //
|
||||
PasswordHash string //
|
||||
Nickname string //
|
||||
Status string //
|
||||
LastLoginAt string //
|
||||
CreatedAt string //
|
||||
UpdatedAt string //
|
||||
DeletedAt string //
|
||||
Id string //
|
||||
Username string //
|
||||
PasswordHash string //
|
||||
Nickname string //
|
||||
Status string //
|
||||
BarkDeviceId string //
|
||||
PushplusToken string //
|
||||
LastLoginAt string //
|
||||
CreatedAt string //
|
||||
UpdatedAt string //
|
||||
DeletedAt string //
|
||||
}
|
||||
|
||||
// adminUserColumns 保存表 admin_user 的列信息。
|
||||
var adminUserColumns = AdminUserColumns{
|
||||
Id: "id",
|
||||
Username: "username",
|
||||
PasswordHash: "password_hash",
|
||||
Nickname: "nickname",
|
||||
Status: "status",
|
||||
LastLoginAt: "last_login_at",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
DeletedAt: "deleted_at",
|
||||
Id: "id",
|
||||
Username: "username",
|
||||
PasswordHash: "password_hash",
|
||||
Nickname: "nickname",
|
||||
Status: "status",
|
||||
BarkDeviceId: "bark_device_id",
|
||||
PushplusToken: "pushplus_token",
|
||||
LastLoginAt: "last_login_at",
|
||||
CreatedAt: "created_at",
|
||||
UpdatedAt: "updated_at",
|
||||
DeletedAt: "deleted_at",
|
||||
}
|
||||
|
||||
// NewAdminUserDao 创建并返回一个新的表数据访问 DAO 对象。
|
||||
|
||||
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) }
|
||||
18
internal/dao/server_security_log.go
Normal file
18
internal/dao/server_security_log.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package dao 服务器安全日志数据访问(主库默认分组,手写)。
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type serverSecurityLogDao struct{ table string }
|
||||
|
||||
// ServerSecurityLog 安全日志表全局访问对象(主库 service)。
|
||||
var ServerSecurityLog = serverSecurityLogDao{table: "server_security_log"}
|
||||
|
||||
func (d serverSecurityLogDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
return g.DB().Model(d.table).Safe()
|
||||
}
|
||||
11
internal/library/response/http.go
Normal file
11
internal/library/response/http.go
Normal file
@ -0,0 +1,11 @@
|
||||
package response
|
||||
|
||||
import "github.com/gogf/gf/v2/net/ghttp"
|
||||
|
||||
// JSONWithStatus 输出带 HTTP 状态码的统一 JSON 信封。
|
||||
// 401 等需要触发前端认证拦截器(vben 仅识别 HTTP 401)的场景必须用它,
|
||||
// 普通业务错误仍用 JSON(HTTP 200 + code 区分),避免前端按异常处理。
|
||||
func JSONWithStatus(r *ghttp.Request, httpStatus, code int, message string, data any) {
|
||||
r.Response.WriteStatus(httpStatus)
|
||||
r.Response.WriteJsonExit(Body{Code: code, Message: message, Data: data})
|
||||
}
|
||||
@ -55,7 +55,7 @@ func UserAuth(s *jwt.Service) ghttp.HandlerFunc {
|
||||
return func(r *ghttp.Request) {
|
||||
c, err := s.Parse(bearer(r), "access", "user")
|
||||
if err != nil {
|
||||
response.JSON(r, consts.CodeUnauthorized, "login required", nil)
|
||||
response.JSONWithStatus(r, 401, consts.CodeUnauthorized, "login required", nil)
|
||||
return
|
||||
}
|
||||
r.SetCtxVar(UserIDKey, c.Subject)
|
||||
@ -68,7 +68,7 @@ func AdminAuthOnly(s *jwt.Service) ghttp.HandlerFunc {
|
||||
return func(r *ghttp.Request) {
|
||||
c, err := s.Parse(bearer(r), "access", "admin")
|
||||
if err != nil {
|
||||
response.JSON(r, consts.CodeUnauthorized, "admin login required", nil)
|
||||
response.JSONWithStatus(r, 401, consts.CodeUnauthorized, "admin login required", nil)
|
||||
return
|
||||
}
|
||||
r.SetCtxVar(AdminIDKey, c.Subject)
|
||||
@ -83,7 +83,7 @@ func AdminAuth(s *jwt.Service, permissionLookup func(context.Context, string, st
|
||||
start := time.Now()
|
||||
c, err := s.Parse(bearer(r), "access", "admin")
|
||||
if err != nil {
|
||||
response.JSON(r, consts.CodeUnauthorized, "admin login required", nil)
|
||||
response.JSONWithStatus(r, 401, consts.CodeUnauthorized, "admin login required", nil)
|
||||
return
|
||||
}
|
||||
permission, err := permissionLookup(r.Context(), r.Method, r.URL.Path)
|
||||
|
||||
@ -11,14 +11,16 @@ import (
|
||||
|
||||
// AdminUser 是表 admin_user 的 Go 结构体,供 DAO 的 Where/Data 等操作使用。
|
||||
type AdminUser struct {
|
||||
g.Meta `orm:"table:admin_user, do:true"`
|
||||
Id any //
|
||||
Username any //
|
||||
PasswordHash any //
|
||||
Nickname any //
|
||||
Status any //
|
||||
LastLoginAt *gtime.Time //
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
DeletedAt *gtime.Time //
|
||||
g.Meta `orm:"table:admin_user, do:true"`
|
||||
Id any //
|
||||
Username any //
|
||||
PasswordHash any //
|
||||
Nickname any //
|
||||
Status any //
|
||||
BarkDeviceId any //
|
||||
PushplusToken any //
|
||||
LastLoginAt *gtime.Time //
|
||||
CreatedAt *gtime.Time //
|
||||
UpdatedAt *gtime.Time //
|
||||
DeletedAt *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 //
|
||||
}
|
||||
18
internal/model/do/server_security_log.go
Normal file
18
internal/model/do/server_security_log.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package do 定义服务器安全日志数据对象(手写,未跑 gf gen)。
|
||||
// 字段类型统一用 any,与 gf gen 产物保持一致。
|
||||
package do
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ServerSecurityLog 安全日志表 DO。
|
||||
type ServerSecurityLog struct {
|
||||
g.Meta `orm:"table:server_security_log, do:true"`
|
||||
Id any // 主键
|
||||
LogTime any // 事件发生时间
|
||||
SrcIp any // 来源IP
|
||||
SrcPort any // 来源端口
|
||||
DestPort any // 目标端口
|
||||
EventType any // 事件类型
|
||||
Detail any // 日志详情
|
||||
CreatedAt any // 入库时间
|
||||
}
|
||||
@ -8,27 +8,33 @@ type PageQuery struct {
|
||||
}
|
||||
|
||||
type AdminItem struct {
|
||||
Id uint64
|
||||
Username string
|
||||
Nickname string
|
||||
Status int
|
||||
RoleIds []uint64
|
||||
RoleNames []string
|
||||
CreatedAt string
|
||||
Id uint64
|
||||
Username string
|
||||
Nickname string
|
||||
Status int
|
||||
BarkDeviceId string
|
||||
PushplusToken string
|
||||
RoleIds []uint64
|
||||
RoleNames []string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
type AdminCreateInput struct {
|
||||
Username string
|
||||
Password string
|
||||
Nickname string
|
||||
RoleIds []uint64
|
||||
Username string
|
||||
Password string
|
||||
Nickname string
|
||||
BarkDeviceId string
|
||||
PushplusToken string
|
||||
RoleIds []uint64
|
||||
}
|
||||
|
||||
type AdminUpdateInput struct {
|
||||
Id uint64
|
||||
Nickname string
|
||||
Status int
|
||||
RoleIds []uint64
|
||||
Id uint64
|
||||
Nickname string
|
||||
Status int
|
||||
BarkDeviceId string
|
||||
PushplusToken string
|
||||
RoleIds []uint64
|
||||
}
|
||||
|
||||
type RoleItem struct {
|
||||
|
||||
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
|
||||
}
|
||||
74
internal/model/dto/security.go
Normal file
74
internal/model/dto/security.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Package dto 定义服务器安全日志模块的服务边界对象(入参/出参)。
|
||||
package dto
|
||||
|
||||
// SecurityLogFilter 安全日志查询筛选条件。
|
||||
type SecurityLogFilter struct {
|
||||
Page int // 页码,从 1 开始
|
||||
Size int // 每页大小
|
||||
SrcIp string // 来源 IP
|
||||
EventType string // 事件类型(空=全部)
|
||||
DestPort int // 目标端口(0=全部)
|
||||
DateFrom string // 起始时间 YYYY-MM-DD HH:MM:SS
|
||||
DateTo string // 结束时间 YYYY-MM-DD HH:MM:SS
|
||||
}
|
||||
|
||||
// SecurityLogInput 上报入库的日志项。
|
||||
type SecurityLogInput struct {
|
||||
LogTime string // 事件发生时间 YYYY-MM-DD HH:MM:SS
|
||||
SrcIp string // 来源 IP
|
||||
SrcPort int // 来源端口
|
||||
DestPort int // 目标端口
|
||||
EventType string // 事件类型
|
||||
Detail string // 日志详情
|
||||
}
|
||||
|
||||
// SecurityLogVO 日志出参视图。
|
||||
type SecurityLogVO struct {
|
||||
Id uint64
|
||||
LogTime string
|
||||
SrcIp string
|
||||
SrcPort int
|
||||
DestPort int
|
||||
EventType string
|
||||
EventName string // 派生:事件中文名
|
||||
Detail string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
// SecurityStats 统计出参。
|
||||
type SecurityStats struct {
|
||||
Total int
|
||||
Failed int
|
||||
Banned int
|
||||
Accepted int
|
||||
TopIps []SecurityTopIp
|
||||
ByType []SecurityByType
|
||||
}
|
||||
|
||||
// SecurityTopIp TOP 攻击源。
|
||||
type SecurityTopIp struct {
|
||||
SrcIp string `json:"srcIp"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// SecurityByType 按类型分布。
|
||||
type SecurityByType struct {
|
||||
EventType string `json:"eventType"`
|
||||
EventName string `json:"eventName"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// SecurityEventName 事件类型中文名。
|
||||
func SecurityEventName(t string) string {
|
||||
switch t {
|
||||
case "failed_ssh":
|
||||
return "SSH爆破尝试"
|
||||
case "accepted_ssh":
|
||||
return "SSH成功登录"
|
||||
case "banned":
|
||||
return "fail2ban封禁"
|
||||
case "unbanned":
|
||||
return "fail2ban解封"
|
||||
}
|
||||
return t
|
||||
}
|
||||
@ -10,13 +10,15 @@ import (
|
||||
|
||||
// AdminUser 是表 admin_user 的 Go 结构体。
|
||||
type AdminUser struct {
|
||||
Id uint64 `json:"id" orm:"id" description:""` //
|
||||
Username string `json:"username" orm:"username" description:""` //
|
||||
PasswordHash string `json:"passwordHash" orm:"password_hash" description:""` //
|
||||
Nickname string `json:"nickname" orm:"nickname" description:""` //
|
||||
Status int `json:"status" orm:"status" description:""` //
|
||||
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:""` //
|
||||
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:""` //
|
||||
Id uint64 `json:"id" orm:"id" description:""` //
|
||||
Username string `json:"username" orm:"username" description:""` //
|
||||
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:""` //
|
||||
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_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:"创建时间"`
|
||||
}
|
||||
14
internal/model/entity/server_security_log.go
Normal file
14
internal/model/entity/server_security_log.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Package entity 定义服务器安全日志表结构(手写,未跑 gf gen)。
|
||||
package entity
|
||||
|
||||
// ServerSecurityLog 服务器安全监控日志(宿主机采集脚本上报)。
|
||||
type ServerSecurityLog struct {
|
||||
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||
LogTime string `json:"logTime" orm:"log_time" description:"事件发生时间"`
|
||||
SrcIp string `json:"srcIp" orm:"src_ip" description:"来源IP"`
|
||||
SrcPort int `json:"srcPort" orm:"src_port" description:"来源端口"`
|
||||
DestPort int `json:"destPort" orm:"dest_port" description:"目标端口"`
|
||||
EventType string `json:"eventType" orm:"event_type" description:"事件类型 failed_ssh/accepted_ssh/banned/unbanned"`
|
||||
Detail string `json:"detail" orm:"detail" 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")
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ import (
|
||||
type IAdminAuth interface {
|
||||
Login(context.Context, dto.AdminLoginInput) (*dto.TokenPair, uint64, error)
|
||||
Refresh(context.Context, string) (*dto.TokenPair, uint64, error)
|
||||
// Revoke 撤销刷新令牌会话(登出时调用),幂等:解析失败或已撤销均视为成功。
|
||||
Revoke(context.Context, string) error
|
||||
HasPermission(context.Context, uint64, string) (bool, error)
|
||||
Info(context.Context, uint64) (*dto.AdminInfo, error)
|
||||
Codes(context.Context, uint64) ([]string, error)
|
||||
@ -64,6 +66,8 @@ func (s *adminAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair
|
||||
if err != nil {
|
||||
return nil, 0, response.Error(consts.CodeUnauthorized, "invalid refresh token")
|
||||
}
|
||||
// 顺手清理已过期的刷新会话,防止表无限膨胀(失败不影响主流程)。
|
||||
_, _ = dao.AuthRefreshSession.Ctx(ctx).WhereLT("expired_at", gtime.Now()).Delete()
|
||||
// 单次使用:撤销旧刷新会话并校验影响行数,重复使用旧令牌直接拒绝。
|
||||
result, err := dao.AuthRefreshSession.Ctx(ctx).Where(do.AuthRefreshSession{Jti: c.JTI}).WhereNull("revoked_at").Data(do.AuthRefreshSession{RevokedAt: gtime.Now()}).Update()
|
||||
if err != nil {
|
||||
@ -76,6 +80,22 @@ func (s *adminAuth) Refresh(ctx context.Context, refresh string) (*dto.TokenPair
|
||||
return s.issue(ctx, c.Subject, c.Terminal)
|
||||
}
|
||||
|
||||
// Revoke 撤销刷新令牌会话:登出时由控制器携带 refreshToken 调用。
|
||||
// 无状态 JWT 无法真正作废 access token(等它自然过期,默认 2h),
|
||||
// 但撤销 refresh 会话可阻止续期,等于关闭该设备的长期会话。
|
||||
func (s *adminAuth) Revoke(ctx context.Context, refresh string) error {
|
||||
c, err := s.tokens.Parse(refresh, "refresh", "admin")
|
||||
if err != nil {
|
||||
// 令牌无效/已过期:无需撤销,视为成功(幂等)。
|
||||
return nil
|
||||
}
|
||||
_, err = dao.AuthRefreshSession.Ctx(ctx).Where(do.AuthRefreshSession{Jti: c.JTI}).WhereNull("revoked_at").Data(do.AuthRefreshSession{RevokedAt: gtime.Now()}).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "撤销刷新令牌会话失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// issue 签发令牌对并把刷新令牌的 JTI 落库,支持撤销与设备会话追踪。
|
||||
func (s *adminAuth) issue(ctx context.Context, id uint64, terminal string) (*dto.TokenPair, uint64, error) {
|
||||
access, refresh, exp, err := s.tokens.Issue(id, "admin", terminal)
|
||||
|
||||
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,26 +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。
|
||||
func StartScheduler(ctx context.Context) {
|
||||
if _, err := gcron.Add(ctx, "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)
|
||||
return summary, e
|
||||
}
|
||||
}, "recruit-crawl-daily"); err != nil {
|
||||
g.Log().Errorf(ctx, "add recruit crawl cron failed: %v", err)
|
||||
}
|
||||
if _, err := gcron.Add(ctx, "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, nil
|
||||
})
|
||||
// 每日 08:00 推送招聘早报(订阅列表)
|
||||
job.Job().Register("recruit-push-daily", func(ctx context.Context) (string, error) {
|
||||
sendDailyDigest(ctx)
|
||||
return "招聘早报推送完成", nil
|
||||
})
|
||||
}
|
||||
|
||||
155
internal/service/serversecurity/security.go
Normal file
155
internal/service/serversecurity/security.go
Normal file
@ -0,0 +1,155 @@
|
||||
// Package serversecurity 提供服务器安全监控日志领域服务(采集上报/查询/统计)。
|
||||
package serversecurity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// ISecurity 服务器安全日志领域服务接口。
|
||||
type ISecurity interface {
|
||||
Report(context.Context, string, []dto.SecurityLogInput) (int, error)
|
||||
List(context.Context, dto.SecurityLogFilter) ([]dto.SecurityLogVO, int, error)
|
||||
Stats(context.Context, string, string) (*dto.SecurityStats, error)
|
||||
}
|
||||
|
||||
type security struct{}
|
||||
|
||||
var localSecurity ISecurity
|
||||
|
||||
// New 创建安全日志服务实现。
|
||||
func New() ISecurity { return &security{} }
|
||||
|
||||
// Security 返回已注册的安全日志服务实现。
|
||||
func Security() ISecurity {
|
||||
if localSecurity == nil {
|
||||
panic("Security implementation not registered")
|
||||
}
|
||||
return localSecurity
|
||||
}
|
||||
|
||||
// RegisterServerSecurity 注册安全日志服务实现。
|
||||
func RegisterServerSecurity(i ISecurity) { localSecurity = i }
|
||||
|
||||
// Report 校验内部令牌并把采集到的日志批量入库(幂等:按 log_time+src_ip+event_type 去重)。
|
||||
func (s *security) Report(ctx context.Context, token string, items []dto.SecurityLogInput) (int, error) {
|
||||
// 校验内部上报令牌(配置 internalToken,来自环境变量 INTERNAL_TOKEN)。
|
||||
expect := g.Cfg().MustGet(ctx, "internalToken").String()
|
||||
if expect == "" || token != expect {
|
||||
return 0, response.Error(consts.CodeUnauthorized, "内部上报令牌无效")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
rows := make([]map[string]interface{}, 0, len(items))
|
||||
for _, it := range items {
|
||||
// 事件时间兜底:非法/空则用当前时间。
|
||||
logTime := strings.TrimSpace(it.LogTime)
|
||||
if logTime == "" {
|
||||
logTime = gtime.Now().Format("Y-m-d H:i:s")
|
||||
}
|
||||
srcIp := strings.TrimSpace(it.SrcIp)
|
||||
if len(srcIp) > 45 {
|
||||
srcIp = srcIp[:45]
|
||||
}
|
||||
detail := strings.TrimSpace(it.Detail)
|
||||
if len(detail) > 500 {
|
||||
detail = detail[:500]
|
||||
}
|
||||
rows = append(rows, map[string]interface{}{
|
||||
"log_time": logTime,
|
||||
"src_ip": srcIp,
|
||||
"src_port": it.SrcPort,
|
||||
"dest_port": it.DestPort,
|
||||
"event_type": strings.TrimSpace(it.EventType),
|
||||
"detail": detail,
|
||||
})
|
||||
}
|
||||
if _, err := dao.ServerSecurityLog.Ctx(ctx).Data(rows).Insert(); err != nil {
|
||||
return 0, gerror.Wrap(err, "insert server security log")
|
||||
}
|
||||
return len(rows), nil
|
||||
}
|
||||
|
||||
// List 分页查询安全日志(时间倒序)。
|
||||
func (s *security) List(ctx context.Context, f dto.SecurityLogFilter) ([]dto.SecurityLogVO, int, error) {
|
||||
m := dao.ServerSecurityLog.Ctx(ctx)
|
||||
if f.SrcIp != "" {
|
||||
m = m.Where("src_ip LIKE ?", "%"+f.SrcIp+"%")
|
||||
}
|
||||
if f.EventType != "" {
|
||||
m = m.Where("event_type", f.EventType)
|
||||
}
|
||||
if f.DestPort > 0 {
|
||||
m = m.Where("dest_port", f.DestPort)
|
||||
}
|
||||
if f.DateFrom != "" {
|
||||
m = m.WhereGTE("log_time", f.DateFrom)
|
||||
}
|
||||
if f.DateTo != "" {
|
||||
m = m.WhereLTE("log_time", f.DateTo)
|
||||
}
|
||||
total, err := m.Clone().Count()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "count security log")
|
||||
}
|
||||
var list []dto.SecurityLogVO
|
||||
if err = m.Clone().Page(f.Page, f.Size).OrderDesc("log_time").OrderDesc("id").Scan(&list); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "query security log")
|
||||
}
|
||||
for i := range list {
|
||||
list[i].EventName = dto.SecurityEventName(list[i].EventType)
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// Stats 统计:总数/爆破/封禁/成功登录 + TOP 攻击源 + 按类型分布。
|
||||
// 时间范围为闭区间,空则默认最近 24 小时。
|
||||
func (s *security) Stats(ctx context.Context, dateFrom, dateTo string) (*dto.SecurityStats, error) {
|
||||
if dateFrom == "" {
|
||||
dateFrom = gtime.Now().AddDate(0, 0, -1).Format("Y-m-d H:i:s")
|
||||
}
|
||||
if dateTo == "" {
|
||||
dateTo = gtime.Now().Format("Y-m-d H:i:s")
|
||||
}
|
||||
base := dao.ServerSecurityLog.Ctx(ctx).Where("log_time BETWEEN ? AND ?", dateFrom, dateTo)
|
||||
|
||||
st := &dto.SecurityStats{}
|
||||
var err error
|
||||
if st.Total, err = base.Clone().Count(); err != nil {
|
||||
return nil, gerror.Wrap(err, "count total")
|
||||
}
|
||||
if st.Failed, err = base.Clone().Where("event_type", "failed_ssh").Count(); err != nil {
|
||||
return nil, gerror.Wrap(err, "count failed")
|
||||
}
|
||||
if st.Banned, err = base.Clone().Where("event_type", "banned").Count(); err != nil {
|
||||
return nil, gerror.Wrap(err, "count banned")
|
||||
}
|
||||
if st.Accepted, err = base.Clone().Where("event_type", "accepted_ssh").Count(); err != nil {
|
||||
return nil, gerror.Wrap(err, "count accepted")
|
||||
}
|
||||
// TOP 攻击源(爆破+封禁优先)。
|
||||
if err = base.Clone().WhereIn("event_type", g.Slice{"failed_ssh", "banned"}).
|
||||
Fields("src_ip, COUNT(*) AS cnt").
|
||||
Group("src_ip").OrderDesc("cnt").Limit(10).Scan(&st.TopIps); err != nil {
|
||||
return nil, gerror.Wrap(err, "query top ips")
|
||||
}
|
||||
// 按类型分布。
|
||||
if err = base.Clone().Fields("event_type, COUNT(*) AS cnt").
|
||||
Group("event_type").Scan(&st.ByType); err != nil {
|
||||
return nil, gerror.Wrap(err, "query by type")
|
||||
}
|
||||
for i := range st.ByType {
|
||||
st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
@ -3,3 +3,5 @@ logger: { level: "warning", stdout: true }
|
||||
database: { default: { link: "${DB_DSN}" }, recruitment: { link: "${RECRUITMENT_DB_DSN}" } }
|
||||
jwt: { secret: "${JWT_SECRET}", accessExpire: "2h", refreshExpire: "720h" }
|
||||
bark: { baseUrl: "${BARK_BASE_URL}", deviceKey: "${BARK_DEVICE_KEY}", pushTime: "${BARK_PUSH_TIME}" }
|
||||
# 安全日志上报令牌(宿主机采集脚本携带,环境变量 INTERNAL_TOKEN 注入)。
|
||||
internalToken: "${INTERNAL_TOKEN}"
|
||||
|
||||
38
manifest/sql/015_server_security.sql
Normal file
38
manifest/sql/015_server_security.sql
Normal file
@ -0,0 +1,38 @@
|
||||
-- 015_server_security.sql
|
||||
-- 服务器安全监控日志:建表 + 菜单/权限种子。
|
||||
-- 依赖:service 主库;admin_menu / admin_role_menu 已存在。
|
||||
-- 幂等:CREATE TABLE IF NOT EXISTS + INSERT ... ON DUPLICATE KEY UPDATE + INSERT IGNORE,可重复执行。
|
||||
|
||||
-- ============ 1) 安全日志表 ============
|
||||
CREATE TABLE IF NOT EXISTS `server_security_log` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`log_time` DATETIME NOT NULL COMMENT '事件发生时间',
|
||||
`src_ip` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '来源IP(含IPv6)',
|
||||
`src_port` INT NOT NULL DEFAULT 0 COMMENT '来源端口',
|
||||
`dest_port` INT NOT NULL DEFAULT 0 COMMENT '目标端口(如22025)',
|
||||
`event_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '事件类型 failed_ssh/accepted_ssh/banned/unbanned',
|
||||
`detail` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '日志详情/原文',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_time` (`log_time`),
|
||||
KEY `idx_ip` (`src_ip`),
|
||||
KEY `idx_type` (`event_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务器安全监控日志(宿主机采集上报)';
|
||||
|
||||
-- ============ 2) 菜单:挂到「系统监控」(id=5) 下 ============
|
||||
INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES
|
||||
(96, 5, '安全日志', 'mdi:shield-alert-outline', 1, 'security', 'monitor/security/index', 'server:security', 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;
|
||||
|
||||
-- ============ 3) API 权限(type=2,path 必须与实际路由一致) ============
|
||||
INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES
|
||||
(960, 96, '查询', '', 2, 'POST /api/service/admin/server-security/log/list', '', 'server:security:list', 1, 1, 0, NOW(), NOW()),
|
||||
(961, 96, '统计', '', 2, 'POST /api/service/admin/server-security/log/stats', '', 'server:security:stats', 2, 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;
|
||||
|
||||
-- ============ 4) 超管角色绑定 ============
|
||||
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 (96, 960, 961);
|
||||
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);
|
||||
@ -10,11 +10,11 @@
|
||||
|
||||
-- ============ 1) 招聘中心菜单(type=1) ============
|
||||
INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES
|
||||
(95, 0, '招聘中心', 'mdi:briefcase-search', 1, '/recruitment', '', 'recruitment:center', 7, 1, 0, NOW(), NOW()),
|
||||
(951, 95, '招聘公告', 'mdi:file-document', 1, 'recruitment/info', 'recruitment/info/index', 'recruitment:info', 1, 1, 0, NOW(), NOW()),
|
||||
(952, 95, '数据看板', 'mdi:chart-box', 1, 'recruitment/dashboard','recruitment/dashboard/index','recruitment:dashboard', 2, 1, 0, NOW(), NOW()),
|
||||
(953, 95, '数据源', 'mdi:web-sync', 1, 'recruitment/crawler', 'recruitment/crawler/index', 'recruitment:crawler', 3, 1, 0, NOW(), NOW()),
|
||||
(954, 95, '推送订阅', 'mdi:bell-ring', 1, 'recruitment/push', 'recruitment/push/index', 'recruitment:push', 4, 1, 0, NOW(), NOW())
|
||||
(97, 0, '招聘中心', 'mdi:briefcase-search', 1, '/recruitment', '', 'recruitment:center', 7, 1, 0, NOW(), NOW()),
|
||||
(951, 97, '招聘公告', 'mdi:file-document', 1, 'recruitment/info', 'recruitment/info/index', 'recruitment:info', 1, 1, 0, NOW(), NOW()),
|
||||
(952, 97, '数据看板', 'mdi:chart-box', 1, 'recruitment/dashboard','recruitment/dashboard/index','recruitment:dashboard', 2, 1, 0, NOW(), NOW()),
|
||||
(953, 97, '数据源', 'mdi:web-sync', 1, 'recruitment/crawler', 'recruitment/crawler/index', 'recruitment:crawler', 3, 1, 0, NOW(), NOW()),
|
||||
(954, 97, '推送订阅', 'mdi:bell-ring', 1, 'recruitment/push', 'recruitment/push/index', 'recruitment:push', 4, 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;
|
||||
@ -56,4 +56,4 @@ ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id), name=VALUES(name), icon=VAL
|
||||
-- ============ 6) 超管角色绑定新菜单(role_id=1 超管) ============
|
||||
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 (95,951,952,953,954,9510,9511,9520,9521,9530,9531,9540,9541,9542,9543);
|
||||
WHERE id IN (97,951,952,953,954,9510,9511,9520,9521,9530,9531,9540,9541,9542,9543);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user