- Remove I18n translation from localized_name - Return city name directly instead This change simplifies the method's implementation by returning the city name directly, avoiding the overhead of translation. Additionally, it improves code clarity by reducing complexity.
339 lines
11 KiB
Ruby
339 lines
11 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 :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)
|
|
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, quick = false, is_user = true)
|
|
# 标准化语言代码为小写字符串
|
|
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
|
|
|
|
if quick
|
|
GenerateCityDescriptionWorker.perform_async(id, force_refresh)
|
|
temp_desc = !is_user ?
|
|
"#{full_name}" :
|
|
"#{full_name} - Please wait a Minute. (City description is being generated in the background. Please refresh the page in a moment to see the complete description)."
|
|
return temp_desc
|
|
end
|
|
|
|
# 否则,生成新的描述并保存
|
|
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 100 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
|