// Package house_dashboard 提供看房数据看板聚合服务。 package house_dashboard import ( "context" "github.com/gogf/gf/v2/errors/gerror" "service.xpcool.com/internal/dao" "service.xpcool.com/internal/model/dto" ) // IDashboard 看板聚合领域服务接口。 type IDashboard interface { Overview(context.Context, string) (dto.DashboardOverview, error) MapPoints(context.Context, string, float64, float64, int) ([]dto.DashboardMapPoint, error) PriceTrend(context.Context, uint64, string, int) ([]dto.DashboardTrendPoint, error) AggregateRegion(context.Context) ([]dto.DashboardRegionAgg, error) } type dashboard struct{} var localDashboard IDashboard func NewDashboard() IDashboard { return &dashboard{} } // Dashboard 返回已注册的看板服务实现。 func Dashboard() IDashboard { if localDashboard == nil { panic("Dashboard 实现未注册") } return localDashboard } // RegisterDashboard 注册看板服务实现。 func RegisterDashboard(i IDashboard) { localDashboard = i } // Overview 统计概览:小区数、在售房源、笋盘、低可信、均价、平均挂牌天数。 func (s *dashboard) Overview(ctx context.Context, region string) (dto.DashboardOverview, error) { var out dto.DashboardOverview cm := dao.HouseCommunity.Ctx(ctx) if region != "" { cm = cm.Where("region", region) } cnt, err := cm.Count() if err != nil { return out, gerror.Wrap(err, "统计小区总数失败") } out.CommunityCount = cnt // 房源相关指标统一经小区联表,支持按区域过滤。 lm := dao.HouseListing.Ctx(ctx).As("l").LeftJoin("house_community c", "l.community_id=c.id") if region != "" { lm = lm.Where("c.region", region) } out.ListingCount, _ = lm.Clone().Where("l.status", 1).Count() out.BargainCount, _ = lm.Clone().Where("l.is_bargain", 1).Count() out.LowConfidence, _ = lm.Clone().Where("l.confidence", 1).Count() var agg struct { AvgUnitPrice float64 `orm:"avg_unit_price"` AvgTotalPrice float64 `orm:"avg_total_price"` AvgListDays float64 `orm:"avg_list_days"` } if err := lm.Clone().Where("l.status", 1).Fields( "COALESCE(AVG(l.unit_price),0) AS avg_unit_price, COALESCE(AVG(l.total_price),0) AS avg_total_price, COALESCE(AVG(l.on_market_days),0) AS avg_list_days", ).Scan(&agg); err != nil { return out, gerror.Wrap(err, "聚合概览指标失败") } out.AvgUnitPrice = agg.AvgUnitPrice out.AvgTotalPrice = agg.AvgTotalPrice out.AvgListDays = agg.AvgListDays return out, nil } // MapPoints 地图点位:按小区聚合均价与在售/笋盘数量。 func (s *dashboard) MapPoints(ctx context.Context, region string, priceMin, priceMax float64, status int) ([]dto.DashboardMapPoint, error) { m := dao.HouseListing.Ctx(ctx).As("l"). LeftJoin("house_community c", "l.community_id=c.id"). Fields("l.community_id AS community_id, c.name AS name, c.region AS region, c.lng AS lng, c.lat AS lat, COALESCE(AVG(l.unit_price),0) AS avg_unit_price, COUNT(*) AS listing_count, COALESCE(SUM(l.is_bargain),0) AS bargain_count"). Group("l.community_id, c.name, c.region, c.lng, c.lat") if region != "" { m = m.Where("c.region", region) } if priceMin > 0 { m = m.WhereGTE("l.total_price", priceMin) } if priceMax > 0 { m = m.WhereLTE("l.total_price", priceMax) } if status > 0 { m = m.Where("l.status", status) } var list []dto.DashboardMapPoint if err := m.Scan(&list); err != nil { return nil, gerror.Wrap(err, "查询地图点位失败") } return list, nil } // PriceTrend 价格趋势:按快照日期聚合挂牌/成交均价,返回时间升序。 func (s *dashboard) PriceTrend(ctx context.Context, communityId uint64, region string, limit int) ([]dto.DashboardTrendPoint, error) { m := dao.HousePriceSnapshot.Ctx(ctx).As("p") if communityId > 0 { m = m.Where("p.community_id", communityId) } if region != "" { m = m.LeftJoin("house_community c", "p.community_id=c.id").Where("c.region", region) } m = m.Fields("p.snap_date AS date, COALESCE(AVG(p.list_price),0) AS avg_list_price, COALESCE(AVG(p.deal_price),0) AS avg_deal_price"). Group("p.snap_date"). OrderDesc("p.snap_date") if limit > 0 { m = m.Limit(limit) } var list []dto.DashboardTrendPoint if err := m.Scan(&list); err != nil { return nil, gerror.Wrap(err, "查询价格趋势失败") } // 反转为时间升序,便于前端画趋势线。 for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { list[i], list[j] = list[j], list[i] } return list, nil } // AggregateRegion 区域聚合:按区县统计在售均价与数量。 func (s *dashboard) AggregateRegion(ctx context.Context) ([]dto.DashboardRegionAgg, error) { m := dao.HouseListing.Ctx(ctx).As("l"). LeftJoin("house_community c", "l.community_id=c.id"). Fields("c.region AS region, COALESCE(AVG(l.unit_price),0) AS avg_unit_price, COUNT(*) AS listing_count, COALESCE(SUM(l.is_bargain),0) AS bargain_count"). Where("l.status", 1). WhereGT("c.region", ""). Group("c.region"). OrderAsc("c.region") var list []dto.DashboardRegionAgg if err := m.Scan(&list); err != nil { return nil, gerror.Wrap(err, "查询区域聚合失败") } return list, nil }