service.xpcool.com/internal/model/dto/notice_meta.go
夏犀麟 ad4567fc01 feat: 招聘爬虫健壮性 + 通知记录增强(归档工作区留存改动)
这两批改动此前一直压在工作区未提交,本次一并归档。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,
生产库需手动导入。
2026-09-14 01:22:39 +08:00

313 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package dto 通知模块的枚举字典:渠道 / 事件 / 类型 / 分组。
// 说明:通知历史记录需要展示「渠道、类型、分组」等中文名称,
// 这里集中维护编码→名称映射,前端下拉选项也由 OptionList 统一下发,避免前后端各写一份。
package dto
// NoticeMetaItem 通知字典项(编码 + 名称)。
type NoticeMetaItem struct {
Code string `json:"code"`
Name string `json:"name"`
}
// ---------------- 渠道 ----------------
// 渠道编码常量(与 notice_channel.code 对应)。
const (
NoticeChannelBark = "bark" // BarkiOS 推送)
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
}