Compare commits
No commits in common. "62bfe8888ecc3ef04119f6d80a7e9165f6f883d7" and "a895216bda8c64d9a7f77fe384028266316361a0" have entirely different histories.
62bfe8888e
...
a895216bda
@ -1,5 +1,4 @@
|
||||
ActiveAdmin.register AdminUser do
|
||||
menu label: "AdminUser Manager", parent: "系统管理"
|
||||
permit_params :email, :password, :password_confirmation
|
||||
|
||||
index do
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
ActiveAdmin.register City do
|
||||
menu label: "City Manager", parent: "系统管理"
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
|
@ -1,5 +1,4 @@
|
||||
ActiveAdmin.register Country do
|
||||
menu label: "Country Manager", parent: "系统管理"
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
|
@ -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
|
||||
|
||||
# 添加一个事件列表面板
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
@ -1,5 +1,4 @@
|
||||
ActiveAdmin.register WeatherArt do
|
||||
menu label: "WeatherArt Manager", parent: "系统管理"
|
||||
controller do
|
||||
def find_resource
|
||||
scoped_collection.friendly.find(params[:id])
|
||||
|
@ -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
|
||||
|
7
app/jobs/application_job.rb
Normal file
7
app/jobs/application_job.rb
Normal 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
|
33
app/jobs/batch_generate_weather_arts_job.rb
Normal file
33
app/jobs/batch_generate_weather_arts_job.rb
Normal 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
|
45
app/jobs/generate_weather_art_job.rb
Normal file
45
app/jobs/generate_weather_art_job.rb
Normal 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
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user