feat: add admin management for various entities

- Add menu labels and parents for AdminUser, City, Country, Region, WeatherArt, Ahoy::Event, and Ahoy::Visit.
- Introduce a new page for managing Sidekiq jobs, providing functionality to execute or delete scheduled jobs.
- Adjust batch job for generating weather art by using Sidekiq for improved performance.
- Implement clean-up worker for old Ahoy data and functionalities for refreshing the sitemap.

These changes enhance the administration interface by providing better organization and management tools for backend entities. The addition of Sidekiq jobs management further improves system maintenance capabilities.
This commit is contained in:
songtianlun 2025-01-28 01:03:06 +08:00
parent c68fecf3fa
commit ce5d09b621
13 changed files with 252 additions and 58 deletions

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -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
#

78
app/admin/sidekiq_jobs.rb Normal file
View File

@ -0,0 +1,78 @@
# app/admin/sidekiq_jobs.rb
ActiveAdmin.register_page "Sidekiq Jobs" do
menu priority: 1, label: "Sidekiq 任务管理"
content title: "Sidekiq 任务管理" do
columns do
column do
panel "任务统计" do
stats = Sidekiq::Stats.new
table_for [ "统计数据" ] do
column "处理的任务总数" do
stats.processed
end
column "失败的任务总数" do
stats.failed
end
column "等待中的任务数" do
stats.enqueued
end
end
end
end
end
columns do
column do
panel "计划任务列表" do
table_for Sidekiq::ScheduledSet.new.to_a do
column "任务名称" do |job|
job.item["class"]
end
column "执行时间" do |job|
Time.at(job.at)
end
column "参数" do |job|
job.item["args"].to_s
end
column "操作" do |job|
links = []
links << link_to("立即执行", execute_admin_sidekiq_jobs_path(jid: job.jid), method: :post)
links << link_to("删除", delete_admin_sidekiq_jobs_path(jid: job.jid), method: :delete)
links.join(" | ").html_safe
end
end
end
end
end
end
action_item :refresh do
link_to "刷新", admin_sidekiq_jobs_path
end
# 将 collection_action 改为 page_action
page_action :execute, method: :post do
jid = params[:jid]
job = Sidekiq::ScheduledSet.new.find_job(jid)
if job
job.delete
klass = job.item["class"].constantize
klass.perform_async(*job.item["args"])
redirect_to admin_sidekiq_jobs_path, notice: "任务已立即执行"
else
redirect_to admin_sidekiq_jobs_path, alert: "任务未找到"
end
end
page_action :delete, method: :delete do
jid = params[:jid]
job = Sidekiq::ScheduledSet.new.find_job(jid)
if job
job.delete
redirect_to admin_sidekiq_jobs_path, notice: "任务已删除"
else
redirect_to admin_sidekiq_jobs_path, alert: "任务未找到"
end
end
end

View File

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

View File

@ -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

View File

@ -1,33 +1,36 @@
class BatchGenerateWeatherArtsJob < ApplicationJob
queue_as :default
class BatchGenerateWeatherArtsWorker
include Sidekiq::Worker
GENERATION_INTERVAL = 24.hours
MAX_DURATION = 50.minutes
SLEEP_DURATION = 120.seconds
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
break if Time.current - start_time > MAX_DURATION
Rails.logger.info "Generating weather art for #{city.name}"
GenerateWeatherArtJob.perform_now(city)
sleep 1.minute # 确保不超过API限制
GenerateWeatherArtWorker.perform_async(city.id)
sleep SLEEP_DURATION
end
end
private
def get_eligible_cities
cutoff_time = Time.current - GENERATION_INTERVAL
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) }
.joins("LEFT JOIN (
SELECT city_id, MAX(created_at) as last_generation_time
FROM weather_arts
GROUP BY city_id
) latest_arts ON cities.id = latest_arts.city_id")
.where("latest_arts.last_generation_time IS NULL OR latest_arts.last_generation_time < ?", cutoff_time)
.order(:priority)
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,32 @@
# app/workers/clean_ahoy_data_worker.rb
class CleanAhoyDataWorker
include Sidekiq::Worker
sidekiq_options queue: :default, retry: false
def perform
cleanup_old_events
cleanup_old_visits
log_cleanup_results
end
private
def cleanup_old_events
cutoff_date = 3.months.ago
deleted_events_count = Ahoy::Event.where("time < ?", cutoff_date).delete_all
Rails.logger.info "Deleted #{deleted_events_count} old Ahoy events"
end
def cleanup_old_visits
cutoff_date = 3.months.ago
deleted_visits_count = Ahoy::Visit.where("started_at < ?", cutoff_date).delete_all
Rails.logger.info "Deleted #{deleted_visits_count} old Ahoy visits"
end
def log_cleanup_results
Rails.logger.info "Ahoy cleanup completed at #{Time.current}"
Rails.logger.info "Remaining events: #{Ahoy::Event.count}"
Rails.logger.info "Remaining visits: #{Ahoy::Visit.count}"
end
end

