这两批改动此前一直压在工作区未提交,本次一并归档。go build ./... 通过。
一、招聘爬虫健壮性
- 新增 internal/service/recruitment/source_config.go:
crawl_source.config(VARCHAR(1000) JSON)的源级抓取参数解析 —— 增量窗口天数、
单次详情页上限、最大翻页数、详情间隔限速、自定义标准词/排除词。
全部字段可选,JSON 解析失败即回退默认值,保证历史数据零迁移可用,
且一个源的脏配置不会拖垮整个调度。
- 新增 internal/service/recruitment/keywords.go(含 keywords_test.go 与 testdata/):
链接筛选由「URL 形态命中 AND 文本语义命中」改为评分制 —— URL 形态加分、
标准词加分、排除词大幅减分,达阈值即入选。原与逻辑会系统性漏抓
「补充工作人员的通知」「公开选调公务员简章」等标题变体,且静默无报错。
- 新增 manifest/sql/recruitment/004_crawl_source_status.sql:
crawl_source 增加「最近一次运行状态」冗余列,抓取结束时写入,
使 Sources() 查询零 JOIN 零扫描 —— 规避原实现全表扫描只增不减的 crawl_log、
随运行时间线性劣化的问题。幂等,可重复执行。
- internal/service/recruitment/crawler.go:在既有「静态两级 / SPA / curl 回退」
流程上做锚点抽取与过滤、编码回退的健壮性增强。
- internal/service/recruitment/bark.go、recruitment.go:配合上述调整。
二、通知记录增强
- api/notice、internal/controller/notice、internal/service/notice、
internal/model/{dto,entity,do}/notice.go:通知日志查询与操作扩展 ——
批次号、状态、重试次数、来源、耗时、详情、删除、清空。
- 新增 internal/model/dto/notice_meta.go(字典选项元数据,供前端筛选器取选项)。
- 新增 manifest/sql/017_notice_log_enhance.sql:notice_log 扩展列 +
删除/清空/详情/字典选项接口权限,幂等(ALTER 走 information_schema 判断,
菜单 ON DUPLICATE KEY UPDATE,角色绑定 INSERT IGNORE)。
三、仓库卫生
- .gitignore 补 rc_*.log 与 rc_verify*,挡掉本地离线验证产物
(含 30MB 的 rc_verify.exe),避免误入库。
设计依据见 docs/recruitment-crawler-design.md。
注:017 与 recruitment/004 两个 SQL 为增量迁移,DB 结构变更不自动 DDL,
生产库需手动导入。
313 lines
10 KiB
Go
313 lines
10 KiB
Go
// Package dto 通知模块的枚举字典:渠道 / 事件 / 类型 / 分组。
|
||
// 说明:通知历史记录需要展示「渠道、类型、分组」等中文名称,
|
||
// 这里集中维护编码→名称映射,前端下拉选项也由 OptionList 统一下发,避免前后端各写一份。
|
||
package dto
|
||
|
||
// NoticeMetaItem 通知字典项(编码 + 名称)。
|
||
type NoticeMetaItem struct {
|
||
Code string `json:"code"`
|
||
Name string `json:"name"`
|
||
}
|
||
|
||
// ---------------- 渠道 ----------------
|
||
|
||
// 渠道编码常量(与 notice_channel.code 对应)。
|
||
const (
|
||
NoticeChannelBark = "bark" // Bark(iOS 推送)
|
||
NoticeChannelPushplus = "pushplus" // pushplus(微信推送)
|
||
NoticeChannelWebhook = "webhook" // 通用 Webhook
|
||
NoticeChannelEmail = "email" // 邮件
|
||
NoticeChannelInternal = "internal" // 站内消息
|
||
)
|
||
|
||
// channelNames 渠道编码 → 中文名称。
|
||
var channelNames = map[string]string{
|
||
NoticeChannelBark: "Bark",
|
||
NoticeChannelPushplus: "pushplus",
|
||
NoticeChannelWebhook: "Webhook",
|
||
NoticeChannelEmail: "邮件",
|
||
NoticeChannelInternal: "站内消息",
|
||
}
|
||
|
||
// NoticeChannelName 渠道编码转中文名称(未知编码原样返回)。
|
||
func NoticeChannelName(code string) string {
|
||
if code == "" {
|
||
return ""
|
||
}
|
||
if name, ok := channelNames[code]; ok {
|
||
return name
|
||
}
|
||
return code
|
||
}
|
||
|
||
// NoticeChannelOptions 渠道选项(前端下拉)。
|
||
func NoticeChannelOptions() []NoticeMetaItem {
|
||
return []NoticeMetaItem{
|
||
{Code: NoticeChannelBark, Name: channelNames[NoticeChannelBark]},
|
||
{Code: NoticeChannelPushplus, Name: channelNames[NoticeChannelPushplus]},
|
||
{Code: NoticeChannelWebhook, Name: channelNames[NoticeChannelWebhook]},
|
||
{Code: NoticeChannelEmail, Name: channelNames[NoticeChannelEmail]},
|
||
{Code: NoticeChannelInternal, Name: channelNames[NoticeChannelInternal]},
|
||
}
|
||
}
|
||
|
||
// ---------------- 事件类型 ----------------
|
||
|
||
// 事件类型常量(与 notice_rule.event_type / notice_log.event_type 对应)。
|
||
const (
|
||
NoticeEventJobDone = "job_done" // 自动任务执行成功
|
||
NoticeEventJobFail = "job_fail" // 自动任务执行失败
|
||
NoticeEventSecurityAlert = "security_alert" // 服务器安全告警
|
||
NoticeEventRecruitDaily = "recruit_daily" // 招聘公告每日推送
|
||
NoticeEventRecruitCrawl = "recruit_crawl" // 招聘公告抓取结果
|
||
NoticeEventTest = "test" // 手动测试发送
|
||
NoticeEventManual = "manual" // 后台手动触发
|
||
)
|
||
|
||
// eventNames 事件编码 → 中文名称。
|
||
var eventNames = map[string]string{
|
||
NoticeEventJobDone: "任务完成",
|
||
NoticeEventJobFail: "任务失败",
|
||
NoticeEventSecurityAlert: "安全告警",
|
||
NoticeEventRecruitDaily: "招聘每日推送",
|
||
NoticeEventRecruitCrawl: "招聘抓取",
|
||
NoticeEventTest: "测试发送",
|
||
NoticeEventManual: "手动触发",
|
||
}
|
||
|
||
// NoticeEventName 事件编码转中文名称(未知编码原样返回)。
|
||
func NoticeEventName(code string) string {
|
||
if code == "" {
|
||
return ""
|
||
}
|
||
if name, ok := eventNames[code]; ok {
|
||
return name
|
||
}
|
||
return code
|
||
}
|
||
|
||
// NoticeEventOptions 事件选项(前端下拉)。
|
||
func NoticeEventOptions() []NoticeMetaItem {
|
||
return []NoticeMetaItem{
|
||
{Code: NoticeEventJobDone, Name: eventNames[NoticeEventJobDone]},
|
||
{Code: NoticeEventJobFail, Name: eventNames[NoticeEventJobFail]},
|
||
{Code: NoticeEventSecurityAlert, Name: eventNames[NoticeEventSecurityAlert]},
|
||
{Code: NoticeEventRecruitDaily, Name: eventNames[NoticeEventRecruitDaily]},
|
||
{Code: NoticeEventRecruitCrawl, Name: eventNames[NoticeEventRecruitCrawl]},
|
||
{Code: NoticeEventTest, Name: eventNames[NoticeEventTest]},
|
||
{Code: NoticeEventManual, Name: eventNames[NoticeEventManual]},
|
||
}
|
||
}
|
||
|
||
// ---------------- 通知类型(按事件大类归拢) ----------------
|
||
|
||
// 通知类型编码(由事件类型推导,用于「类型」维度筛选与展示)。
|
||
const (
|
||
NoticeTypeJob = "job" // 任务通知
|
||
NoticeTypeSecurity = "security" // 安全告警
|
||
NoticeTypeRecruit = "recruit" // 招聘推送
|
||
NoticeTypeTest = "test" // 测试通知
|
||
NoticeTypeManual = "manual" // 手动通知
|
||
NoticeTypeSystem = "system" // 其他/系统
|
||
)
|
||
|
||
// typeNames 类型编码 → 中文名称。
|
||
var typeNames = map[string]string{
|
||
NoticeTypeJob: "任务通知",
|
||
NoticeTypeSecurity: "安全告警",
|
||
NoticeTypeRecruit: "招聘推送",
|
||
NoticeTypeTest: "测试通知",
|
||
NoticeTypeManual: "手动通知",
|
||
NoticeTypeSystem: "系统通知",
|
||
}
|
||
|
||
// eventTypes 事件编码 → 归属类型编码。
|
||
var eventTypes = map[string]string{
|
||
NoticeEventJobDone: NoticeTypeJob,
|
||
NoticeEventJobFail: NoticeTypeJob,
|
||
NoticeEventSecurityAlert: NoticeTypeSecurity,
|
||
NoticeEventRecruitDaily: NoticeTypeRecruit,
|
||
NoticeEventRecruitCrawl: NoticeTypeRecruit,
|
||
NoticeEventTest: NoticeTypeTest,
|
||
NoticeEventManual: NoticeTypeManual,
|
||
}
|
||
|
||
// NoticeTypeName 类型编码转中文名称(未知编码原样返回)。
|
||
func NoticeTypeName(code string) string {
|
||
if code == "" {
|
||
return ""
|
||
}
|
||
if name, ok := typeNames[code]; ok {
|
||
return name
|
||
}
|
||
return code
|
||
}
|
||
|
||
// NoticeEventToType 由事件编码推导类型编码(未知归入 system)。
|
||
func NoticeEventToType(eventType string) string {
|
||
if t, ok := eventTypes[eventType]; ok {
|
||
return t
|
||
}
|
||
return NoticeTypeSystem
|
||
}
|
||
|
||
// NoticeTypeOptions 类型选项(前端下拉)。
|
||
func NoticeTypeOptions() []NoticeMetaItem {
|
||
return []NoticeMetaItem{
|
||
{Code: NoticeTypeJob, Name: typeNames[NoticeTypeJob]},
|
||
{Code: NoticeTypeSecurity, Name: typeNames[NoticeTypeSecurity]},
|
||
{Code: NoticeTypeRecruit, Name: typeNames[NoticeTypeRecruit]},
|
||
{Code: NoticeTypeTest, Name: typeNames[NoticeTypeTest]},
|
||
{Code: NoticeTypeManual, Name: typeNames[NoticeTypeManual]},
|
||
{Code: NoticeTypeSystem, Name: typeNames[NoticeTypeSystem]},
|
||
}
|
||
}
|
||
|
||
// NoticeTypeEventCodes 类型编码 → 该类型下所有事件编码(用于按类型筛选日志)。
|
||
// 注意:system 为兜底类型,无固定事件列表,需配合 KnownEventCodes 反向排除。
|
||
func NoticeTypeEventCodes(noticeType string) []string {
|
||
if noticeType == "" {
|
||
return nil
|
||
}
|
||
out := make([]string, 0, len(eventTypes))
|
||
for event, t := range eventTypes {
|
||
if t == noticeType {
|
||
out = append(out, event)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---------------- 通知分组 ----------------
|
||
|
||
// 分组编码(业务归属分组,用于「分组」维度筛选与展示)。
|
||
const (
|
||
NoticeGroupAutoJob = "auto_job" // 自动任务
|
||
NoticeGroupServer = "server" // 服务器
|
||
NoticeGroupRecruitment = "recruitment" // 招聘
|
||
NoticeGroupSystem = "system" // 系统
|
||
)
|
||
|
||
// groupNames 分组编码 → 中文名称。
|
||
var groupNames = map[string]string{
|
||
NoticeGroupAutoJob: "自动任务",
|
||
NoticeGroupServer: "服务器监控",
|
||
NoticeGroupRecruitment: "招聘中心",
|
||
NoticeGroupSystem: "系统管理",
|
||
}
|
||
|
||
// groupTypes 分组编码 → 包含的通知类型。
|
||
var groupTypes = map[string]string{
|
||
NoticeGroupAutoJob: NoticeTypeJob,
|
||
NoticeGroupServer: NoticeTypeSecurity,
|
||
NoticeGroupRecruitment: NoticeTypeRecruit,
|
||
NoticeGroupSystem: NoticeTypeSystem,
|
||
}
|
||
|
||
// NoticeGroupName 分组编码转中文名称(未知编码原样返回)。
|
||
func NoticeGroupName(code string) string {
|
||
if code == "" {
|
||
return ""
|
||
}
|
||
if name, ok := groupNames[code]; ok {
|
||
return name
|
||
}
|
||
return code
|
||
}
|
||
|
||
// NoticeTypeToGroup 由类型编码推导分组编码。
|
||
func NoticeTypeToGroup(noticeType string) string {
|
||
switch noticeType {
|
||
case NoticeTypeJob:
|
||
return NoticeGroupAutoJob
|
||
case NoticeTypeSecurity:
|
||
return NoticeGroupServer
|
||
case NoticeTypeRecruit:
|
||
return NoticeGroupRecruitment
|
||
}
|
||
return NoticeGroupSystem
|
||
}
|
||
|
||
// NoticeEventToGroup 由事件编码推导分组编码。
|
||
func NoticeEventToGroup(eventType string) string {
|
||
return NoticeTypeToGroup(NoticeEventToType(eventType))
|
||
}
|
||
|
||
// NoticeGroupOptions 分组选项(前端下拉)。
|
||
func NoticeGroupOptions() []NoticeMetaItem {
|
||
return []NoticeMetaItem{
|
||
{Code: NoticeGroupAutoJob, Name: groupNames[NoticeGroupAutoJob]},
|
||
{Code: NoticeGroupServer, Name: groupNames[NoticeGroupServer]},
|
||
{Code: NoticeGroupRecruitment, Name: groupNames[NoticeGroupRecruitment]},
|
||
{Code: NoticeGroupSystem, Name: groupNames[NoticeGroupSystem]},
|
||
}
|
||
}
|
||
|
||
// NoticeGroupEventCodes 分组编码 → 该分组下所有事件编码(用于按分组筛选日志)。
|
||
func NoticeGroupEventCodes(group string) []string {
|
||
if group == "" {
|
||
return nil
|
||
}
|
||
out := make([]string, 0, len(eventTypes))
|
||
for event := range eventTypes {
|
||
if NoticeEventToGroup(event) == group {
|
||
out = append(out, event)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// KnownEventCodes 字典中已登记的全部事件编码(用于「系统通知」等兜底维度的反向排除)。
|
||
func KnownEventCodes() []string {
|
||
out := make([]string, 0, len(eventTypes))
|
||
for event := range eventTypes {
|
||
out = append(out, event)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---------------- 发送状态 ----------------
|
||
|
||
// 发送状态常量(notice_log.status)。
|
||
const (
|
||
NoticeStatusPending = 0 // 待发送(已入队)
|
||
NoticeStatusSuccess = 1 // 成功
|
||
NoticeStatusFailed = 2 // 失败
|
||
)
|
||
|
||
// NoticeStatusName 发送状态码转中文名称。
|
||
func NoticeStatusName(status int) string {
|
||
switch status {
|
||
case NoticeStatusPending:
|
||
return "待发送"
|
||
case NoticeStatusSuccess:
|
||
return "成功"
|
||
case NoticeStatusFailed:
|
||
return "失败"
|
||
}
|
||
return "未知"
|
||
}
|
||
|
||
// ---------------- 统计 ----------------
|
||
|
||
// NoticeLogStats 通知历史记录统计(列表页顶部概览)。
|
||
type NoticeLogStats struct {
|
||
Total int `json:"total"` // 总记录数(按当前筛选条件)
|
||
Success int `json:"success"` // 成功数
|
||
Failed int `json:"failed"` // 失败数
|
||
SuccessRate int `json:"successRate"` // 成功率(百分比,四舍五入)
|
||
}
|
||
|
||
// NoticeLogStatsFilter 统计入参:与列表筛选保持一致(仅去掉分页维度)。
|
||
// DateField 为空时按 created_at 统计。
|
||
type NoticeLogStatsFilter struct {
|
||
Keyword string
|
||
NoticeType string
|
||
Group string
|
||
EventType string
|
||
ChannelCode string
|
||
Status int
|
||
DateFrom string
|
||
DateTo string
|
||
}
|