- Implement method to get multilingual city descriptions - Create worker for generating descriptions in the background - Update database schema to include description translations - Update AiService to fetch descriptions from City model This commit introduces a feature to generate city descriptions in multiple languages using AI. It includes methods for caching descriptions and a background job to handle generation to improve performance and user experience.
330 lines
11 KiB
Ruby
330 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)
|
|
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
|