service.xpcool.com/internal/service/house/dashboard/dashboard.go
夏犀麟 4aca0c7f6f
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 5m7s
feat(house): 实现看房模块后端功能
- 新增 9 张房屋相关数据表(社区/楼宇/房源/价格快照/交易/设施/社区设施/学区/偏好)
- 添加菜单权限种子数据并绑定超级管理员角色
- 生成 DAO 层代码和实体对象
- 实现房屋模块 API 接口(社区/房源/看板)和控制器服务层
- 支持多平台软关联匹配、笋盘标记和低可信度标记功能
- 更新超级管理员账号为 xxcool/xxCool@2026
- 调整 RBAC 菜单结构,移除管理员管理功能,新增日志管理菜单
- 修复 RBAC 安全漏洞,确保禁用角色权限失效
- 重构认证模块,将登录相关接口迁移到统一包结构下
- 移除废弃的管理模块和工具类接口定义
- 为通用工具包添加中文注释和文档说明
2026-08-26 23:42:38 +08:00

142 lines
5.0 KiB
Go

// 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 implementation not registered")
}
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, "count community")
}
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, "aggregate overview")
}
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, "query map points")
}
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, "query price trend")
}
// 反转为时间升序,便于前端画趋势线。
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, "query region aggregate")
}
return list, nil
}