today_ai_weather/app/models/city.rb
songtianlun 9ac7dd46af
Some checks are pending
Docker / docker (push) Waiting to run
feat: migrate to OpenLayers for map display
- Replaced Leaflet with OpenLayers for improved map rendering
- Added OpenLayers CSS and removed Leaflet CSS
- Updated map controller to use OpenLayers API
- Added marker icon in public directory
- Added scopes and associations for weather art in City model

This change migrates the map display from Leaflet to
OpenLayers, providing better performance and more features.
It also introduces new model associations for weather arts,
allowing to sort cities by latest weather updates.
2025-02-15 00:19:04 +08:00

218 lines
6.9 KiB
Ruby

class City < ApplicationRecord
extend FriendlyId
friendly_id :slug_candidates, use: :slugged
belongs_to :country, optional: true
belongs_to :state, optional: true
has_many :weather_arts, dependent: :destroy
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
delegate :region, to: :country
validates :name, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
delegate :region, to: :country
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
scope :by_country, ->(country_id) { where(country_id: country_id) }
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
# 在 City 模型中
scope :by_popularity, ->(period = :year, limit = 100) {
# 根据时间周期确定时间范围
start_time =
case period.to_sym
when :day
1.day.ago
when :week
1.week.ago
when :month
1.month.ago
when :year
1.year.ago
else
1.year.ago
end
# 根据数据库类型构建不同的查询
base_query = if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
joins(<<-SQL.squish)
LEFT JOIN ahoy_events ON#{' '}
json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'
AND ahoy_events.time > '#{start_time}'
SQL
else
joins(<<-SQL.squish)
LEFT JOIN ahoy_events ON#{' '}
(ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'
AND ahoy_events.time > '#{start_time}'
SQL
end
base_query
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
.limit(limit)
}
scope :least_popular_active, ->(limit = 100) {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
active
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC").limit(limit)
else
active
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC").limit(limit)
end
}
scope :most_popular_inactive, ->(limit = 100) {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
where(active: false)
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC").limit(limit)
else
where(active: false)
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC").limit(limit)
end
}
scope :search_by_name, ->(query) {
return all if query.blank?
decoded_query = URI.decode_www_form_component(query).downcase
where(
"LOWER(cities.name) LIKE :query", query: "%#{decoded_query}%"
)
}
# 定义 latest_weather_art 关联
has_one :latest_weather_art, -> { order(weather_date: :desc) },
class_name: 'WeatherArt'
# 包含最新天气艺术的 scope
scope :with_latest_weather_art, -> {
includes(:latest_weather_art)
}
# 只获取有最新天气艺术的城市
scope :has_weather_art, -> {
joins(:weather_arts).distinct
}
# 按最新天气更新时间排序
scope :order_by_latest_weather, -> {
joins(:weather_arts)
.group('cities.id')
.order('MAX(weather_arts.weather_date) DESC')
}
# 获取最近24小时内更新过天气的城市
scope :recently_updated, -> {
joins(:weather_arts)
.where('weather_arts.weather_date > ?', 24.hours.ago)
.distinct
}
def to_s
name
end
def slug_candidates
[
:name,
[ :country, :name ]
]
end
def localized_name
I18n.t("cities.#{name.parameterize.underscore}", default: name)
end
def full_name
"#{name}, #{country}"
end
def should_generate_new_friendly_id?
name_changed? || super
end
def self.ransackable_associations(auth_object = nil)
[ "weather_arts" ]
end
def self.ransackable_attributes(auth_object = nil)
[ "active", "country_id", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
end
def last_weather_fetch
# latest_weather_art&.created_at
Rails.cache.fetch("city/#{id}/last_weather_fetch", expires_in: 1.hour) do
latest_weather_art&.created_at
end
end
def last_image_generation
# latest_weather_art&.image&.created_at
Rails.cache.fetch("city/#{id}/last_image_generation", expires_in: 1.hour) do
latest_weather_art&.image&.created_at
end
end
def latest_weather_art
weather_arts.order(weather_date: :desc).first
end
def view_count(period = :day)
start_time = case period
when :day
1.day.ago
when :week
7.days.ago
when :month
1.month.ago
when :year
1.year.ago
else
1.year.ago
end
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
# Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
Ahoy::Event
.where("time >= ?", start_time)
.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?",
self.id)
.count
else
# Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
Ahoy::Event
.where("time >= ?", start_time)
.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?",
self.id)
.count
end
end
end