Compare commits

..

No commits in common. "a895216bda8c64d9a7f77fe384028266316361a0" and "9d1ff31c5323e12cd008289173c8431ced41febd" have entirely different histories.

22 changed files with 9 additions and 524 deletions

View File

@ -1,48 +0,0 @@
name: Docker
on:
push:
branches:
- dev
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }}
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
-
name: Get Version
id: get_version
run: |
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
-
name: Login to ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{github.actor}}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:dev

View File

@ -47,5 +47,5 @@ jobs:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:main
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}

View File

@ -50,8 +50,6 @@ gem "kaminari", "~> 1.2"
gem "meta-tags", "~> 2.22"
gem "sitemap_generator", "~> 6.3"
gem "ahoy_matey", "~> 5.2"
# gem "whenever", "~> 1.0"
gem "ruby-openai", "~> 7.3"
gem "httparty", "~> 0.22.0"

View File

@ -84,10 +84,6 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ahoy_matey (5.2.1)
activesupport (>= 6.1)
device_detector (>= 1)
safely_block (>= 0.4)
arbre (1.7.0)
activesupport (>= 3.0.0)
ruby2_keywords (>= 0.0.2)
@ -140,7 +136,6 @@ GEM
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
device_detector (1.1.3)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@ -390,7 +385,6 @@ GEM
rubyzip (2.4.1)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safely_block (0.4.1)
securerandom (0.4.1)
selenium-webdriver (4.28.0)
base64 (~> 0.2)
@ -491,7 +485,6 @@ PLATFORMS
DEPENDENCIES
activeadmin (~> 3.2)
ahoy_matey (~> 5.2)
aws-sdk-s3 (~> 1.177)
bootsnap
brakeman

View File

@ -1,31 +0,0 @@
ActiveAdmin.register Ahoy::Event do
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :visit_id, :user_id, :name, :properties, :time
#
# or
#
# permit_params do
# permitted = [:visit_id, :user_id, :name, :properties, :time]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
menu priority: 101, label: "事件统计"
actions :index
index do
column :id
column :name
column :time
column :properties
column :user_id
end
filter :name
filter :time
filter :properties
end

View File

@ -1,34 +0,0 @@
# app/admin/ahoy_management.rb
ActiveAdmin.register_page "Ahoy Management" do
menu label: "访问数据管理", parent: "系统管理"
content title: "访问数据管理" do
columns do
column do
panel "数据统计" do
attributes_table_for :ahoy do
row("总事件数") { Ahoy::Event.count }
row("总访问数") { Ahoy::Visit.count }
row("最早事件") { Ahoy::Event.minimum(:time)&.strftime("%Y-%m-%d %H:%M:%S") }
row("最早访问") { Ahoy::Visit.minimum(:started_at)&.strftime("%Y-%m-%d %H:%M:%S") }
end
end
end
column do
panel "操作" do
div class: "buttons" do
button_to "立即清理旧数据", admin_ahoy_management_cleanup_path, method: :post,
data: { confirm: "确定要清理3个月前的数据吗" },
class: "button"
end
end
end
end
end
page_action :cleanup, method: :post do
CleanAhoyDataWorker.perform_async
redirect_to admin_ahoy_management_path, notice: "清理任务已加入队列"
end
end

View File

@ -1,35 +0,0 @@
ActiveAdmin.register Ahoy::Visit do
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at
#
# or
#
# permit_params do
# permitted = [:visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
menu priority: 100, label: "访问统计"
actions :index
index do
column :id
column :visitor_token
column :ip
column :user_agent
column :started_at
column :city
column :country
column :region
end
filter :started_at
filter :city
filter :country
end

View File

@ -11,53 +11,6 @@ ActiveAdmin.register_page "Dashboard" do
end
end
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
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
end
# 添加一个事件列表面板
panel "最近事件" do
table_for Ahoy::Event.order(time: :desc).limit(10) do
column :time
column :name
column :properties
end
end
# Here is an example of a simple dashboard with columns and panels.
#
# columns do

