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 :by_state, ->(state_id) { where(state_id: state_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 left_joins(:state) .where( "LOWER(cities.name) LIKE :query OR LOWER(states.name) LIKE :query", query: "%#{decoded_query}%" ) .distinct # 避免重复结果 } # 定义 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 def formatted_current_time(type = :date, use_local_timezone = true) # 获取时区 timezone_info = self.country&.timezones.present? ? JSON.parse(country.timezones)&.first : { "zoneName" => "UTC", "gmtOffsetName" => "UTC+00:00" } # 设置时区对象 time_zone = ActiveSupport::TimeZone[timezone_info["zoneName"]] || ActiveSupport::TimeZone["UTC"] time = Time.current case type when :date # 格式化日期 time.strftime("%B %d, %Y") when :time use_local_timezone ? "#{time.in_time_zone(time_zone).strftime('%H:%M')} #{timezone_info['gmtOffsetName']}" : "#{time.utc.strftime('%H:%M')} UTC" when :all # 返回日期 + 时间 + UTC 信息 date = time.strftime("%B %d, %Y") time = use_local_timezone ? updated_at.in_time_zone(time_zone).strftime("%H:%M") + " #{timezone_info['gmtOffsetName']}" : updated_at.utc.strftime("%H:%M") + " UTC" "#{date} #{time}" else "Unknown #{type}" end end # 获取城市描述,支持多语言 def get_description(locale = I18n.locale.to_s, force_refresh = false) # 标准化语言代码为小写字符串 locale = locale.to_s.downcase # 从text字段解析JSON数据 cached_descriptions = get_description_hash # 如果请求的语言版本存在,直接返回 return cached_descriptions[locale] if cached_descriptions[locale].present? && !force_refresh # 如果有英文版本且请求的不是英文,返回英文版本 return cached_descriptions['en'] if locale != 'en' && cached_descriptions['en'].present? && !force_refresh # 否则,生成新的描述并保存 new_description = generate_ai_description(locale) # 更新数据库中的描述 update_description(locale, new_description) new_description end # 获取描述的JSON哈希 def get_description_hash begin # 尝试解析存储的JSON字符串 JSON.parse(description_translations || "{}") rescue JSON::ParserError # 如果解析失败,返回空哈希 {} end end # 更新特定语言的城市描述 def update_description(locale, new_description) # 标准化语言代码 locale = locale.to_s.downcase # 获取当前描述哈希并更新 updated_descriptions = get_description_hash updated_descriptions[locale] = new_description # 将哈希转换回JSON字符串并更新数据库 update(description_translations: updated_descriptions.to_json) # 返回更新后的描述 new_description end # 使用AI生成城市描述 def generate_ai_description(locale = 'en') # 暂时只支持英文生成 return generate_en_description if locale == 'en' # 其他语言暂不支持,默认返回英文描述 generate_en_description end private # 生成英文描述 def generate_en_description region_name = country&.region&.name || "" country_name = country&.name || "" state_name = state&.name || "" # 创建AI服务实例 ai_service = AiService.new # 生成描述 system_message = "You are a global geography master, you understand the culture, geography, architecture, customs and other information of all cities around the world. Describe this city based on the city I gave you. Include details about its culture, climate, landmarks, and any unique features that make this place special. Condense the keyword into a description of about 50 words" user_message = "Describe the characteristics of the city of #{name}, located in the #{state_name}, #{country_name}, #{region_name}" ai_service.ask_ai(system_message, user_message) end end