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