service.xpcool.com/internal/service/recruitment/bark.go
夏犀麟 ea50261cb8
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 35s
feat(house): 扩充看房数据并增强服务器日志功能
- 新增新房预售证表 house_presale 存储预售许可信息
- 为 house_community 表添加 avg_price 字段存储小区参考均价
- 增强服务器操作审计日志功能,新增管理员账号、IP归属地、错误信息、UA等字段
- 实现纯Go版IP归属地离线解析器,无外部依赖,支持二分查找
- 优化看房列表查询逻辑,修复Fields设置位置导致的SQL语法错误
- 集成招聘模块,添加独立数据库配置和Bark推送服务支持
- 重构日志查询接口,支持多维度筛选和综合分页列表展示
- 更新DAO实体结构同步数据库表结构调整
2026-08-27 01:47:32 +08:00

177 lines
5.4 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 recruitment
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"service.xpcool.com/internal/dao"
"service.xpcool.com/internal/model/do"
"service.xpcool.com/internal/model/dto"
"service.xpcool.com/internal/model/entity"
)
// barkPayload 对应自建 Bark 服务的 /push 接口入参。
type barkPayload struct {
DeviceKey string `json:"device_key"`
Title string `json:"title"`
Body string `json:"body"`
Group string `json:"group"` // 通知分组
Level string `json:"level"` // 优先级
URL string `json:"url"` // 点击跳转
}
// defaultDeviceKey 读取环境变量注入的兜底设备密钥BARK_DEVICE_KEY -> bark.deviceKey
// 当数据库没有任何订阅记录时,推送与测试推送回退到该密钥,做到「填了 env 即可推送」。
func defaultDeviceKey(ctx context.Context) string {
return g.Cfg().MustGet(ctx, "bark.deviceKey", "").String()
}
// defaultSubscription 构造一个使用兜底密钥的默认订阅(全地区/全分类/仅新公告)。
func defaultSubscription(ctx context.Context) *entity.PushSubscription {
key := defaultDeviceKey(ctx)
if key == "" {
return nil
}
return &entity.PushSubscription{
Id: 0, Name: "默认(Bark环境变量)", DeviceKey: key,
Regions: "[]", Categories: "[]", OnlyNew: 1, PushTime: "08:00", Enabled: 1,
}
}
// barkPush 向指定设备密钥发送 Bark 推送自建服务POST {baseUrl}/push
func barkPush(ctx context.Context, deviceKey, title, body string) (bool, string, error) {
baseURL := g.Cfg().MustGet(ctx, "bark.baseUrl", "").String()
if baseURL == "" {
return false, "未配置 bark.baseUrl", fmt.Errorf("bark baseUrl empty")
}
if deviceKey == "" {
// 未显式传入则回退到环境变量兜底密钥。
deviceKey = defaultDeviceKey(ctx)
}
if deviceKey == "" {
return false, "设备密钥为空", fmt.Errorf("device key empty")
}
payload := barkPayload{
DeviceKey: deviceKey,
Title: title,
Body: body,
Group: "recruit_daily",
Level: "active",
}
client := g.Client()
client.SetTimeout(10 * time.Second)
resp, err := client.Post(ctx, strings.TrimRight(baseURL, "/")+"/push", payload)
if err != nil {
return false, err.Error(), err
}
defer resp.Close()
var out struct {
Code int `json:"code"`
Message string `json:"message"`
}
_ = json.Unmarshal(resp.ReadAll(), &out)
ok := resp.StatusCode == 200 && out.Code == 200
msg := out.Message
if msg == "" {
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
return ok, msg, nil
}
// sendDailyDigest 对所有启用订阅发送「招聘早报」汇总(自建 Bark
// 若库内无任何订阅则回退到环境变量兜底密钥BARK_DEVICE_KEY发一份全量早报。
func sendDailyDigest(ctx context.Context) {
var subs []entity.PushSubscription
if err := dao.PushSubscription.Ctx(ctx).Where("enabled", 1).Scan(&subs); err != nil {
g.Log().Errorf(ctx, "load subscriptions failed: %v", err)
return
}
if len(subs) == 0 {
def := defaultSubscription(ctx)
if def == nil {
g.Log().Infof(ctx, "no subscription and no default bark device key, skip digest")
return
}
subs = []entity.PushSubscription{*def}
}
for _, sub := range subs {
title, body := buildDigest(ctx, sub)
if title == "" && body == "" {
// 仅推送新公告且今日无新:跳过,避免打扰。
continue
}
ok, msg, _ := barkPush(ctx, sub.DeviceKey, title, body)
recordPushLog(ctx, sub.Id, title, body, ok, msg)
}
}
// buildDigest 按订阅过滤条件生成早报内容;仅推送新公告且无新时返回空串。
func buildDigest(ctx context.Context, sub entity.PushSubscription) (string, string) {
regions := parseJSONStrings(sub.Regions)
cats := parseJSONInts(sub.Categories)
since := gtime.Now().StartOfDay().String()
m := dao.RecruitmentInfo.Ctx(ctx).Where("status", g.Slice{0, 1}).Where("created_at >= ?", since)
if len(regions) > 0 {
m = m.Where("region IN (?)", regions)
}
if len(cats) > 0 {
m = m.Where("category IN (?)", cats)
}
var list []dto.RecruitmentInfoVO
if err := m.Fields("title, category, region, deadline").OrderDesc("publish_date").Scan(&list); err != nil {
g.Log().Errorf(ctx, "build digest query failed: %v", err)
return "", ""
}
if sub.OnlyNew == 1 && len(list) == 0 {
return "", "" // 无新公告则跳过
}
counts := map[int]int{}
for _, v := range list {
counts[v.Category]++
}
var sb strings.Builder
sb.WriteString("今日新增:")
parts := make([]string, 0, len(counts))
for c, n := range counts {
parts = append(parts, dto.CategoryName(c)+" "+itoa(n))
}
sb.WriteString(strings.Join(parts, " · "))
sb.WriteString("\n")
limit := 8
for i, v := range list {
if i >= limit {
break
}
line := "· " + v.Title
if v.Deadline != "" {
line += "(截止 " + v.Deadline + ""
}
sb.WriteString(line + "\n")
}
title := "贵州招聘早报 · " + gtime.Now().Format("Y-m-d")
if len(regions) == 1 {
title += " · " + regions[0]
}
return title, strings.TrimRight(sb.String(), "\n")
}
// recordPushLog 写推送记录。
func recordPushLog(ctx context.Context, subId uint64, title, body string, ok bool, msg string) {
res := 0
if ok {
res = 1
}
if _, err := dao.PushLog.Ctx(ctx).Data(do.PushLog{
SubscriptionId: subId, PushAt: gtime.Now(), Title: title, Body: body,
Result: res, Error: msg, CreatedAt: gtime.Now(),
}).Insert(); err != nil {
g.Log().Errorf(ctx, "write push log failed: %v", err)
}
}