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) } }