Compare commits

..

No commits in common. "62bfe8888ecc3ef04119f6d80a7e9165f6f883d7" and "a895216bda8c64d9a7f77fe384028266316361a0" have entirely different histories.

16 changed files with 126 additions and 279 deletions

View File

@ -1,5 +1,4 @@
ActiveAdmin.register AdminUser do
menu label: "AdminUser Manager", parent: "系统管理"
permit_params :email, :password, :password_confirmation
index do

View File

@ -1,83 +0,0 @@
ActiveAdmin.register_page "Ahoy Dashboard" do
menu label: "总览", parent: "数据统计", priority: 1
page_action :toggle_city_status, method: :post do
city = City.find(params[:city_id])
city.update(active: !city.active)
redirect_back(fallback_location: admin_dashboard_path, notice: "城市状态已更新")
end
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? ? "活跃" : "停用") }
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("操作") { |city|
button_to "停用",
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
method: :post,
data: { confirm: "确定要停用 #{city.name} 吗?" }
}
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 }
column("操作") { |city|
button_to "激活",
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
method: :post,
data: { confirm: "确定要激活 #{city.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

View File

@ -1,5 +1,4 @@
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
#
@ -14,7 +13,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

View File

@ -1,5 +1,4 @@
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
#
@ -15,7 +14,7 @@ ActiveAdmin.register Ahoy::Visit do
# permitted
# end
# menu priority: 100, label: "访问统计"
menu priority: 100, label: "访问统计"
actions :index

View File

@ -1,5 +1,4 @@
ActiveAdmin.register City do
menu label: "City Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])

View File

@ -1,5 +1,4 @@
ActiveAdmin.register Country do
menu label: "Country Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])

View File

@ -37,11 +37,9 @@ 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 }
@ -49,17 +47,6 @@ 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
# 添加一个事件列表面板

View File

@ -1,5 +1,4 @@
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
#

View File

@ -1,98 +0,0 @@
# 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

View File

@ -1,5 +1,4 @@
ActiveAdmin.register WeatherArt do
menu label: "WeatherArt Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])

View File

@ -1,65 +1,46 @@
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
# }
allow_browser versions: :modern do |config|
# 通用移动浏览器
config.allow(/Mobile Safari/) # iOS Safari
config.allow(/Chrome\/[\d.]+/) # Chrome 内核浏览器
# 国内主流浏览器
config.allow(/Quark\/[\d.]+/) # 夸克浏览器
config.allow(/HuaweiBrowser\/[\d.]+/) # 华为浏览器
config.allow(/MiuiBrowser\/[\d.]+/) # 小米浏览器
config.allow(/VivoBrowser\/[\d.]+/) # vivo浏览器
config.allow(/OppoBrowser\/[\d.]+/) # OPPO浏览器
config.allow(/UCBrowser\/[\d.]+/) # UC浏览器
config.allow(/QQBrowser\/[\d.]+/) # QQ浏览器
config.allow(/MicroMessenger\/[\d.]+/) # 微信内置浏览器
config.allow(/Alipay/) # 支付宝内置浏览器
config.allow(/BaiduBoxApp/) # 百度 App
config.allow(/baiduboxapp/i) # 百度 App (小写)
config.allow(/SogouMobile/) # 搜狗移动浏览器
config.allow(/Weibo/) # 微博内置浏览器
config.allow(/DingTalk/) # 钉钉内置浏览器
config.allow(/ToutiaoMicroApp/) # 今日头条小程序
config.allow(/BytedanceWebview/) # 字节跳动系浏览器
config.allow(/ArkWeb/) # 鸿蒙浏览器
# 调试日志(开发环境)
if Rails.env.development?
config.on_failure do |browser|
Rails.logger.info <<~INFO
Browser blocked:
Name: #{browser.name}
Version: #{browser.version}
User Agent: #{browser.ua}
INFO
end
end
end
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

View File

@ -0,0 +1,7 @@
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

View File

@ -0,0 +1,33 @@
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

View File

@ -0,0 +1,45 @@
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

View File

@ -53,24 +53,6 @@ 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
@ -84,7 +66,7 @@ class City < ApplicationRecord
end
def localized_name
I18n.t("cities.#{name.parameterize.underscore}", default: name)
I18n.t("cities.#{name.parameterize.underscore}")
end
def full_name

View File

@ -35,7 +35,7 @@
<%= @city.country.name %>, <%= @city.region %>
</div>
<div class="badge badge-lg badge-secondary gap-2">
<%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>
<%= Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") %>
</div>
</div>