这两批改动此前一直压在工作区未提交,本次一并归档。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,
生产库需手动导入。
178 lines
6.6 KiB
Go
178 lines
6.6 KiB
Go
// 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, stats, err := notice.Notice().LogList(ctx, dto.NoticeLogFilter{
|
|
Page: req.Page, Size: req.Size, Keyword: req.Keyword,
|
|
NoticeType: req.NoticeType, Group: req.Group, EventType: req.EventType,
|
|
ChannelCode: req.ChannelCode, UserId: req.UserId, BatchId: req.BatchId,
|
|
Status: req.Status, Result: req.Result,
|
|
DateFrom: req.DateFrom, DateTo: req.DateTo,
|
|
OrderBy: req.OrderBy, OrderDir: req.OrderDir,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]*noticev1.NoticeLogItem, 0, len(list))
|
|
for i := range list {
|
|
out = append(out, toLogItem(&list[i]))
|
|
}
|
|
res = ¬icev1.NoticeLogListRes{List: out, Total: total}
|
|
if stats != nil {
|
|
res.Stats = ¬icev1.NoticeLogStatsItem{
|
|
Total: stats.Total, Success: stats.Success,
|
|
Failed: stats.Failed, SuccessRate: stats.SuccessRate,
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// LogDetail 通知历史记录详情。
|
|
func (c *Controller) LogDetail(ctx context.Context, req *noticev1.NoticeLogDetailReq) (res *noticev1.NoticeLogDetailRes, err error) {
|
|
vo, err := notice.Notice().LogDetail(ctx, req.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ¬icev1.NoticeLogDetailRes{Item: toLogItem(vo)}, nil
|
|
}
|
|
|
|
// LogDelete 删除通知历史记录(支持批量)。
|
|
func (c *Controller) LogDelete(ctx context.Context, req *noticev1.NoticeLogDeleteReq) (res *noticev1.NoticeLogDeleteRes, err error) {
|
|
n, err := notice.Notice().LogDelete(ctx, req.Ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ¬icev1.NoticeLogDeleteRes{Deleted: n}, nil
|
|
}
|
|
|
|
// LogClear 清空通知历史记录。
|
|
func (c *Controller) LogClear(ctx context.Context, req *noticev1.NoticeLogClearReq) (res *noticev1.NoticeLogClearRes, err error) {
|
|
n, err := notice.Notice().LogClear(ctx, req.KeepDays, req.DateFrom, req.DateTo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ¬icev1.NoticeLogClearRes{Deleted: n}, nil
|
|
}
|
|
|
|
// MetaOptions 通知字典选项(渠道/事件/类型/分组)。
|
|
func (c *Controller) MetaOptions(ctx context.Context, req *noticev1.NoticeMetaOptionsReq) (res *noticev1.NoticeMetaOptionsRes, err error) {
|
|
return ¬icev1.NoticeMetaOptionsRes{
|
|
Channels: toMetaItems(dto.NoticeChannelOptions()),
|
|
Events: toMetaItems(dto.NoticeEventOptions()),
|
|
Types: toMetaItems(dto.NoticeTypeOptions()),
|
|
Groups: toMetaItems(dto.NoticeGroupOptions()),
|
|
}, 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
|
|
}
|
|
|
|
// toLogItem dto → API 契约转换。
|
|
func toLogItem(v *dto.NoticeLogVO) *noticev1.NoticeLogItem {
|
|
return ¬icev1.NoticeLogItem{
|
|
Id: v.Id, BatchId: v.BatchId, RuleId: v.RuleId, RuleName: v.RuleName,
|
|
EventType: v.EventType, EventName: v.EventName,
|
|
NoticeType: v.NoticeType, TypeName: v.TypeName,
|
|
Group: v.Group, GroupName: v.GroupName,
|
|
ChannelCode: v.ChannelCode, ChannelName: v.ChannelName,
|
|
UserId: v.UserId, UserName: v.UserName, Target: v.Target,
|
|
Title: v.Title, Body: v.Body,
|
|
Status: v.Status, StatusName: v.StatusName,
|
|
Result: v.Result, Error: v.Error,
|
|
RetryCount: v.RetryCount, DurationMs: v.DurationMs,
|
|
Source: v.Source, Remark: v.Remark, CreatedAt: v.CreatedAt,
|
|
}
|
|
}
|
|
|
|
// toMetaItems dto 字典项 → API 契约转换。
|
|
func toMetaItems(list []dto.NoticeMetaItem) []*noticev1.NoticeMetaItem {
|
|
out := make([]*noticev1.NoticeMetaItem, 0, len(list))
|
|
for i := range list {
|
|
out = append(out, ¬icev1.NoticeMetaItem{Code: list[i].Code, Name: list[i].Name})
|
|
}
|
|
return out
|
|
}
|