diff --git a/app/admin/admin_users.rb b/app/admin/admin_users.rb index fed0ec1..1e1fd4f 100644 --- a/app/admin/admin_users.rb +++ b/app/admin/admin_users.rb @@ -1,4 +1,5 @@ ActiveAdmin.register AdminUser do + menu label: "AdminUser Manager", parent: "系统管理" permit_params :email, :password, :password_confirmation index do diff --git a/app/admin/ahoy_dashboard.rb b/app/admin/ahoy_dashboard.rb new file mode 100644 index 0000000..eabbe4a --- /dev/null +++ b/app/admin/ahoy_dashboard.rb @@ -0,0 +1,65 @@ +ActiveAdmin.register_page "Ahoy Dashboard" do + menu label: "总览", parent: "数据统计", priority: 1 + + content title: "总览" do + columns do + column do + panel "访问统计" do + para "总访问量: #{Ahoy::Visit.count}" + para "总事件数: #{Ahoy::Event.count}" + para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}" + end + end + + column do + panel "热门城市" do + table_for City.by_popularity.limit(10) do + column("城市") { |city| link_to(city.name, admin_city_path(city)) } + column("访问量") { |city| city.view_count } + end + end + end + + column do + panel "热门天气艺术" do + table_for WeatherArt.by_popularity.limit(10) do + column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) } + column("访问量") { |art| art.view_count } + end + end + end + end + + columns do + column do + panel "最冷门活跃城市" do + table_for City.least_popular_active.limit(10) do + column("城市") { |city| link_to(city.name, admin_city_path(city)) } + column("访问量") { |city| city.view_count } + # column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") } + end + end + end + + column do + panel "热门未活跃城市" do + table_for City.most_popular_inactive.limit(10) do + column("城市") { |city| link_to(city.name, admin_city_path(city)) } + column("访问量") { |city| city.view_count } + column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") } + column("所属区域") { |city| city.country.region.name } + end + end + end + end + + # 添加一个事件列表面板 + panel "最近事件" do + table_for Ahoy::Event.order(time: :desc).limit(10) do + column :time + column :name + column :properties + end + end + end +end diff --git a/app/admin/ahoy_events.rb b/app/admin/ahoy_events.rb index 35e945b..b1453c1 100644 --- a/app/admin/ahoy_events.rb +++ b/app/admin/ahoy_events.rb @@ -1,4 +1,5 @@ ActiveAdmin.register Ahoy::Event do + menu label: "事件统计", parent: "数据统计" # See permitted parameters documentation: # https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters # @@ -13,7 +14,7 @@ ActiveAdmin.register Ahoy::Event do # permitted << :other if params[:action] == 'create' && current_user.admin? # permitted # end - menu priority: 101, label: "事件统计" + # menu priority: 101, label: "事件统计" actions :index diff --git a/app/admin/ahoy_visits.rb b/app/admin/ahoy_visits.rb index 9bf914e..83218f8 100644 --- a/app/admin/ahoy_visits.rb +++ b/app/admin/ahoy_visits.rb @@ -1,4 +1,5 @@ ActiveAdmin.register Ahoy::Visit do + menu label: "访客统计", parent: "数据统计" # See permitted parameters documentation: # https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters # @@ -14,7 +15,7 @@ ActiveAdmin.register Ahoy::Visit do # permitted # end - menu priority: 100, label: "访问统计" + # menu priority: 100, label: "访问统计" actions :index diff --git a/app/admin/cities.rb b/app/admin/cities.rb index 013b901..53895cc 100644 --- a/app/admin/cities.rb +++ b/app/admin/cities.rb @@ -1,4 +1,5 @@ ActiveAdmin.register City do + menu label: "City Manager", parent: "系统管理" controller do def find_resource scoped_collection.friendly.find(params[:id]) diff --git a/app/admin/countries.rb b/app/admin/countries.rb index 13d31f9..c6be79f 100644 --- a/app/admin/countries.rb +++ b/app/admin/countries.rb @@ -1,4 +1,5 @@ ActiveAdmin.register Country do + menu label: "Country Manager", parent: "系统管理" controller do def find_resource scoped_collection.friendly.find(params[:id]) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index 7a6d65c..f11bf0d 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -37,9 +37,11 @@ ActiveAdmin.register_page "Dashboard" do end end end + end + columns do column do - panel "冷门活跃城市" do + panel "最冷门活跃城市" do table_for City.least_popular_active.limit(10) do column("城市") { |city| link_to(city.name, admin_city_path(city)) } column("访问量") { |city| city.view_count } @@ -47,6 +49,17 @@ ActiveAdmin.register_page "Dashboard" do end end end + + column do + panel "热门未活跃城市" do + table_for City.most_popular_inactive.limit(10) do + column("城市") { |city| link_to(city.name, admin_city_path(city)) } + column("访问量") { |city| city.view_count } + column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") } + column("所属区域") { |city| city.country.region.name } + end + end + end end # 添加一个事件列表面板 diff --git a/app/admin/regions.rb b/app/admin/regions.rb index 494ea4c..3827b06 100644 --- a/app/admin/regions.rb +++ b/app/admin/regions.rb @@ -1,4 +1,5 @@ ActiveAdmin.register Region do + menu label: "Region Manager", parent: "系统管理" # See permitted parameters documentation: # https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters # diff --git a/app/admin/sidekiq_jobs.rb b/app/admin/sidekiq_jobs.rb new file mode 100644 index 0000000..e52bb4c --- /dev/null +++ b/app/admin/sidekiq_jobs.rb @@ -0,0 +1,98 @@ +# app/admin/sidekiq_tasks.rb +ActiveAdmin.register_page "Sidekiq Tasks" do + # menu label: "Sidekiq Tasks", priority: 99 + menu label: "Sidekiq Tasks Manager", parent: "系统管理" + + content title: "Sidekiq Tasks Management" do + div class: "sidekiq-tasks" do + panel "Manual Task Execution" do + div class: "task-buttons" do + div class: "task-button" do + h3 "Generate Weather Arts" + form action: admin_sidekiq_tasks_run_task_path, method: :post do + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "task", value: "GenerateWeatherArtsWorker" + select name: "city_id" do + City.all.map do |city| + option city.name, value: city.id + end + end + input type: "submit", value: "Run Task", class: "button" + end + end + + div class: "task-button" do + h3 "Batch Generate Weather Arts" + form action: admin_sidekiq_tasks_run_task_path, method: :post do + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "task", value: "BatchGenerateWeatherArts" + input type: "submit", value: "Run Task", class: "button" + end + end + + div class: "task-button" do + h3 "Refresh Sitemap" + form action: admin_sidekiq_tasks_run_task_path, method: :post do + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "task", value: "RefreshSitemapWorker" + input type: "submit", value: "Run Task", class: "button" + end + end + + div class: "task-button" do + h3 "Clean Ahoy Data" + form action: admin_sidekiq_tasks_run_task_path, method: :post do + input type: "hidden", name: "authenticity_token", value: form_authenticity_token + input type: "hidden", name: "task", value: "CleanAhoyDataWorker" + input type: "submit", value: "Run Task", class: "button" + end + end + end + end + + panel "Sidekiq Statistics" do + stats = Sidekiq::Stats.new + table class: "sidekiq-stats" do + tr do + th "Processed Jobs" + td stats.processed + end + tr do + th "Failed Jobs" + td stats.failed + end + tr do + th "Enqueued Jobs" + td stats.enqueued + end + tr do + th "Scheduled Jobs" + td stats.scheduled_size + end + tr do + th "Retry Set Size" + td stats.retry_size + end + end + end + end + end + + page_action :run_task, method: :post do + task_name = params[:task] + city_id = params[:city_id] + + case task_name + when "BatchGenerateWeatherArts" + BatchGenerateWeatherArtsWorker.perform_async + when "GenerateWeatherArtsWorker" + GenerateWeatherArtWorker.perform_async(city_id) + when "RefreshSitemapWorker" + RefreshSitemapWorker.perform_async + when "CleanAhoyDataWorker" + CleanAhoyDataWorker.perform_async + end + + redirect_to admin_sidekiq_tasks_path, notice: "Task #{task_name} has been queued" + end +end diff --git a/app/admin/weather_arts.rb b/app/admin/weather_arts.rb index 209e8dc..7f560d5 100644 --- a/app/admin/weather_arts.rb +++ b/app/admin/weather_arts.rb @@ -1,4 +1,5 @@ ActiveAdmin.register WeatherArt do + menu label: "WeatherArt Manager", parent: "系统管理" controller do def find_resource scoped_collection.friendly.find(params[:id]) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index be708a8..7fb8d85 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,65 @@ class ApplicationController < ActionController::Base include SeoConcern # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + before_action :log_browser_info # allow_browser versions: :modern + # allow_browser versions: :modern, + # patterns: [ + # # 鸿蒙系统相关 + # /OpenHarmony/, # 鸿蒙系统标识 + # /ArkWeb\/[\d.]+/, # 鸿蒙浏览器内核 + # /Mobile HuaweiBrowser/, # 华为浏览器(新格式) + # /HuaweiBrowser\/[\d.]+/, # 华为浏览器(旧格式) + # + # # 夸克浏览器(更宽松的匹配) + # /Quark[\s\/][\d.]+/, # 匹配 "Quark/7.4.6.681" 或 "Quark 7.4.6.681" + # + # /Mobile Safari/, + # /Chrome\/[\d.]+/, + # /Quark\/[\d.]+/, + # /HuaweiBrowser\/[\d.]+/, + # /MiuiBrowser\/[\d.]+/, + # /VivoBrowser\/[\d.]+/, + # /OppoBrowser\/[\d.]+/, + # /UCBrowser\/[\d.]+/, + # /QQBrowser\/[\d.]+/, + # /MicroMessenger\/[\d.]+/, + # /Alipay/, + # /BaiduBoxApp/, + # /baiduboxapp/i, + # /SogouMobile/, + # /Weibo/, + # /DingTalk/, + # /ToutiaoMicroApp/, + # /BytedanceWebview/, + # /ArkWeb/ + # ], + # on_failure: ->(browser) { + # Rails.logger.warn <<~BROWSER_INFO + # Browser Blocked: + # User Agent: #{browser.ua} + # Name: #{browser.name} + # Version: #{browser.version} + # Platform: #{browser.platform.name} + # Device: #{browser.device.name} + # Mobile: #{browser.mobile?} + # Modern: #{browser.modern?} + # Bot: #{browser.bot?} + # BROWSER_INFO + # } before_action :set_locale after_action :track_action + def log_browser_info + # 构建详细的浏览器信息 + Rails.logger.debug "User Agent: #{request.user_agent}" + + # 如果是被拦截的浏览器,记录额外信息 + # unless browser_allowed? + # Rails.logger.info "User Agent: #{request.user_agent}" + # end + end + protected def track_action diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb deleted file mode 100644 index d394c3d..0000000 --- a/app/jobs/application_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ApplicationJob < ActiveJob::Base - # Automatically retry jobs that encountered a deadlock - # retry_on ActiveRecord::Deadlocked - - # Most jobs are safe to ignore if the underlying records are no longer available - # discard_on ActiveJob::DeserializationError -end diff --git a/app/jobs/batch_generate_weather_arts_job.rb b/app/jobs/batch_generate_weather_arts_job.rb deleted file mode 100644 index 350753b..0000000 --- a/app/jobs/batch_generate_weather_arts_job.rb +++ /dev/null @@ -1,33 +0,0 @@ -class BatchGenerateWeatherArtsJob < ApplicationJob - queue_as :default - - def perform(*args) - start_time = Time.current - max_duration = 50.minutes - - cities_to_process = get_eligible_cities - - cities_to_process.each do |city| - break if Time.current - start_time > max_duration - - GenerateWeatherArtJob.perform_now(city) - sleep 1.minute # 确保不超过API限制 - end - end - - private - - def get_eligible_cities - City.active - .where(active: true) - .where("last_weather_fetch IS NULL OR last_weather_fetch < ?", Date.today) - # .select { |city| early_morning_in_timezone?(city.timezone) } - end - - # def early_morning_in_timezone?(timezone) - # return false if timezone.blank? - - # time = Time.current.in_time_zone(timezone) - # time.hour == 2 - # end -end diff --git a/app/jobs/generate_weather_art_job.rb b/app/jobs/generate_weather_art_job.rb deleted file mode 100644 index adbaaa2..0000000 --- a/app/jobs/generate_weather_art_job.rb +++ /dev/null @@ -1,45 +0,0 @@ -class GenerateWeatherArtJob < ApplicationJob - queue_as :default - - def perform(*args) - city = args[0] - return if city.last_weather_fetch&.today? - - weather_service = WeatherService.new - ai_service = AiService.new - - # 获取天气数据 - weather_data = weather_service.get_weather(city.latitude, city.longitude) - return unless weather_data - - # 生成提示词 - prompt = ai_service.generate_prompt(city, weather_data) - return unless prompt - - # 生成图像 - image_url = ai_service.generate_image(prompt) - return unless image_url - - # 创建天气艺术记录 - weather_art = city.weather_arts.create!( - weather_date: Date.today, - **weather_data, - prompt: prompt - ) - - # 下载并附加图像 - tempfile = Down.download(image_url) - weather_art.image.attach( - io: tempfile, - filename: "#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png" - ) - - # 更新城市状态 - city.update!( - last_weather_fetch: Time.current, - last_image_generation: Time.current - ) - rescue => e - Rails.logger.error "Error generating weather art for #{city.name}: #{e.message}" - end -end diff --git a/app/models/city.rb b/app/models/city.rb index fe9de45..95f8f4d 100644 --- a/app/models/city.rb +++ b/app/models/city.rb @@ -53,6 +53,24 @@ class City < ApplicationRecord .order("visit_count ASC, cities.name ASC") end } + scope :most_popular_inactive, -> { + 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") + 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") + end + } + def to_s name @@ -66,7 +84,7 @@ class City < ApplicationRecord end def localized_name - I18n.t("cities.#{name.parameterize.underscore}") + I18n.t("cities.#{name.parameterize.underscore}", default: name) end def full_name diff --git a/app/views/cities/show.html.erb b/app/views/cities/show.html.erb index 55f6353..c709d87 100644 --- a/app/views/cities/show.html.erb +++ b/app/views/cities/show.html.erb @@ -35,7 +35,7 @@ <%= @city.country.name %>, <%= @city.region %>
- <%= Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") %> + <%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>