class WeatherArt < ApplicationRecord extend FriendlyId friendly_id :weather_date, use: :slugged belongs_to :city has_one_attached :image has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id validates :weather_date, presence: true validates :city_id, presence: true scope :latest, ->(limit = 100) { order(created_at: :desc).limit(limit) } scope :by_popularity, ->(limit = 100) { if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite" joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'") .group("weather_arts.id") .select("weather_arts.*, COUNT(ahoy_events.id) as visit_count") .order("visit_count DESC").limit(limit) else joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'") .group("weather_arts.id") .select("weather_arts.*, COUNT(ahoy_events.id) as visit_count") .order("visit_count DESC").limit(limit) end } scope :random, ->(limit = 3) { if ActiveRecord::Base.connection.adapter_name.downcase == "postgresql" # PostgreSQL 优化版本 order(Arel.sql("RANDOM()")).limit(limit) elsif ActiveRecord::Base.connection.adapter_name.downcase == "mysql2" # MySQL 优化版本 order(Arel.sql("RAND()")).limit(limit) else # SQLite 或其他数据库的通用版本 order(Arel.sql("RANDOM()")).limit(limit) end } def should_generate_new_friendly_id? weather_date_changed? || city_id_changed? || super end def to_s "#{city.name} - #{weather_date.strftime('%Y-%m-%d')}" end def self.ransackable_associations(auth_object = nil) [ "city", "image_attachment", "image_blob" ] end def self.ransackable_attributes(auth_object = nil) [ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ] end def view_count if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite" Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count else Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count end end def formatted_time(type = :date, use_local_timezone = false) # 获取时区 timezone_info = self.city&.country&.timezones.present? ? eval(self.city.country.timezones).first : { "zoneName" => "UTC", "gmtOffsetName" => "UTC+00:00" } # 设置时区对象 time_zone = ActiveSupport::TimeZone[timezone_info["zoneName"]] || ActiveSupport::TimeZone["UTC"] case type when :date # 格式化日期 self&.weather_date&.strftime("%B %d, %Y") when :time # 获取时间 time = updated_at if use_local_timezone # 使用本地时区 local_time = time.in_time_zone(time_zone) "#{local_time.strftime('%H:%M')} #{timezone_info['gmtOffsetName']}" else # 使用 UTC "#{time.utc.strftime('%H:%M')} UTC" end end end def image_url image.attached? ? image.blob : nil end def webp_image return nil unless image.attached? image.variant( format: "webp", saver: { quality: 100, strip: true, # 移除元数据以减小文件大小 interlace: "plane" # 渐进式加载 } ) end # 添加图片变体处理 PREVIEW_DIMENSIONS = { big: [ 1792, 1024 ], medium: [ 896, 512 ], small: [ 448, 256 ] }.freeze def preview_image(size = :medium) return nil unless image.attached? width, height = PREVIEW_DIMENSIONS[size] || PREVIEW_DIMENSIONS[:medium] image.variant( resize_to_limit: [ width, height ], format: "webp", saver: { quality: 75, strip: true, # 移除元数据以减小文件大小 interlace: "plane" # 渐进式加载 } ) end def watermarked_image return nil unless image.attached? overlay_text = create_overlay_text image.variant( composite: [ { input: overlay_text, gravity: "southeast" } ] ) end private def create_overlay_text { create: { width: 400, height: 100, background: [ 0, 0, 0, 0.5 ] # 半透明黑色背景 }, "svg-overlay": %( #{city.name} - #{weather_date.strftime('%Y-%m-%d')} © todayaiweather.com ) } end def create_text_layer(font_size) text = [ weather_date.strftime("%Y-%m-%d"), "#{temperature}°C, #{description}", "#{city.name}, #{city.country.name}, #{city.country.region.name}", "© todayaiweather.com" ].join("\n") { create: { width: 600, height: 200, background: [ 0, 0, 0, 0 ] }, "svg-overlay": %( #{weather_date.strftime('%Y-%m-%d')} #{temperature}°C, #{description} #{city.name}, #{city.country.name} © todayaiweather.com ) } end def watermark_command(font_size:, stroke_width:, spacing:) date_str = weather_date.strftime("%Y-%m-%d") weather_info = "#{temperature}°C, #{description}" location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}" copyright = "© todayaiweather.com" "gravity southeast " \ "fill white " \ "font Arial " \ "pointsize #{font_size} " \ "stroke black " \ "strokewidth #{stroke_width} " \ "text 30,#{spacing * 12} '#{copyright}' " \ "text 30,#{spacing * 8} '#{location_info}' " \ "text 30,#{spacing * 4} '#{weather_info}' " \ "text 30,#{spacing} '#{date_str}'" end def watermark_text date_str = weather_date.strftime("%Y-%m-%d") weather_info = "#{temperature}°C, #{description}" location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}" copyright = "© todayaiweather.com" [ "text 30,120 '#{copyright}'", "text 30,80 '#{location_info}'", "text 30,40 '#{weather_info}'", "text 30,0 '#{date_str}'" ].join(" ") end end