View File

@ -1,51 +1,8 @@
class ApplicationController < ActionController::Base
include SeoConcern
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
# allow_browser versions: :modern
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
allow_browser versions: :modern
before_action :set_locale
after_action :track_action
protected
def track_action
ahoy.track "Viewed Application", request.path_parameters
end
private

View File

@ -1,7 +1,7 @@
class CitiesController < ApplicationController
def index
@regions = Region.includes(:countries).order(:name)
@cities = City.includes(:country, country: :region).order(:name)
@cities = City.includes(:country, country: :region).active.order(:name)
if params[:region]
@current_region = Region.friendly.find(params[:region])
@ -24,11 +24,6 @@ class CitiesController < ApplicationController
def show
@city = City.friendly.find(params[:id])
ahoy.track "View City", {
city_id: @city.id,
name: @city.name,
event_type: "city_view"
}
set_meta_tags(
title: @city.name,

View File

@ -2,18 +2,6 @@ class WeatherArtsController < ApplicationController
def show
@city = City.friendly.find(params[:city_id])
@weather_art = @city.weather_arts.friendly.find(params[:slug])
ahoy.track "View Weather Art", {
weather_art_id: @weather_art.id,
city_id: @weather_art.city_id,
event_type: "weather_art_view"
}
ahoy.track "View City", {
city_id: @city.id,
name: @city.name,
event_type: "city_view"
}
set_meta_tags(
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",

View File

@ -1,14 +0,0 @@
class Ahoy::Event < ApplicationRecord
# include Ahoy::QueryMethods
self.table_name = "ahoy_events"
belongs_to :visit
belongs_to :user, optional: true
serialize :properties, coder: JSON
def self.ransackable_attributes(auth_object = nil)
[ "id", "id_value", "name", "properties", "time", "user_id", "visit_id" ]
end
end

View File

@ -1,10 +0,0 @@
class Ahoy::Visit < ApplicationRecord
self.table_name = "ahoy_visits"
has_many :events, class_name: "Ahoy::Event"
belongs_to :user, optional: true
def self.ransackable_attributes(auth_object = nil)
[ "app_version", "browser", "city", "country", "device_type", "id", "ip", "landing_page", "latitude", "longitude", "os", "os_version", "platform", "referrer", "referring_domain", "region", "started_at", "user_agent", "user_id", "utm_campaign", "utm_content", "utm_medium", "utm_source", "utm_term", "visit_token", "visitor_token" ]
end
end

View File

@ -5,9 +5,6 @@ class City < ApplicationRecord
has_many :weather_arts, dependent: :destroy
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
delegate :region, to: :country
validates :name, presence: true
@ -20,40 +17,6 @@ class City < ApplicationRecord
scope :by_country, ->(country_id) { where(country_id: country_id) }
scope :active, -> { where(active: true) }
scope :by_popularity, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
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("visit_count DESC")
else
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("visit_count DESC")
end
}
scope :least_popular_active, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
active
.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("visit_count ASC, cities.name ASC")
else
active
.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("visit_count ASC, cities.name ASC")
end
}
def to_s
name
end
@ -102,12 +65,4 @@ class City < ApplicationRecord
def latest_weather_art
weather_arts.order(weather_date: :desc).first
end
def view_count
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
else
Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
end
end
end

View File

@ -5,28 +5,9 @@ class WeatherArt < ApplicationRecord
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 :by_popularity, -> {
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")
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")
end
}
def should_generate_new_friendly_id?
weather_date_changed? || city_id_changed? || super
end
@ -42,12 +23,4 @@ class WeatherArt < ApplicationRecord
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
end

View File

@ -58,7 +58,7 @@
</div>
</section>
</div>
<div class="text-center mt-12 mb-12">
<div class="text-center mt-12">
<%= link_to arts_path, class: "btn btn-primary btn-lg gap-2" do %>
View All Weather Arts
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">

View File