View File

@ -1,45 +1,68 @@
class GenerateWeatherArtJob < ApplicationJob
queue_as :default
class GenerateWeatherArtWorker
include Sidekiq::Worker
def perform(*args)
city = args[0]
return if city.last_weather_fetch&.today?
def perform(city_id)
@city = City.find(city_id)
weather_service = WeatherService.new
ai_service = AiService.new
# 获取天气数据
weather_data = weather_service.get_weather(city.latitude, city.longitude)
weather_data = fetch_weather_data
return unless weather_data
# 生成提示词
prompt = ai_service.generate_prompt(city, weather_data)
prompt = generate_prompt(weather_data)
return unless prompt
# 生成图像
image_url = ai_service.generate_image(prompt)
image_url = generate_image(prompt)
return unless image_url
# 创建天气艺术记录
weather_art = city.weather_arts.create!(
weather_date: Date.today,
**weather_data,
prompt: prompt
)
create_weather_art(weather_data, prompt, image_url)
rescue StandardError => e
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
# 下载并附加图像
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"
)
private
# 更新城市状态
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}"
attr_reader :city
def fetch_weather_data
WeatherService.new.get_weather(city.latitude, city.longitude)
end
def generate_prompt(weather_data)
AiService.new.generate_prompt(city, weather_data)
end
def generate_image(prompt)
AiService.new.generate_image(prompt)
end
def create_weather_art(weather_data, prompt, image_url)
tempfile = nil
ActiveRecord::Base.transaction do
weather_art = city.weather_arts.create!(
weather_date: Date.today,
prompt: prompt,
**weather_data
)
tempfile = Down.download(image_url)
weather_art.image.attach(
io: File.open(tempfile.path),
filename: generate_filename,
content_type: "image/png"
)
weather_art
end
ensure
if tempfile
tempfile.close
tempfile.unlink
end
end
def generate_filename
"#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png"
end
end

View File

@ -0,0 +1,58 @@
class RefreshSitemapWorker
include Sidekiq::Worker
def perform
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
Rails.application.routes.default_url_options[:host] = host
SitemapGenerator::Sitemap.default_host = host
if Rails.env.production?
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)),
aws_access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
aws_secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)),
aws_region: ENV.fetch("AWS_REGION", "wnam"),
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)),
)
else
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)),
aws_access_key_id: ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
aws_secret_access_key: ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)),
aws_region: ENV.fetch("AWS_DEV_REGION", "wnam"),
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)),
)
end
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
SitemapGenerator::Sitemap.create do
add root_path, changefreq: "daily", priority: 1.0
add cities_path, changefreq: "daily", priority: 0.9
add arts_path, changefreq: "daily", priority: 0.9
City.find_each do |city|
add city_path(city),
changefreq: "daily",
priority: 0.8,
lastmod: city.updated_at
end
WeatherArt.includes(:city).find_each do |art|
if art.image.attached?
add city_weather_art_path(art.city, art),
changefreq: "daily",
priority: 0.7,
lastmod: art.updated_at,
images: [ {
loc: url_for(art.image),
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
} ]
end
end
end
# SitemapGenerator::Sitemap.ping_search_engines if Rails.env.production?
Rails.logger.info "Sitemap has been generated and uploaded to S3 successfully"
rescue => e
Rails.logger.error "Error refreshing sitemap: #{e.message}"
end
end