CHG: house transaction service 层 + recruitment 招聘模块 + 012 菜单 SQL(补提交未推送代码)
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 14s
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 14s
This commit is contained in:
parent
a2f60c3410
commit
4445b8e8bf
191
api/recruitment/v1/recruitment.go
Normal file
191
api/recruitment/v1/recruitment.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
// Package recruitment_v1 招聘考试聚合模块接口契约。
|
||||||
|
// 规范(2026-08-27):全部 POST;URL 不含任何参数(查询/路径参数均禁止);入参一律走 body。
|
||||||
|
package recruitment_v1
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
// ---------- 公告列表 ----------
|
||||||
|
type RecruitmentListReq struct {
|
||||||
|
g.Meta `path:"/recruitment/info/list" method:"post" tags:"Admin/Recruitment/Info" summary:"招聘公告列表(多维筛选)"`
|
||||||
|
Page int `json:"page" d:"1" v:"min:1"`
|
||||||
|
Size int `json:"size" d:"10" v:"min:1|max:100"`
|
||||||
|
Region string `json:"region"` // 地区过滤(空=全部)
|
||||||
|
Category int `json:"category"` // 分类(0=全部)
|
||||||
|
Keyword string `json:"keyword"` // 标题/正文/单位关键字
|
||||||
|
Status int `json:"status"` // 状态(0=有效+已更正;传 2 只看已失效等)
|
||||||
|
DateFrom string `json:"dateFrom"` // 发布日期起点 YYYY-MM-DD
|
||||||
|
DateTo string `json:"dateTo"` // 发布日期终点 YYYY-MM-DD
|
||||||
|
OrgName string `json:"orgName"` // 发布主体名称模糊匹配
|
||||||
|
SourceId uint64 `json:"sourceId"` // 指定数据源
|
||||||
|
OnlyNewToday bool `json:"onlyNewToday"` // 仅当日新增
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentItem struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
SourceId uint64 `json:"sourceId"`
|
||||||
|
SourceName string `json:"sourceName"`
|
||||||
|
OrgId uint64 `json:"orgId"`
|
||||||
|
OrgName string `json:"orgName"`
|
||||||
|
Category int `json:"category"`
|
||||||
|
CategoryName string `json:"categoryName"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
PublishDate string `json:"publishDate"`
|
||||||
|
Deadline string `json:"deadline"`
|
||||||
|
ExamDate string `json:"examDate"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Attachments string `json:"attachments"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
StatusName string `json:"statusName"`
|
||||||
|
GroupKey string `json:"groupKey"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentListRes struct {
|
||||||
|
List []*RecruitmentItem `json:"list"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 公告详情 ----------
|
||||||
|
type RecruitmentDetailReq struct {
|
||||||
|
g.Meta `path:"/recruitment/info/detail" method:"post" tags:"Admin/Recruitment/Info" summary:"招聘公告详情"`
|
||||||
|
Id uint64 `json:"id" v:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentDetailRes struct {
|
||||||
|
Info *RecruitmentItem `json:"info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 看板统计 ----------
|
||||||
|
type RecruitmentStatsReq struct {
|
||||||
|
g.Meta `path:"/recruitment/stats" method:"post" tags:"Admin/Recruitment/Dashboard" summary:"招聘数据看板统计"`
|
||||||
|
Region string `json:"region"` // 可选:按地区过滤统计
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentStatsRes struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
TodayNew int `json:"todayNew"`
|
||||||
|
WeekNew int `json:"weekNew"`
|
||||||
|
ByCategory []CategoryAggItem `json:"byCategory"`
|
||||||
|
ByRegion []RegionAggItem `json:"byRegion"`
|
||||||
|
RecentTrend []TrendPointItem `json:"recentTrend"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryAggItem struct {
|
||||||
|
Category int `json:"category"`
|
||||||
|
CategoryName string `json:"categoryName"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionAggItem struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrendPointItem struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 趋势分析 ----------
|
||||||
|
type RecruitmentTrendReq struct {
|
||||||
|
g.Meta `path:"/recruitment/trend" method:"post" tags:"Admin/Recruitment/Dashboard" summary:"招聘公告趋势(按日)"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Category int `json:"category"`
|
||||||
|
Days int `json:"days" d:"30" v:"min:1|max:365"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentTrendRes struct {
|
||||||
|
Trend []TrendPointItem `json:"trend"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 数据源状态 ----------
|
||||||
|
type RecruitmentSourcesReq struct {
|
||||||
|
g.Meta `path:"/recruitment/source/list" method:"post" tags:"Admin/Recruitment/Crawler" summary:"数据源列表与运行状态"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceItem struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
BaseUrl string `json:"baseUrl"`
|
||||||
|
SourceType int `json:"sourceType"`
|
||||||
|
Category int `json:"category"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Enabled int `json:"enabled"`
|
||||||
|
LastSuccessAt string `json:"lastSuccessAt"`
|
||||||
|
FailCount int `json:"failCount"`
|
||||||
|
LastSummary string `json:"lastSummary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentSourcesRes struct {
|
||||||
|
List []*SourceItem `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 手动触发抓取 ----------
|
||||||
|
type RecruitmentTriggerReq struct {
|
||||||
|
g.Meta `path:"/recruitment/crawl/trigger" method:"post" tags:"Admin/Recruitment/Crawler" summary:"手动触发抓取(单源或全量)"`
|
||||||
|
SourceId uint64 `json:"sourceId"` // 0=全部启用源
|
||||||
|
Force bool `json:"force"` // 是否强制全量回溯
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentTriggerRes struct {
|
||||||
|
Triggered int `json:"triggered"` // 触发的源数量
|
||||||
|
Summary string `json:"summary"` // 运行摘要
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Bark 推送测试 ----------
|
||||||
|
type RecruitmentPushTestReq struct {
|
||||||
|
g.Meta `path:"/recruitment/push/test" method:"post" tags:"Admin/Recruitment/Push" summary:"Bark 推送测试"`
|
||||||
|
SubscriptionId uint64 `json:"subscriptionId"` // 0=使用首个启用订阅
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentPushTestRes struct {
|
||||||
|
Result bool `json:"result"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 推送订阅管理 ----------
|
||||||
|
type RecruitmentSubscriptionListReq struct {
|
||||||
|
g.Meta `path:"/recruitment/subscription/list" method:"post" tags:"Admin/Recruitment/Push" summary:"推送订阅列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionItem struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DeviceKey string `json:"deviceKey"`
|
||||||
|
Regions []string `json:"regions"`
|
||||||
|
Categories []int `json:"categories"`
|
||||||
|
OnlyNew int `json:"onlyNew"`
|
||||||
|
PushTime string `json:"pushTime"`
|
||||||
|
Enabled int `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentSubscriptionListRes struct {
|
||||||
|
List []*SubscriptionItem `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentSubscriptionSaveReq struct {
|
||||||
|
g.Meta `path:"/recruitment/subscription/save" method:"post" tags:"Admin/Recruitment/Push" summary:"保存推送订阅(新增/更新)"`
|
||||||
|
Id uint64 `json:"id"` // 0=新增
|
||||||
|
Name string `json:"name" v:"required"`
|
||||||
|
DeviceKey string `json:"deviceKey" v:"required"`
|
||||||
|
Regions []string `json:"regions"`
|
||||||
|
Categories []int `json:"categories"`
|
||||||
|
OnlyNew int `json:"onlyNew" d:"1"`
|
||||||
|
PushTime string `json:"pushTime" d:"08:00"`
|
||||||
|
Enabled int `json:"enabled" d:"1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentSubscriptionSaveRes struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentSubscriptionDeleteReq struct {
|
||||||
|
g.Meta `path:"/recruitment/subscription/delete" method:"post" tags:"Admin/Recruitment/Push" summary:"删除推送订阅"`
|
||||||
|
Id uint64 `json:"id" v:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecruitmentSubscriptionDeleteRes struct{}
|
||||||
8
internal/controller/recruitment/controller.go
Normal file
8
internal/controller/recruitment/controller.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Package recruitment 实现招聘考试聚合模块管理端点,绑定在 admin 受权限保护分组下。
|
||||||
|
package recruitment
|
||||||
|
|
||||||
|
// Controller 实现招聘考试聚合模块的所有端点。
|
||||||
|
type Controller struct{}
|
||||||
|
|
||||||
|
// New 创建招聘模块控制器。
|
||||||
|
func New() *Controller { return &Controller{} }
|
||||||
159
internal/controller/recruitment/recruitment.go
Normal file
159
internal/controller/recruitment/recruitment.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package recruitment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
recruitmentv1 "service.xpcool.com/api/recruitment/v1"
|
||||||
|
"service.xpcool.com/internal/model/dto"
|
||||||
|
"service.xpcool.com/internal/service/recruitment"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toItem 将服务层 VO 映射为 API 出参项。
|
||||||
|
func toItem(v *dto.RecruitmentInfoVO) *recruitmentv1.RecruitmentItem {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentItem{
|
||||||
|
Id: v.Id, Title: v.Title, SourceId: v.SourceId, SourceName: v.SourceName,
|
||||||
|
OrgId: v.OrgId, OrgName: v.OrgName, Category: v.Category, CategoryName: v.CategoryName,
|
||||||
|
Region: v.Region, PublishDate: v.PublishDate, Deadline: v.Deadline, ExamDate: v.ExamDate,
|
||||||
|
Url: v.Url, Content: v.Content, Attachments: v.Attachments, Status: v.Status,
|
||||||
|
StatusName: v.StatusName, GroupKey: v.GroupKey, CreatedAt: v.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoList 招聘公告列表(多维筛选)。
|
||||||
|
func (c *Controller) InfoList(ctx context.Context, req *recruitmentv1.RecruitmentListReq) (res *recruitmentv1.RecruitmentListRes, err error) {
|
||||||
|
list, total, err := recruitment.Recruitment().List(ctx, dto.RecruitmentFilter{
|
||||||
|
Page: req.Page, Size: req.Size, Region: req.Region, Category: req.Category,
|
||||||
|
Keyword: req.Keyword, Status: req.Status, DateFrom: req.DateFrom, DateTo: req.DateTo,
|
||||||
|
OrgName: req.OrgName, SourceId: req.SourceId, OnlyNewToday: req.OnlyNewToday,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]*recruitmentv1.RecruitmentItem, 0, len(list))
|
||||||
|
for i := range list {
|
||||||
|
out = append(out, toItem(&list[i]))
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentListRes{List: out, Total: total}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoDetail 招聘公告详情。
|
||||||
|
func (c *Controller) InfoDetail(ctx context.Context, req *recruitmentv1.RecruitmentDetailReq) (res *recruitmentv1.RecruitmentDetailRes, err error) {
|
||||||
|
info, err := recruitment.Recruitment().Detail(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentDetailRes{Info: toItem(info)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats 招聘数据看板统计。
|
||||||
|
func (c *Controller) Stats(ctx context.Context, req *recruitmentv1.RecruitmentStatsReq) (res *recruitmentv1.RecruitmentStatsRes, err error) {
|
||||||
|
s, err := recruitment.Recruitment().Stats(ctx, req.Region)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
byCat := make([]recruitmentv1.CategoryAggItem, 0, len(s.ByCategory))
|
||||||
|
for _, v := range s.ByCategory {
|
||||||
|
byCat = append(byCat, recruitmentv1.CategoryAggItem{Category: v.Category, CategoryName: v.CategoryName, Count: v.Count})
|
||||||
|
}
|
||||||
|
byRegion := make([]recruitmentv1.RegionAggItem, 0, len(s.ByRegion))
|
||||||
|
for _, v := range s.ByRegion {
|
||||||
|
byRegion = append(byRegion, recruitmentv1.RegionAggItem{Region: v.Region, Count: v.Count})
|
||||||
|
}
|
||||||
|
trend := make([]recruitmentv1.TrendPointItem, 0, len(s.RecentTrend))
|
||||||
|
for _, v := range s.RecentTrend {
|
||||||
|
trend = append(trend, recruitmentv1.TrendPointItem{Date: v.Date, Count: v.Count})
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentStatsRes{
|
||||||
|
Total: s.Total, TodayNew: s.TodayNew, WeekNew: s.WeekNew,
|
||||||
|
ByCategory: byCat, ByRegion: byRegion, RecentTrend: trend,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trend 招聘公告趋势(按日)。
|
||||||
|
func (c *Controller) Trend(ctx context.Context, req *recruitmentv1.RecruitmentTrendReq) (res *recruitmentv1.RecruitmentTrendRes, err error) {
|
||||||
|
pts, err := recruitment.Recruitment().Trend(ctx, req.Region, req.Category, req.Days)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]recruitmentv1.TrendPointItem, 0, len(pts))
|
||||||
|
for _, v := range pts {
|
||||||
|
out = append(out, recruitmentv1.TrendPointItem{Date: v.Date, Count: v.Count})
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentTrendRes{Trend: out}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceList 数据源列表与运行状态。
|
||||||
|
func (c *Controller) SourceList(ctx context.Context, req *recruitmentv1.RecruitmentSourcesReq) (res *recruitmentv1.RecruitmentSourcesRes, err error) {
|
||||||
|
list, err := recruitment.Recruitment().Sources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]*recruitmentv1.SourceItem, 0, len(list))
|
||||||
|
for i := range list {
|
||||||
|
v := &list[i]
|
||||||
|
out = append(out, &recruitmentv1.SourceItem{
|
||||||
|
Id: v.Id, Name: v.Name, BaseUrl: v.BaseUrl, SourceType: v.SourceType,
|
||||||
|
Category: v.Category, Region: v.Region, Enabled: v.Enabled,
|
||||||
|
LastSuccessAt: v.LastSuccessAt, FailCount: v.FailCount, LastSummary: v.LastSummary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentSourcesRes{List: out}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrawlTrigger 手动触发抓取(单源或全量)。
|
||||||
|
func (c *Controller) CrawlTrigger(ctx context.Context, req *recruitmentv1.RecruitmentTriggerReq) (res *recruitmentv1.RecruitmentTriggerRes, err error) {
|
||||||
|
n, summary, err := recruitment.Recruitment().Trigger(ctx, req.SourceId, req.Force)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentTriggerRes{Triggered: n, Summary: summary}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushTest Bark 推送测试。
|
||||||
|
func (c *Controller) PushTest(ctx context.Context, req *recruitmentv1.RecruitmentPushTestReq) (res *recruitmentv1.RecruitmentPushTestRes, err error) {
|
||||||
|
ok, msg, err := recruitment.Recruitment().PushTest(ctx, req.SubscriptionId, req.Title, req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentPushTestRes{Result: ok, Message: msg}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionList 推送订阅列表。
|
||||||
|
func (c *Controller) SubscriptionList(ctx context.Context, req *recruitmentv1.RecruitmentSubscriptionListReq) (res *recruitmentv1.RecruitmentSubscriptionListRes, err error) {
|
||||||
|
list, err := recruitment.Recruitment().SubscriptionList(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]*recruitmentv1.SubscriptionItem, 0, len(list))
|
||||||
|
for i := range list {
|
||||||
|
v := &list[i]
|
||||||
|
out = append(out, &recruitmentv1.SubscriptionItem{
|
||||||
|
Id: v.Id, Name: v.Name, DeviceKey: v.DeviceKey, Regions: v.Regions,
|
||||||
|
Categories: v.Categories, OnlyNew: v.OnlyNew, PushTime: v.PushTime, Enabled: v.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentSubscriptionListRes{List: out}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionSave 保存推送订阅(新增/更新)。
|
||||||
|
func (c *Controller) SubscriptionSave(ctx context.Context, req *recruitmentv1.RecruitmentSubscriptionSaveReq) (res *recruitmentv1.RecruitmentSubscriptionSaveRes, err error) {
|
||||||
|
id, err := recruitment.Recruitment().SubscriptionSave(ctx, dto.SubscriptionInput{
|
||||||
|
Id: req.Id, Name: req.Name, DeviceKey: req.DeviceKey, Regions: req.Regions,
|
||||||
|
Categories: req.Categories, OnlyNew: req.OnlyNew, PushTime: req.PushTime, Enabled: req.Enabled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentSubscriptionSaveRes{Id: id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionDelete 删除推送订阅。
|
||||||
|
func (c *Controller) SubscriptionDelete(ctx context.Context, req *recruitmentv1.RecruitmentSubscriptionDeleteReq) (res *recruitmentv1.RecruitmentSubscriptionDeleteRes, err error) {
|
||||||
|
if err = recruitment.Recruitment().SubscriptionDelete(ctx, req.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &recruitmentv1.RecruitmentSubscriptionDeleteRes{}, nil
|
||||||
|
}
|
||||||
63
internal/dao/recruitment.go
Normal file
63
internal/dao/recruitment.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Package dao 招聘考试聚合模块数据访问层(手写,未跑 gf gen)。
|
||||||
|
// 仅暴露 Ctx(ctx) *gdb.Model,供 service 层链式调用,形态与生成产物一致。
|
||||||
|
// 全部表落在独立数据库 recruitment(由 database.recruitment.link 配置)。
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dbGroup 招聘模块独立数据库分组名,对应 config 中 database.recruitment.link。
|
||||||
|
const dbGroup = "recruitment"
|
||||||
|
|
||||||
|
// model 返回指向指定表的安全 Model。
|
||||||
|
func model(table string) func(ctx context.Context) *gdb.Model {
|
||||||
|
return func(ctx context.Context) *gdb.Model {
|
||||||
|
return g.DB(dbGroup).Model(table).Safe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type recruitmentInfoDao struct{ table string }
|
||||||
|
|
||||||
|
// RecruitmentInfo 公告主表全局访问对象。
|
||||||
|
var RecruitmentInfo = recruitmentInfoDao{table: "recruitment_info"}
|
||||||
|
|
||||||
|
func (d recruitmentInfoDao) Ctx(ctx context.Context) *gdb.Model { return model(d.table)(ctx) }
|
||||||
|
|
||||||
|
type organizationDao struct{ table string }
|
||||||
|
|
||||||
|
// Organization 发布主体表全局访问对象。
|
||||||
|
var Organization = organizationDao{table: "organization"}
|
||||||
|
|
||||||
|
func (d organizationDao) Ctx(ctx context.Context) *gdb.Model { return model(d.table)(ctx) }
|
||||||
|
|
||||||
|
type crawlSourceDao struct{ table string }
|
||||||
|
|
||||||
|
// CrawlSource 数据源表全局访问对象。
|
||||||
|
var CrawlSource = crawlSourceDao{table: "crawl_source"}
|
||||||
|
|
||||||
|
func (d crawlSourceDao) Ctx(ctx context.Context) *gdb.Model { return model(d.table)(ctx) }
|
||||||
|
|
||||||
|
type crawlLogDao struct{ table string }
|
||||||
|
|
||||||
|
// CrawlLog 抓取日志表全局访问对象。
|
||||||
|
var CrawlLog = crawlLogDao{table: "crawl_log"}
|
||||||
|
|
||||||
|
func (d crawlLogDao) Ctx(ctx context.Context) *gdb.Model { return model(d.table)(ctx) }
|
||||||
|
|
||||||
|
type pushSubscriptionDao struct{ table string }
|
||||||
|
|
||||||
|
// PushSubscription 推送订阅表全局访问对象。
|
||||||
|
var PushSubscription = pushSubscriptionDao{table: "push_subscription"}
|
||||||
|
|
||||||
|
func (d pushSubscriptionDao) Ctx(ctx context.Context) *gdb.Model { return model(d.table)(ctx) }
|
||||||
|
|
||||||
|
type pushLogDao struct{ table string }
|
||||||
|
|
||||||
|
// PushLog 推送记录表全局访问对象。
|
||||||
|
var PushLog = pushLogDao{table: "push_log"}
|
||||||
|
|
||||||
|
func (d pushLogDao) Ctx(ctx context.Context) *gdb.Model { return model(d.table)(ctx) }
|
||||||
102
internal/model/do/recruitment.go
Normal file
102
internal/model/do/recruitment.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Package do 定义招聘考试聚合模块的数据对象(手写,未跑 gf gen)。
|
||||||
|
// 字段类型统一用 any,与 gf gen 产物保持一致,便于 .Data()/.Where() 使用。
|
||||||
|
package do
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecruitmentInfo 公告主表 DO。
|
||||||
|
type RecruitmentInfo struct {
|
||||||
|
g.Meta `orm:"table:recruitment_info, do:true"`
|
||||||
|
Id any //
|
||||||
|
Title any // 公告标题
|
||||||
|
SourceId any // 数据源ID
|
||||||
|
OrgId any // 发布主体ID
|
||||||
|
OrgName any // 发布主体名称
|
||||||
|
Category any // 考试分类
|
||||||
|
Region any // 地区
|
||||||
|
PublishDate any // 发布日期
|
||||||
|
Deadline any // 报名/截止日期
|
||||||
|
ExamDate any // 笔试日期
|
||||||
|
Url any // 原文链接
|
||||||
|
Content any // 正文/摘要
|
||||||
|
Attachments any // 附件(JSON)
|
||||||
|
Status any // 0有效 1已更正 2已失效 3已删除
|
||||||
|
Fingerprint any // 去重指纹
|
||||||
|
GroupKey any // 跨源同公告聚合键
|
||||||
|
CreatedAt any //
|
||||||
|
UpdatedAt any //
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization 发布主体 DO。
|
||||||
|
type Organization struct {
|
||||||
|
g.Meta `orm:"table:organization, do:true"`
|
||||||
|
Id any //
|
||||||
|
Name any // 主体名称
|
||||||
|
Type any // 主体类型
|
||||||
|
OfficialSite any // 官网
|
||||||
|
Region any // 地区
|
||||||
|
CreatedAt any //
|
||||||
|
UpdatedAt any //
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrawlSource 数据源 DO。
|
||||||
|
type CrawlSource struct {
|
||||||
|
g.Meta `orm:"table:crawl_source, do:true"`
|
||||||
|
Id any //
|
||||||
|
Name any // 数据源名称
|
||||||
|
BaseUrl any // 站点基础URL
|
||||||
|
SourceType any // 1静态 2SPA 3需登录 4附件
|
||||||
|
Category any // 默认分类
|
||||||
|
Region any // 默认地区
|
||||||
|
ListPath any // 列表路径/接口
|
||||||
|
Config any // 扩展配置(JSON)
|
||||||
|
Enabled any // 是否启用
|
||||||
|
CronExpr any // 调度表达式
|
||||||
|
LastSuccessAt any // 最近成功抓取时间
|
||||||
|
FailCount any // 连续失败次数
|
||||||
|
CreatedAt any //
|
||||||
|
UpdatedAt any //
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrawlLog 抓取日志 DO。
|
||||||
|
type CrawlLog struct {
|
||||||
|
g.Meta `orm:"table:crawl_log, do:true"`
|
||||||
|
Id any //
|
||||||
|
SourceId any // 数据源ID
|
||||||
|
RunAt any // 运行时间
|
||||||
|
Fetched any // 抓取条数
|
||||||
|
NewCount any // 新增条数
|
||||||
|
UpdatedCount any // 更新条数
|
||||||
|
Error any // 错误信息
|
||||||
|
CreatedAt any //
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushSubscription 推送订阅 DO。
|
||||||
|
type PushSubscription struct {
|
||||||
|
g.Meta `orm:"table:push_subscription, do:true"`
|
||||||
|
Id any //
|
||||||
|
Name any // 订阅名称
|
||||||
|
DeviceKey any // Bark 设备密钥
|
||||||
|
Regions any // 订阅地区(JSON)
|
||||||
|
Categories any // 订阅分类(JSON)
|
||||||
|
OnlyNew any // 仅推送新公告
|
||||||
|
PushTime any // 推送时间
|
||||||
|
Enabled any // 是否启用
|
||||||
|
CreatedAt any //
|
||||||
|
UpdatedAt any //
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushLog 推送记录 DO。
|
||||||
|
type PushLog struct {
|
||||||
|
g.Meta `orm:"table:push_log, do:true"`
|
||||||
|
Id any //
|
||||||
|
SubscriptionId any // 订阅ID
|
||||||
|
PushAt any // 推送时间
|
||||||
|
Title any // 推送标题
|
||||||
|
Body any // 推送内容
|
||||||
|
Result any // 1成功 0失败
|
||||||
|
Error any // 错误信息
|
||||||
|
CreatedAt any //
|
||||||
|
}
|
||||||
161
internal/model/dto/recruitment.go
Normal file
161
internal/model/dto/recruitment.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Package dto 招聘考试聚合模块的服务边界对象(入参/出参)。
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// RecruitmentInfoVO 公告列表/详情的出参视图(由 entity 映射,含派生字段)。
|
||||||
|
type RecruitmentInfoVO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
SourceId uint64 `json:"sourceId"`
|
||||||
|
SourceName string `json:"sourceName"` // 派生:数据源名称
|
||||||
|
OrgId uint64 `json:"orgId"`
|
||||||
|
OrgName string `json:"orgName"`
|
||||||
|
Category int `json:"category"`
|
||||||
|
CategoryName string `json:"categoryName"` // 派生:分类中文名
|
||||||
|
Region string `json:"region"`
|
||||||
|
PublishDate string `json:"publishDate"`
|
||||||
|
Deadline string `json:"deadline"`
|
||||||
|
ExamDate string `json:"examDate"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Attachments string `json:"attachments"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
StatusName string `json:"statusName"` // 派生:状态中文名
|
||||||
|
GroupKey string `json:"groupKey"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecruitmentFilter 公告查询筛选条件(列表/看板共用)。
|
||||||
|
type RecruitmentFilter struct {
|
||||||
|
Page int // 页码,从 1 开始
|
||||||
|
Size int // 每页大小
|
||||||
|
Region string // 地区过滤
|
||||||
|
Category int // 分类(0 表示全部)
|
||||||
|
Keyword string // 标题/正文/单位关键字
|
||||||
|
Status int // 状态(0 表示有效+已更正,即未失效)
|
||||||
|
DateFrom string // 发布日期起点 YYYY-MM-DD
|
||||||
|
DateTo string // 发布日期终点 YYYY-MM-DD
|
||||||
|
OrgName string // 发布主体名称模糊匹配
|
||||||
|
SourceId uint64 // 指定数据源
|
||||||
|
OnlyNewToday bool // 仅查当日新增
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecruitmentInput 公告写入入参(爬虫/手动录入共用)。
|
||||||
|
type RecruitmentInput struct {
|
||||||
|
Id uint64 // 更新时必填
|
||||||
|
Title string
|
||||||
|
SourceId uint64
|
||||||
|
OrgId uint64
|
||||||
|
OrgName string
|
||||||
|
Category int
|
||||||
|
Region string
|
||||||
|
PublishDate string
|
||||||
|
Deadline string
|
||||||
|
ExamDate string
|
||||||
|
Url string
|
||||||
|
Content string
|
||||||
|
Attachments string
|
||||||
|
Status int
|
||||||
|
GroupKey string
|
||||||
|
Fingerprint string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryAgg 按分类聚合。
|
||||||
|
type CategoryAgg struct {
|
||||||
|
Category int `json:"category"`
|
||||||
|
CategoryName string `json:"categoryName"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegionAgg 按地区聚合。
|
||||||
|
type RegionAgg struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrendPoint 按日新增趋势点。
|
||||||
|
type TrendPoint struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecruitmentStats 看板统计数据。
|
||||||
|
type RecruitmentStats struct {
|
||||||
|
Total int `json:"total"` // 有效公告总数
|
||||||
|
TodayNew int `json:"todayNew"` // 今日新增
|
||||||
|
WeekNew int `json:"weekNew"` // 近7天新增
|
||||||
|
ByCategory []CategoryAgg `json:"byCategory"`
|
||||||
|
ByRegion []RegionAgg `json:"byRegion"`
|
||||||
|
RecentTrend []TrendPoint `json:"recentTrend"` // 近30天每日新增
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceItem 数据源状态视图。
|
||||||
|
type SourceItem struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
BaseUrl string `json:"baseUrl"`
|
||||||
|
SourceType int `json:"sourceType"`
|
||||||
|
Category int `json:"category"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Enabled int `json:"enabled"`
|
||||||
|
LastSuccessAt string `json:"lastSuccessAt"`
|
||||||
|
FailCount int `json:"failCount"`
|
||||||
|
LastSummary string `json:"lastSummary"` // 最近一次运行摘要
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionItem 推送订阅视图。
|
||||||
|
type SubscriptionItem struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DeviceKey string `json:"deviceKey"`
|
||||||
|
Regions []string `json:"regions"`
|
||||||
|
Categories []int `json:"categories"`
|
||||||
|
OnlyNew int `json:"onlyNew"`
|
||||||
|
PushTime string `json:"pushTime"`
|
||||||
|
Enabled int `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrawlRunResult 单次抓取运行结果(用于写日志与返回)。
|
||||||
|
type CrawlRunResult struct {
|
||||||
|
SourceId uint64
|
||||||
|
Fetched int
|
||||||
|
NewCount int
|
||||||
|
Updated int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionInput 推送订阅写入入参。
|
||||||
|
type SubscriptionInput struct {
|
||||||
|
Id uint64
|
||||||
|
Name string
|
||||||
|
DeviceKey string
|
||||||
|
Regions []string
|
||||||
|
Categories []int
|
||||||
|
OnlyNew int
|
||||||
|
PushTime string
|
||||||
|
Enabled int
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类与状态的中文名映射(派生展示用)。
|
||||||
|
var CategoryNameMap = map[int]string{
|
||||||
|
1: "公务员", 2: "事业单位", 3: "国企", 4: "央企", 5: "私企", 6: "其他",
|
||||||
|
}
|
||||||
|
|
||||||
|
var StatusNameMap = map[int]string{
|
||||||
|
0: "有效", 1: "已更正", 2: "已失效", 3: "已删除",
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryName 返回分类中文名。
|
||||||
|
func CategoryName(c int) string {
|
||||||
|
if v, ok := CategoryNameMap[c]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "其他"
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusName 返回状态中文名。
|
||||||
|
func StatusName(s int) string {
|
||||||
|
if v, ok := StatusNameMap[s]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
92
internal/model/entity/recruitment.go
Normal file
92
internal/model/entity/recruitment.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Package entity 定义招聘考试聚合模块的数据表结构(手写,未跑 gf gen)。
|
||||||
|
// 字段命名对齐 service 现有约定:json 用驼峰,orm 用下划线。
|
||||||
|
package entity
|
||||||
|
|
||||||
|
// RecruitmentInfo 招聘考试公告主表。
|
||||||
|
type RecruitmentInfo struct {
|
||||||
|
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||||
|
Title string `json:"title" orm:"title" description:"公告标题"`
|
||||||
|
SourceId uint64 `json:"sourceId" orm:"source_id" description:"数据源ID"`
|
||||||
|
OrgId uint64 `json:"orgId" orm:"org_id" description:"发布主体ID"`
|
||||||
|
OrgName string `json:"orgName" orm:"org_name" description:"发布主体名称(冗余便于展示)"`
|
||||||
|
Category int `json:"category" orm:"category" description:"考试分类 1公务员 2事业单位 3国企 4央企 5私企 6其他"`
|
||||||
|
Region string `json:"region" orm:"region" description:"地区(贵阳/省直/其他市州)"`
|
||||||
|
PublishDate string `json:"publishDate" orm:"publish_date" description:"发布日期"`
|
||||||
|
Deadline string `json:"deadline" orm:"deadline" description:"报名/截止日期"`
|
||||||
|
ExamDate string `json:"examDate" orm:"exam_date" description:"笔试日期"`
|
||||||
|
Url string `json:"url" orm:"url" description:"原文链接"`
|
||||||
|
Content string `json:"content" orm:"content" description:"正文/摘要"`
|
||||||
|
Attachments string `json:"attachments" orm:"attachments" description:"附件链接与解析结果(JSON)"`
|
||||||
|
Status int `json:"status" orm:"status" description:"0有效 1已更正 2已失效 3已删除"`
|
||||||
|
Fingerprint string `json:"fingerprint" orm:"fingerprint" description:"去重指纹"`
|
||||||
|
GroupKey string `json:"groupKey" orm:"group_key" description:"跨源同公告聚合键"`
|
||||||
|
CreatedAt string `json:"createdAt" orm:"created_at" description:"入库时间"`
|
||||||
|
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization 发布主体(机关/事业单位/国企/央企/私企)。
|
||||||
|
type Organization struct {
|
||||||
|
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||||
|
Name string `json:"name" orm:"name" description:"主体名称"`
|
||||||
|
Type int `json:"type" orm:"type" description:"主体类型 1机关 2事业 3国企 4央企 5私企 6其他"`
|
||||||
|
OfficialSite string `json:"officialSite" orm:"official_site" description:"官网"`
|
||||||
|
Region string `json:"region" orm:"region" description:"地区"`
|
||||||
|
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||||
|
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrawlSource 数据源配置(插件化爬虫适配器)。
|
||||||
|
type CrawlSource struct {
|
||||||
|
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||||
|
Name string `json:"name" orm:"name" description:"数据源名称"`
|
||||||
|
BaseUrl string `json:"baseUrl" orm:"base_url" description:"站点基础URL"`
|
||||||
|
SourceType int `json:"sourceType" orm:"source_type" description:"1静态列表 2SPA 3需登录 4附件型"`
|
||||||
|
Category int `json:"category" orm:"category" description:"该源默认考试分类"`
|
||||||
|
Region string `json:"region" orm:"region" description:"该源默认地区"`
|
||||||
|
ListPath string `json:"listPath" orm:"list_path" description:"列表页路径/接口"`
|
||||||
|
Config string `json:"config" orm:"config" description:"适配器扩展配置(JSON)"`
|
||||||
|
Enabled int `json:"enabled" orm:"enabled" description:"是否启用 0否 1是"`
|
||||||
|
CronExpr string `json:"cronExpr" orm:"cron_expr" description:"调度表达式(可选)"`
|
||||||
|
LastSuccessAt string `json:"lastSuccessAt" orm:"last_success_at" description:"最近成功抓取时间"`
|
||||||
|
FailCount int `json:"failCount" orm:"fail_count" description:"连续失败次数"`
|
||||||
|
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||||
|
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrawlLog 抓取任务运行日志。
|
||||||
|
type CrawlLog struct {
|
||||||
|
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||||
|
SourceId uint64 `json:"sourceId" orm:"source_id" description:"数据源ID"`
|
||||||
|
RunAt string `json:"runAt" orm:"run_at" description:"运行时间"`
|
||||||
|
Fetched int `json:"fetched" orm:"fetched" description:"本次抓取条数"`
|
||||||
|
NewCount int `json:"newCount" orm:"new_count" description:"新增条数"`
|
||||||
|
UpdatedCount int `json:"updatedCount" orm:"updated_count" description:"更新条数"`
|
||||||
|
Error string `json:"error" orm:"error" description:"错误信息"`
|
||||||
|
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushSubscription Bark 推送订阅配置。
|
||||||
|
type PushSubscription struct {
|
||||||
|
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||||
|
Name string `json:"name" orm:"name" description:"订阅名称"`
|
||||||
|
DeviceKey string `json:"deviceKey" orm:"device_key" description:"Bark 设备密钥"`
|
||||||
|
Regions string `json:"regions" orm:"regions" description:"订阅地区(JSON数组)"`
|
||||||
|
Categories string `json:"categories" orm:"categories" description:"订阅分类(JSON数组)"`
|
||||||
|
OnlyNew int `json:"onlyNew" orm:"only_new" description:"仅推送新公告 0否 1是"`
|
||||||
|
PushTime string `json:"pushTime" orm:"push_time" description:"推送时间 HH:mm"`
|
||||||
|
Enabled int `json:"enabled" orm:"enabled" description:"是否启用 0否 1是"`
|
||||||
|
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||||
|
UpdatedAt string `json:"updatedAt" orm:"updated_at" description:"更新时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushLog 推送记录。
|
||||||
|
type PushLog struct {
|
||||||
|
Id uint64 `json:"id" orm:"id" description:"主键"`
|
||||||
|
SubscriptionId uint64 `json:"subscriptionId" orm:"subscription_id" description:"订阅ID"`
|
||||||
|
PushAt string `json:"pushAt" orm:"push_at" description:"推送时间"`
|
||||||
|
Title string `json:"title" orm:"title" description:"推送标题"`
|
||||||
|
Body string `json:"body" orm:"body" description:"推送内容"`
|
||||||
|
Result int `json:"result" orm:"result" description:"1成功 0失败"`
|
||||||
|
Error string `json:"error" orm:"error" description:"错误信息"`
|
||||||
|
CreatedAt string `json:"createdAt" orm:"created_at" description:"创建时间"`
|
||||||
|
}
|
||||||
55
internal/service/house/transaction/transaction.go
Normal file
55
internal/service/house/transaction/transaction.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Package house_transaction 提供成交记录领域服务。
|
||||||
|
package house_transaction
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
|
||||||
|
"service.xpcool.com/internal/dao"
|
||||||
|
"service.xpcool.com/internal/model/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ITransaction 成交记录领域服务接口。
|
||||||
|
type ITransaction interface {
|
||||||
|
List(context.Context, int, int, string, string) ([]dto.HouseTransactionVO, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type transaction struct{}
|
||||||
|
|
||||||
|
var localTransaction ITransaction
|
||||||
|
|
||||||
|
func NewTransaction() ITransaction { return &transaction{} }
|
||||||
|
|
||||||
|
// Transaction 返回已注册的成交服务实现。
|
||||||
|
func Transaction() ITransaction {
|
||||||
|
if localTransaction == nil {
|
||||||
|
panic("Transaction implementation not registered")
|
||||||
|
}
|
||||||
|
return localTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterTransaction 注册成交服务实现。
|
||||||
|
func RegisterTransaction(i ITransaction) { localTransaction = i }
|
||||||
|
|
||||||
|
// List 分页查询成交记录,关联小区名,按成交日期倒序。
|
||||||
|
func (s *transaction) List(ctx context.Context, page, size int, region, keyword string) ([]dto.HouseTransactionVO, int, error) {
|
||||||
|
m := dao.HouseTransaction.Ctx(ctx).As("t").
|
||||||
|
LeftJoin("house_community c", "t.community_id=c.id").
|
||||||
|
Fields("t.*, c.name AS community_name")
|
||||||
|
if region != "" {
|
||||||
|
m = m.Where("c.region", region)
|
||||||
|
}
|
||||||
|
if keyword != "" {
|
||||||
|
m = m.Where("c.name LIKE ? OR t.layout LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||||
|
}
|
||||||
|
total, err := m.Clone().Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "count transaction")
|
||||||
|
}
|
||||||
|
var list []dto.HouseTransactionVO
|
||||||
|
if err = m.Clone().Page(page, size).OrderDesc("t.deal_date").Scan(&list); err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "query transaction list")
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
322
internal/service/recruitment/recruitment.go
Normal file
322
internal/service/recruitment/recruitment.go
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
// Package recruitment 提供招聘考试聚合领域服务(业务/爬虫/调度/推送)。
|
||||||
|
package recruitment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
|
||||||
|
"service.xpcool.com/internal/consts"
|
||||||
|
"service.xpcool.com/internal/dao"
|
||||||
|
"service.xpcool.com/internal/library/response"
|
||||||
|
"service.xpcool.com/internal/model/dto"
|
||||||
|
"service.xpcool.com/internal/model/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IRecruitment 招聘考试聚合领域服务接口。
|
||||||
|
type IRecruitment interface {
|
||||||
|
List(context.Context, dto.RecruitmentFilter) ([]dto.RecruitmentInfoVO, int, error)
|
||||||
|
Detail(context.Context, uint64) (*dto.RecruitmentInfoVO, error)
|
||||||
|
Stats(context.Context, string) (*dto.RecruitmentStats, error)
|
||||||
|
Trend(context.Context, string, int, int) ([]dto.TrendPoint, error)
|
||||||
|
Sources(context.Context) ([]dto.SourceItem, error)
|
||||||
|
Trigger(context.Context, uint64, bool) (int, string, error)
|
||||||
|
PushTest(context.Context, uint64, string, string) (bool, string, error)
|
||||||
|
SubscriptionList(context.Context) ([]dto.SubscriptionItem, error)
|
||||||
|
SubscriptionSave(context.Context, dto.SubscriptionInput) (uint64, error)
|
||||||
|
SubscriptionDelete(context.Context, uint64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type recruitment struct{}
|
||||||
|
|
||||||
|
var localRecruitment IRecruitment
|
||||||
|
|
||||||
|
// New 创建招聘领域服务实现。
|
||||||
|
func New() IRecruitment { return &recruitment{} }
|
||||||
|
|
||||||
|
// Recruitment 返回已注册的招聘服务实现。
|
||||||
|
func Recruitment() IRecruitment {
|
||||||
|
if localRecruitment == nil {
|
||||||
|
panic("Recruitment implementation not registered")
|
||||||
|
}
|
||||||
|
return localRecruitment
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRecruitment 注册招聘服务实现。
|
||||||
|
func RegisterRecruitment(i IRecruitment) { localRecruitment = i }
|
||||||
|
|
||||||
|
// List 分页查询公告,关联数据源名称,支持多维筛选(默认只看未失效)。
|
||||||
|
func (s *recruitment) List(ctx context.Context, f dto.RecruitmentFilter) ([]dto.RecruitmentInfoVO, int, error) {
|
||||||
|
m := dao.RecruitmentInfo.Ctx(ctx).As("r").LeftJoin("crawl_source cs", "r.source_id=cs.id")
|
||||||
|
m = m.Fields("r.*, cs.name AS source_name")
|
||||||
|
if f.Region != "" {
|
||||||
|
m = m.Where("r.region", f.Region)
|
||||||
|
}
|
||||||
|
if f.Category > 0 {
|
||||||
|
m = m.Where("r.category", f.Category)
|
||||||
|
}
|
||||||
|
if f.Keyword != "" {
|
||||||
|
kw := "%" + f.Keyword + "%"
|
||||||
|
m = m.Where("r.title LIKE ? OR r.content LIKE ? OR r.org_name LIKE ?", kw, kw, kw)
|
||||||
|
}
|
||||||
|
if f.Status > 0 {
|
||||||
|
m = m.Where("r.status", f.Status)
|
||||||
|
} else {
|
||||||
|
// 默认排除「已失效/已删除」,保留有效与已更正。
|
||||||
|
m = m.Where("r.status", g.Slice{0, 1})
|
||||||
|
}
|
||||||
|
if f.DateFrom != "" {
|
||||||
|
m = m.WhereGTE("r.publish_date", f.DateFrom)
|
||||||
|
}
|
||||||
|
if f.DateTo != "" {
|
||||||
|
m = m.WhereLTE("r.publish_date", f.DateTo)
|
||||||
|
}
|
||||||
|
if f.OrgName != "" {
|
||||||
|
m = m.Where("r.org_name LIKE ?", "%"+f.OrgName+"%")
|
||||||
|
}
|
||||||
|
if f.SourceId > 0 {
|
||||||
|
m = m.Where("r.source_id", f.SourceId)
|
||||||
|
}
|
||||||
|
if f.OnlyNewToday {
|
||||||
|
m = m.Where("DATE(r.created_at)=CURDATE()")
|
||||||
|
}
|
||||||
|
total, err := m.Clone().Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "count recruitment")
|
||||||
|
}
|
||||||
|
var list []dto.RecruitmentInfoVO
|
||||||
|
if err = m.Clone().Page(f.Page, f.Size).OrderDesc("r.publish_date").OrderDesc("r.id").Scan(&list); err != nil {
|
||||||
|
return nil, 0, gerror.Wrap(err, "query recruitment list")
|
||||||
|
}
|
||||||
|
for i := range list {
|
||||||
|
list[i].CategoryName = dto.CategoryName(list[i].Category)
|
||||||
|
list[i].StatusName = dto.StatusName(list[i].Status)
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail 公告详情(带数据源名称)。
|
||||||
|
func (s *recruitment) Detail(ctx context.Context, id uint64) (*dto.RecruitmentInfoVO, error) {
|
||||||
|
var v dto.RecruitmentInfoVO
|
||||||
|
err := dao.RecruitmentInfo.Ctx(ctx).
|
||||||
|
As("r").LeftJoin("crawl_source cs", "r.source_id=cs.id").
|
||||||
|
Fields("r.*, cs.name AS source_name").
|
||||||
|
Where("r.id", id).Scan(&v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "query recruitment detail")
|
||||||
|
}
|
||||||
|
if v.Id == 0 {
|
||||||
|
return nil, response.Error(consts.CodeInvalidParam, "公告不存在")
|
||||||
|
}
|
||||||
|
v.CategoryName = dto.CategoryName(v.Category)
|
||||||
|
v.StatusName = dto.StatusName(v.Status)
|
||||||
|
return &v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats 看板统计:总数/今日/近7天/分类分布/地区分布/近30天趋势。
|
||||||
|
func (s *recruitment) Stats(ctx context.Context, region string) (*dto.RecruitmentStats, error) {
|
||||||
|
base := dao.RecruitmentInfo.Ctx(ctx).Where("status", g.Slice{0, 1})
|
||||||
|
if region != "" {
|
||||||
|
base = base.Where("region", region)
|
||||||
|
}
|
||||||
|
total, err := base.Clone().Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "count total")
|
||||||
|
}
|
||||||
|
todayNew, err := base.Clone().Where("DATE(created_at)=CURDATE()").Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "count today")
|
||||||
|
}
|
||||||
|
weekNew, err := base.Clone().Where("created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)").Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "count week")
|
||||||
|
}
|
||||||
|
var catRows []struct {
|
||||||
|
Category int `json:"category"`
|
||||||
|
Cnt int `json:"cnt"`
|
||||||
|
}
|
||||||
|
if err = base.Clone().Fields("category, COUNT(*) AS cnt").Group("category").Scan(&catRows); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "agg category")
|
||||||
|
}
|
||||||
|
var regionRows []struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Cnt int `json:"cnt"`
|
||||||
|
}
|
||||||
|
if err = base.Clone().Fields("region, COUNT(*) AS cnt").Group("region").Scan(®ionRows); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "agg region")
|
||||||
|
}
|
||||||
|
var trendRows []struct {
|
||||||
|
D string `json:"d"`
|
||||||
|
Cnt int `json:"cnt"`
|
||||||
|
}
|
||||||
|
if err = base.Clone().
|
||||||
|
Fields("DATE(publish_date) AS d, COUNT(*) AS cnt").
|
||||||
|
Where("publish_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)").
|
||||||
|
Group("d").OrderAsc("d").Scan(&trendRows); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "trend")
|
||||||
|
}
|
||||||
|
out := &dto.RecruitmentStats{Total: int(total), TodayNew: int(todayNew), WeekNew: int(weekNew)}
|
||||||
|
for _, r := range catRows {
|
||||||
|
out.ByCategory = append(out.ByCategory, dto.CategoryAgg{Category: r.Category, CategoryName: dto.CategoryName(r.Category), Count: r.Cnt})
|
||||||
|
}
|
||||||
|
for _, r := range regionRows {
|
||||||
|
out.ByRegion = append(out.ByRegion, dto.RegionAgg{Region: r.Region, Count: r.Cnt})
|
||||||
|
}
|
||||||
|
for _, r := range trendRows {
|
||||||
|
out.RecentTrend = append(out.RecentTrend, dto.TrendPoint{Date: r.D, Count: r.Cnt})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trend 按日趋势(可选地区/分类/天数)。
|
||||||
|
func (s *recruitment) Trend(ctx context.Context, region string, category, days int) ([]dto.TrendPoint, error) {
|
||||||
|
m := dao.RecruitmentInfo.Ctx(ctx).Where("status", g.Slice{0, 1})
|
||||||
|
if region != "" {
|
||||||
|
m = m.Where("region", region)
|
||||||
|
}
|
||||||
|
if category > 0 {
|
||||||
|
m = m.Where("category", category)
|
||||||
|
}
|
||||||
|
m = m.Where("publish_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY)", days)
|
||||||
|
var rows []struct {
|
||||||
|
D string `json:"d"`
|
||||||
|
Cnt int `json:"cnt"`
|
||||||
|
}
|
||||||
|
if err := m.Fields("DATE(publish_date) AS d, COUNT(*) AS cnt").Group("d").OrderAsc("d").Scan(&rows); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "query trend")
|
||||||
|
}
|
||||||
|
out := make([]dto.TrendPoint, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
out = append(out, dto.TrendPoint{Date: r.D, Count: r.Cnt})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sources 数据源列表与最近运行状态。
|
||||||
|
func (s *recruitment) Sources(ctx context.Context) ([]dto.SourceItem, error) {
|
||||||
|
var srcs []entity.CrawlSource
|
||||||
|
if err := dao.CrawlSource.Ctx(ctx).OrderAsc("id").Scan(&srcs); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "query sources")
|
||||||
|
}
|
||||||
|
// 取每个源最近一条日志作为摘要。
|
||||||
|
var logs []entity.CrawlLog
|
||||||
|
_ = dao.CrawlLog.Ctx(ctx).OrderDesc("id").Scan(&logs)
|
||||||
|
lastBySource := make(map[uint64]entity.CrawlLog)
|
||||||
|
for _, l := range logs {
|
||||||
|
if _, ok := lastBySource[l.SourceId]; !ok {
|
||||||
|
lastBySource[l.SourceId] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make([]dto.SourceItem, 0, len(srcs))
|
||||||
|
for _, src := range srcs {
|
||||||
|
item := dto.SourceItem{
|
||||||
|
Id: src.Id, Name: src.Name, BaseUrl: src.BaseUrl, SourceType: src.SourceType,
|
||||||
|
Category: src.Category, Region: src.Region, Enabled: src.Enabled,
|
||||||
|
LastSuccessAt: src.LastSuccessAt, FailCount: src.FailCount,
|
||||||
|
}
|
||||||
|
if lg, ok := lastBySource[src.Id]; ok {
|
||||||
|
sum := "抓取"
|
||||||
|
if lg.Error != "" {
|
||||||
|
sum += "失败: " + lg.Error
|
||||||
|
} else {
|
||||||
|
sum += "成功 " + gtime.New(lg.RunAt).Format("Y-m-d H:i") +
|
||||||
|
" 新增" + strconv.Itoa(lg.NewCount) + "/抓取" + strconv.Itoa(lg.Fetched)
|
||||||
|
}
|
||||||
|
item.LastSummary = sum
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger 手动触发抓取:sourceId=0 表示全部启用源;force=true 强制全量回溯。
|
||||||
|
func (s *recruitment) Trigger(ctx context.Context, sourceId uint64, force bool) (int, string, error) {
|
||||||
|
m := dao.CrawlSource.Ctx(ctx)
|
||||||
|
if sourceId > 0 {
|
||||||
|
m = m.Where("id", sourceId)
|
||||||
|
} else {
|
||||||
|
m = m.Where("enabled", 1)
|
||||||
|
}
|
||||||
|
var srcs []entity.CrawlSource
|
||||||
|
if err := m.OrderAsc("id").Scan(&srcs); err != nil {
|
||||||
|
return 0, "", gerror.Wrap(err, "query crawl sources")
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
n := 0
|
||||||
|
for _, src := range srcs {
|
||||||
|
res := crawlerRun(ctx, src, force)
|
||||||
|
recordCrawlLog(ctx, res)
|
||||||
|
n++
|
||||||
|
sb.WriteString(src.Name + ": +" + itoa(res.NewCount) + "/~" + itoa(res.Fetched) + "; ")
|
||||||
|
}
|
||||||
|
return n, sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushTest 向指定订阅(或首个启用订阅)发送一条 Bark 测试推送。
|
||||||
|
func (s *recruitment) PushTest(ctx context.Context, subId uint64, title, body string) (bool, string, error) {
|
||||||
|
sub, err := loadSubscription(ctx, subId)
|
||||||
|
if err != nil {
|
||||||
|
return false, err.Error(), err
|
||||||
|
}
|
||||||
|
if sub == nil {
|
||||||
|
return false, "无启用的推送订阅", gerror.New("no enabled subscription")
|
||||||
|
}
|
||||||
|
if title == "" {
|
||||||
|
title = "招聘聚合 · 推送测试"
|
||||||
|
}
|
||||||
|
if body == "" {
|
||||||
|
body = "这是一条来自 service.xpcool.com 的测试推送。"
|
||||||
|
}
|
||||||
|
return barkPush(ctx, sub.DeviceKey, title, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionList 推送订阅列表。
|
||||||
|
func (s *recruitment) SubscriptionList(ctx context.Context) ([]dto.SubscriptionItem, error) {
|
||||||
|
var subs []entity.PushSubscription
|
||||||
|
if err := dao.PushSubscription.Ctx(ctx).OrderDesc("id").Scan(&subs); err != nil {
|
||||||
|
return nil, gerror.Wrap(err, "query subscriptions")
|
||||||
|
}
|
||||||
|
out := make([]dto.SubscriptionItem, 0, len(subs))
|
||||||
|
for _, sub := range subs {
|
||||||
|
out = append(out, dto.SubscriptionItem{
|
||||||
|
Id: sub.Id, Name: sub.Name, DeviceKey: sub.DeviceKey,
|
||||||
|
Regions: parseJSONStrings(sub.Regions), Categories: parseJSONInts(sub.Categories),
|
||||||
|
OnlyNew: sub.OnlyNew, PushTime: sub.PushTime, Enabled: sub.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionSave 新增/更新推送订阅。
|
||||||
|
func (s *recruitment) SubscriptionSave(ctx context.Context, in dto.SubscriptionInput) (uint64, error) {
|
||||||
|
data := doPushSubscription(in)
|
||||||
|
if in.Id > 0 {
|
||||||
|
_, err := dao.PushSubscription.Ctx(ctx).Where("id", in.Id).Data(data).Update()
|
||||||
|
if err != nil {
|
||||||
|
return 0, gerror.Wrap(err, "update subscription")
|
||||||
|
}
|
||||||
|
return in.Id, nil
|
||||||
|
}
|
||||||
|
id, err := dao.PushSubscription.Ctx(ctx).Data(data).InsertAndGetId()
|
||||||
|
if err != nil {
|
||||||
|
return 0, gerror.Wrap(err, "insert subscription")
|
||||||
|
}
|
||||||
|
return uint64(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionDelete 删除推送订阅。
|
||||||
|
func (s *recruitment) SubscriptionDelete(ctx context.Context, id uint64) error {
|
||||||
|
if _, err := dao.PushSubscription.Ctx(ctx).Where("id", id).Delete(); err != nil {
|
||||||
|
return gerror.Wrap(err, "delete subscription")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// itoa 整型转字符串(摘要拼接用)。
|
||||||
|
func itoa(v int) string {
|
||||||
|
return strconv.Itoa(v)
|
||||||
|
}
|
||||||
11
manifest/sql/012_house_transaction_menu.sql
Normal file
11
manifest/sql/012_house_transaction_menu.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- 012_house_transaction_menu.sql
|
||||||
|
-- 成交记录查询权限(挂到数据明细菜单 94 下)。
|
||||||
|
-- 幂等:INSERT ... ON DUPLICATE KEY UPDATE + INSERT IGNORE,可重复执行。
|
||||||
|
|
||||||
|
INSERT INTO admin_menu (id, parent_id, name, icon, type, path, component, permission, sort, status, hidden, created_at, updated_at) VALUES
|
||||||
|
(941, 94, '成交查询', '', 2, 'POST /api/service/admin/house/transaction/list', '', 'house:data:transaction', 2, 1, 0, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE parent_id=VALUES(parent_id), name=VALUES(name), type=VALUES(type),
|
||||||
|
path=VALUES(path), permission=VALUES(permission), sort=VALUES(sort), status=VALUES(status), hidden=VALUES(hidden), deleted_at=NULL;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_menu (role_id, menu_id, created_at, updated_at)
|
||||||
|
SELECT 1, id, NOW(), NOW() FROM admin_menu WHERE id IN (941);
|
||||||
Loading…
Reference in New Issue
Block a user