@ -78,13 +78,13 @@
<div class="container mx-auto flex flex-col gap-2">
<div id="busuanzi_container" class="text-xs opacity-70">
<div class="space-x-2">
<span>Page Views: <span id="busuanzi_page_pv"></span></span>
<span>Page Views: <span id="busuanzi_value_page_pv"></span></span>
<span>|</span>
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
<span>Page Visitors: <span id="busuanzi_value_page_uv"></span></span>
<span>|</span>
<span>Total Views: <span id="busuanzi_site_pv"></span></span>
<span>Total Views: <span id="busuanzi_value_site_pv"></span></span>
<span>|</span>
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span>
<span>Total Visitors: <span id="busuanzi_value_site_uv"></span></span>
</div>
</div>

View File

@ -1,32 +0,0 @@
# 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,14 +0,0 @@
class Ahoy::Store < Ahoy::DatabaseStore
end
# set to true for JavaScript tracking
Ahoy.api = true
# set to true for geocoding (and add the geocoder gem to your Gemfile)
# we recommend configuring local geocoding as well
# see https://github.com/ankane/ahoy#geocoding
Ahoy.geocode = false
Ahoy.visit_duration = 30.minutes
Ahoy.server_side_visits = :when_needed
RETENTION_PERIOD = 3.months

View File

@ -10,9 +10,3 @@ refresh_sitemap:
queue: default
description: "Refresh sitemap daily"
enabled: true
clean_ahoy_data:
cron: '0 0 * * 0'
class: CleanAhoyDataWorker
queue: default
enabled: true

View File

@ -1,61 +0,0 @@
class CreateAhoyVisitsAndEvents < ActiveRecord::Migration[8.0]
def change
create_table :ahoy_visits do |t|
t.string :visit_token
t.string :visitor_token
# the rest are recommended but optional
# simply remove any you don't want
# user
t.references :user
# standard
t.string :ip
t.text :user_agent
t.text :referrer
t.string :referring_domain
t.text :landing_page
# technology
t.string :browser
t.string :os
t.string :device_type
# location
t.string :country
t.string :region
t.string :city
t.float :latitude
t.float :longitude
# utm parameters
t.string :utm_source
t.string :utm_medium
t.string :utm_term
t.string :utm_content
t.string :utm_campaign
# native apps
t.string :app_version
t.string :os_version
t.string :platform
t.datetime :started_at
end
add_index :ahoy_visits, :visit_token, unique: true
add_index :ahoy_visits, [ :visitor_token, :started_at ]
create_table :ahoy_events do |t|
t.references :visit
t.references :user
t.string :name
t.text :properties
t.datetime :time
end
add_index :ahoy_events, [ :name, :time ]
end
end

44
db/schema.rb generated
View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_01_26_155239) do
ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace"
t.text "body"
@ -65,48 +65,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_26_155239) do
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
end
create_table "ahoy_events", force: :cascade do |t|
t.integer "visit_id"
t.integer "user_id"
t.string "name"
t.text "properties"
t.datetime "time"
t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time"
t.index ["user_id"], name: "index_ahoy_events_on_user_id"
t.index ["visit_id"], name: "index_ahoy_events_on_visit_id"
end
create_table "ahoy_visits", force: :cascade do |t|
t.string "visit_token"
t.string "visitor_token"
t.integer "user_id"
t.string "ip"
t.text "user_agent"
t.text "referrer"
t.string "referring_domain"
t.text "landing_page"
t.string "browser"
t.string "os"
t.string "device_type"
t.string "country"
t.string "region"
t.string "city"
t.float "latitude"
t.float "longitude"
t.string "utm_source"
t.string "utm_medium"
t.string "utm_term"
t.string "utm_content"
t.string "utm_campaign"
t.string "app_version"
t.string "os_version"
t.string "platform"
t.datetime "started_at"
t.index ["user_id"], name: "index_ahoy_visits_on_user_id"
t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true
t.index ["visitor_token", "started_at"], name: "index_ahoy_visits_on_visitor_token_and_started_at"
end
create_table "cities", force: :cascade do |t|
t.string "name"
t.float "latitude"