Compare commits
30 Commits
1ab860c739
...
799dfc18ed
Author | SHA1 | Date | |
---|---|---|---|
799dfc18ed | |||
a2c75ba3c2 | |||
e9095ece6e | |||
c20ff296eb | |||
f74f34ce82 | |||
a6cea6b80d | |||
08c584b85b | |||
78722caeb9 | |||
2bcfea30ee | |||
607fc9e8b8 | |||
494ae40088 | |||
3748ea5215 | |||
fd910fb469 | |||
b5c40f2e13 | |||
2d81dd91e7 | |||
2ab495897d | |||
853a1d03ce | |||
d728d7f50e | |||
6e387d1a8c | |||
f7d295b41b | |||
da7fca139c | |||
9cb1467301 | |||
be7856935f | |||
e70763dbe0 | |||
f83600412b | |||
93b24ff50c | |||
c9aef3ddfe | |||
48f3da8913 | |||
a988c49ae0 | |||
|
e2e5e38a7d |
60
.github/workflows/docker.yml
vendored
60
.github/workflows/docker.yml
vendored
@ -1,12 +1,13 @@
|
|||||||
name: Build and Publish Docker Image
|
name: Docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Use docker.io for Docker Hub if empty
|
|
||||||
REGISTRY: docker.io
|
REGISTRY: docker.io
|
||||||
IMAGE_NAME: ${{ github.event.repository.name }}
|
IMAGE_NAME: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
@ -14,40 +15,51 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get version
|
|
||||||
|
- name: Get Version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
GIT_VERSION=$(git describe --dirty --always --long --abbrev=7 --tags)
|
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
|
||||||
VERSION="v${GIT_VERSION}"
|
|
||||||
IMAGE_PREFIX="${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}"
|
- name: Login to ${{ env.REGISTRY }}
|
||||||
LATEST_TAG="${IMAGE_PREFIX}:latest"
|
|
||||||
VERSION_TAG="${IMAGE_PREFIX}:${VERSION}"
|
|
||||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
|
||||||
echo "GIT_VERSION=${GIT_VERSION}" >> $GITHUB_ENV
|
|
||||||
echo "LATEST_TAG=${LATEST_TAG}" >> $GITHUB_OUTPUT
|
|
||||||
echo "VERSION_TAG=${VERSION_TAG}" >> $GITHUB_OUTPUT
|
|
||||||
-
|
|
||||||
name: Login to ${{ env.REGISTRY }}
|
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USER }}
|
username: ${{github.actor}}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
|
||||||
name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
-
|
with:
|
||||||
name: Build and push
|
driver-opts: |
|
||||||
|
network=host
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ env.LATEST_TAG }},${{ env.VERSION_TAG }}
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
|
||||||
|
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}
|
||||||
|
cache-from: |
|
||||||
|
type=local,src=/tmp/.buildx-cache
|
||||||
|
type=registry,ref=${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
@ -16,7 +16,7 @@ WORKDIR /rails
|
|||||||
|
|
||||||
# Install base packages
|
# Install base packages
|
||||||
RUN apt-get update -qq && \
|
RUN apt-get update -qq && \
|
||||||
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
|
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libpq5 redis-tools && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Set production environment
|
# Set production environment
|
||||||
@ -30,7 +30,7 @@ FROM base AS build
|
|||||||
|
|
||||||
# Install packages needed to build gems and node modules
|
# Install packages needed to build gems and node modules
|
||||||
RUN apt-get update -qq && \
|
RUN apt-get update -qq && \
|
||||||
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 && \
|
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 libpq-dev && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
# Install JavaScript dependencies
|
# Install JavaScript dependencies
|
||||||
@ -59,7 +59,7 @@ COPY . .
|
|||||||
RUN bundle exec bootsnap precompile app/ lib/
|
RUN bundle exec bootsnap precompile app/ lib/
|
||||||
|
|
||||||
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
||||||
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
RUN RAILS_BUILD=1 SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
||||||
|
|
||||||
|
|
||||||
RUN rm -rf node_modules
|
RUN rm -rf node_modules
|
||||||
|
8
Gemfile
8
Gemfile
@ -45,6 +45,14 @@ gem "devise", "~> 4.9"
|
|||||||
gem "activeadmin", "~> 3.2"
|
gem "activeadmin", "~> 3.2"
|
||||||
gem "friendly_id", "~> 5.5"
|
gem "friendly_id", "~> 5.5"
|
||||||
|
|
||||||
|
# gem "whenever", "~> 1.0"
|
||||||
|
gem "ruby-openai", "~> 7.3"
|
||||||
|
gem "httparty", "~> 0.22.0"
|
||||||
|
gem "down", "~> 5.4"
|
||||||
|
gem "aws-sdk-s3", "~> 1.177"
|
||||||
|
gem "sidekiq", "~> 7.3"
|
||||||
|
gem "sidekiq-scheduler", "~> 5.0"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
||||||
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
|
||||||
|
64
Gemfile.lock
64
Gemfile.lock
@ -88,6 +88,22 @@ GEM
|
|||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
ruby2_keywords (>= 0.0.2)
|
ruby2_keywords (>= 0.0.2)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
|
aws-eventstream (1.3.0)
|
||||||
|
aws-partitions (1.1035.0)
|
||||||
|
aws-sdk-core (3.215.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
aws-sdk-kms (1.96.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.177.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.11.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bcrypt_pbkdf (1.1.1)
|
bcrypt_pbkdf (1.1.1)
|
||||||
@ -127,11 +143,22 @@ GEM
|
|||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
dotenv (3.1.7)
|
dotenv (3.1.7)
|
||||||
|
down (5.4.2)
|
||||||
|
addressable (~> 2.8)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
|
event_stream_parser (1.0.0)
|
||||||
|
faraday (2.12.2)
|
||||||
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
|
json
|
||||||
|
logger
|
||||||
|
faraday-multipart (1.1.0)
|
||||||
|
multipart-post (~> 2.0)
|
||||||
|
faraday-net_http (3.4.0)
|
||||||
|
net-http (>= 0.5.0)
|
||||||
formtastic (5.0.0)
|
formtastic (5.0.0)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
formtastic_i18n (0.7.0)
|
formtastic_i18n (0.7.0)
|
||||||
@ -145,6 +172,10 @@ GEM
|
|||||||
has_scope (0.8.2)
|
has_scope (0.8.2)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
|
httparty (0.22.0)
|
||||||
|
csv
|
||||||
|
mini_mime (>= 1.0.0)
|
||||||
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.14.6)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
inherited_resources (1.14.0)
|
inherited_resources (1.14.0)
|
||||||
@ -159,6 +190,7 @@ GEM
|
|||||||
jbuilder (2.13.0)
|
jbuilder (2.13.0)
|
||||||
actionview (>= 5.0.0)
|
actionview (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
|
jmespath (1.6.2)
|
||||||
jquery-rails (4.6.0)
|
jquery-rails (4.6.0)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
@ -204,6 +236,11 @@ GEM
|
|||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
minitest (5.25.4)
|
minitest (5.25.4)
|
||||||
msgpack (1.7.5)
|
msgpack (1.7.5)
|
||||||
|
multi_xml (0.7.1)
|
||||||
|
bigdecimal (~> 3.1)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
net-http (0.6.0)
|
||||||
|
uri
|
||||||
net-imap (0.5.5)
|
net-imap (0.5.5)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
@ -300,6 +337,8 @@ GEM
|
|||||||
i18n
|
i18n
|
||||||
rdoc (6.11.0)
|
rdoc (6.11.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
|
redis-client (0.23.2)
|
||||||
|
connection_pool
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
reline (0.6.0)
|
reline (0.6.0)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
@ -335,16 +374,32 @@ GEM
|
|||||||
rubocop-minitest
|
rubocop-minitest
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
|
ruby-openai (7.3.1)
|
||||||
|
event_stream_parser (>= 0.3.0, < 2.0.0)
|
||||||
|
faraday (>= 1)
|
||||||
|
faraday-multipart (>= 1)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
|
rufus-scheduler (3.9.2)
|
||||||
|
fugit (~> 1.1, >= 1.11.1)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.27.0)
|
selenium-webdriver (4.28.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
|
sidekiq (7.3.8)
|
||||||
|
base64
|
||||||
|
connection_pool (>= 2.3.0)
|
||||||
|
logger
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
redis-client (>= 0.22.2)
|
||||||
|
sidekiq-scheduler (5.0.6)
|
||||||
|
rufus-scheduler (~> 3.2)
|
||||||
|
sidekiq (>= 6, < 8)
|
||||||
|
tilt (>= 1.4.0, < 3)
|
||||||
solid_cable (3.0.5)
|
solid_cable (3.0.5)
|
||||||
actioncable (>= 7.2)
|
actioncable (>= 7.2)
|
||||||
activejob (>= 7.2)
|
activejob (>= 7.2)
|
||||||
@ -384,6 +439,7 @@ GEM
|
|||||||
thruster (0.1.10-arm64-darwin)
|
thruster (0.1.10-arm64-darwin)
|
||||||
thruster (0.1.10-x86_64-darwin)
|
thruster (0.1.10-x86_64-darwin)
|
||||||
thruster (0.1.10-x86_64-linux)
|
thruster (0.1.10-x86_64-linux)
|
||||||
|
tilt (2.6.0)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
turbo-rails (2.0.11)
|
turbo-rails (2.0.11)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
@ -425,13 +481,16 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
activeadmin (~> 3.2)
|
activeadmin (~> 3.2)
|
||||||
|
aws-sdk-s3 (~> 1.177)
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
capybara
|
capybara
|
||||||
cssbundling-rails
|
cssbundling-rails
|
||||||
debug
|
debug
|
||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
|
down (~> 5.4)
|
||||||
friendly_id (~> 5.5)
|
friendly_id (~> 5.5)
|
||||||
|
httparty (~> 0.22.0)
|
||||||
jbuilder
|
jbuilder
|
||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
kamal
|
kamal
|
||||||
@ -440,7 +499,10 @@ DEPENDENCIES
|
|||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.0.1)
|
rails (~> 8.0.1)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
ruby-openai (~> 7.3)
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
|
sidekiq (~> 7.3)
|
||||||
|
sidekiq-scheduler (~> 5.0)
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
solid_queue
|
solid_queue
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
ActiveAdmin.register WeatherArt do
|
ActiveAdmin.register WeatherArt do
|
||||||
|
controller do
|
||||||
|
def find_resource
|
||||||
|
scoped_collection.friendly.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
# See permitted parameters documentation:
|
# See permitted parameters documentation:
|
||||||
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
|
||||||
#
|
#
|
||||||
@ -16,9 +21,11 @@ ActiveAdmin.register WeatherArt do
|
|||||||
permit_params :city_id, :weather_date, :description, :temperature,
|
permit_params :city_id, :weather_date, :description, :temperature,
|
||||||
:feeling_temp, :humidity, :wind_scale, :wind_speed,
|
:feeling_temp, :humidity, :wind_scale, :wind_speed,
|
||||||
:precipitation, :pressure, :visibility, :cloud,
|
:precipitation, :pressure, :visibility, :cloud,
|
||||||
:prompt, :image
|
:prompt, :image, :slug
|
||||||
|
|
||||||
remove_filter :image_attachment, :image_blob
|
remove_filter :image_attachment, :image_blob
|
||||||
|
filter :city_id
|
||||||
|
filter :weather_data
|
||||||
|
|
||||||
index do
|
index do
|
||||||
selectable_column
|
selectable_column
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
class CitiesController < ApplicationController
|
class CitiesController < ApplicationController
|
||||||
def index
|
def index
|
||||||
@cities = City.all.order(:name)
|
@cities = City.all.order(:name)
|
||||||
|
@regions = Region.includes(:countries).order(:name)
|
||||||
|
@cities = City.includes(:country, country: :region).active.order(:name)
|
||||||
|
|
||||||
|
if params[:region]
|
||||||
|
@current_region = Region.friendly.find(params[:region])
|
||||||
|
@cities = @cities.by_region(@current_region.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:country]
|
||||||
|
@current_country = Country.friendly.find(params[:country])
|
||||||
|
@cities = @cities.by_country(@current_country.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
class WeatherArtsController < ApplicationController
|
class WeatherArtsController < ApplicationController
|
||||||
def show
|
def show
|
||||||
@city = City.friendly.find(params[:city_id])
|
@city = City.friendly.find(params[:city_id])
|
||||||
@weather_art = @city.weather_arts.find(params[:id])
|
@weather_art = @city.weather_arts.friendly.find(params[:slug])
|
||||||
end
|
end
|
||||||
end
|
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
|
@ -1,6 +1,6 @@
|
|||||||
class City < ApplicationRecord
|
class City < ApplicationRecord
|
||||||
extend FriendlyId
|
extend FriendlyId
|
||||||
friendly_id :name, use: :slugged
|
friendly_id :slug_candidates, use: :slugged
|
||||||
belongs_to :country
|
belongs_to :country
|
||||||
|
|
||||||
has_many :weather_arts, dependent: :destroy
|
has_many :weather_arts, dependent: :destroy
|
||||||
@ -11,10 +11,21 @@ class City < ApplicationRecord
|
|||||||
|
|
||||||
delegate :region, to: :country
|
delegate :region, to: :country
|
||||||
|
|
||||||
|
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
|
||||||
|
scope :by_country, ->(country_id) { where(country_id: country_id) }
|
||||||
|
scope :active, -> { where(active: true) }
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def slug_candidates
|
||||||
|
[
|
||||||
|
:name,
|
||||||
|
[ :country, :name ]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def localized_name
|
def localized_name
|
||||||
I18n.t("cities.#{name.parameterize.underscore}")
|
I18n.t("cities.#{name.parameterize.underscore}")
|
||||||
end
|
end
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
class WeatherArt < ApplicationRecord
|
class WeatherArt < ApplicationRecord
|
||||||
belongs_to :city
|
extend FriendlyId
|
||||||
|
friendly_id :weather_date, use: :slugged
|
||||||
|
|
||||||
|
belongs_to :city
|
||||||
has_one_attached :image
|
has_one_attached :image
|
||||||
|
|
||||||
validates :weather_date, presence: true
|
validates :weather_date, presence: true
|
||||||
validates :city_id, presence: true
|
validates :city_id, presence: true
|
||||||
|
|
||||||
|
def should_generate_new_friendly_id?
|
||||||
|
weather_date_changed? || city_id_changed? || super
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
|
||||||
|
end
|
||||||
|
|
||||||
def self.ransackable_associations(auth_object = nil)
|
def self.ransackable_associations(auth_object = nil)
|
||||||
[ "city", "image_attachment", "image_blob" ]
|
[ "city", "image_attachment", "image_blob" ]
|
||||||
end
|
end
|
||||||
|
64
app/services/ai_service.rb
Normal file
64
app/services/ai_service.rb
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
class AiService
|
||||||
|
def initialize
|
||||||
|
@client = OpenAI::Client.new(
|
||||||
|
access_token: Rails.application.credentials.openai.token,
|
||||||
|
uri_base: Rails.application.credentials.openai.uri,
|
||||||
|
request_timeout: 240
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_prompt(city, weather_data)
|
||||||
|
response = @client.chat(
|
||||||
|
parameters: {
|
||||||
|
model: "gpt-4",
|
||||||
|
messages: [ {
|
||||||
|
role: "system",
|
||||||
|
content: "You are a professional artist creating prompts for DALL-E 3. Create realistic, artistic weather scenes featuring iconic landmarks."
|
||||||
|
}, {
|
||||||
|
role: "user",
|
||||||
|
content: generate_prompt_request(city, weather_data)
|
||||||
|
} ],
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 300
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response.dig("choices", 0, "message", "content")
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_image(prompt)
|
||||||
|
response = @client.images.generate(
|
||||||
|
parameters: {
|
||||||
|
model: "dall-e-3",
|
||||||
|
prompt: prompt,
|
||||||
|
size: "1792x1024",
|
||||||
|
quality: "standard",
|
||||||
|
n: 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response.dig("data", 0, "url")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_prompt_request(city, weather_data)
|
||||||
|
<<~PROMPT
|
||||||
|
Create a DALL-E 3 prompt for a weather scene in #{city.name}, #{city.country.name}.
|
||||||
|
|
||||||
|
Weather conditions:
|
||||||
|
- Temperature: #{weather_data[:temperature]}°C
|
||||||
|
- Weather: #{weather_data[:description]}
|
||||||
|
- Cloud cover: #{weather_data[:cloud]}%
|
||||||
|
- Time: Early morning
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Feature iconic landmarks or architecture from #{city.name}
|
||||||
|
- Realistic style
|
||||||
|
- Weather conditions should be clearly visible
|
||||||
|
- Atmospheric and artistic composition
|
||||||
|
|
||||||
|
Generate a detailed, creative prompt that will produce a beautiful and realistic image.
|
||||||
|
PROMPT
|
||||||
|
end
|
||||||
|
end
|
37
app/services/weather_service.rb
Normal file
37
app/services/weather_service.rb
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# app/services/weather_service.rb
|
||||||
|
class WeatherService
|
||||||
|
include HTTParty
|
||||||
|
base_uri Rails.application.credentials.dig(:qweather, :uri)
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@api_key = Rails.application.credentials.qweather.token
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_weather(latitude, longitude)
|
||||||
|
Rails.logger.debug "Get Weather for #{latitude},#{longitude}"
|
||||||
|
response = self.class.get(
|
||||||
|
"/weather/now",
|
||||||
|
headers: {
|
||||||
|
"X-QW-Api-Key" => "#{@api_key}"
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
location: "#{longitude},#{latitude}"
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil unless response.success?
|
||||||
|
|
||||||
|
data = response["now"]
|
||||||
|
{
|
||||||
|
temperature: data["temp"].to_f,
|
||||||
|
feeling_temp: data["feelsLike"].to_f,
|
||||||
|
humidity: data["humidity"].to_f,
|
||||||
|
wind_scale: "#{data['windScale']}级",
|
||||||
|
wind_speed: data["windSpeed"].to_f,
|
||||||
|
precipitation: data["precip"].to_f,
|
||||||
|
pressure: data["pressure"].to_f,
|
||||||
|
visibility: data["vis"].to_f,
|
||||||
|
cloud: data["cloud"].to_f,
|
||||||
|
description: data["text"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
126
app/views/cities/_city.html.erb
Normal file
126
app/views/cities/_city.html.erb
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!-- app/views/cities/_city.html.erb -->
|
||||||
|
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
|
||||||
|
<% if city.latest_weather_art&.image&.attached? %>
|
||||||
|
<!-- 图片和主要信息区域 -->
|
||||||
|
<div class="relative">
|
||||||
|
<!-- 图片 -->
|
||||||
|
<figure class="aspect-[16/9] overflow-hidden">
|
||||||
|
<%= image_tag city.latest_weather_art.image,
|
||||||
|
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- 渐变遮罩 - 使用多层渐变提供更好的文字可读性 -->
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-transparent"></div>
|
||||||
|
|
||||||
|
<!-- 温度显示 - 放在右上角 -->
|
||||||
|
<div class="absolute top-4 right-4 bg-black/30 backdrop-blur-sm rounded-xl px-3 py-2 text-white">
|
||||||
|
<div class="text-2xl font-bold leading-none">
|
||||||
|
<%= city.latest_weather_art.temperature %>°C
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-white/80">
|
||||||
|
Feels <%= city.latest_weather_art.feeling_temp %>°C
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 城市名称和位置 - 放在底部 -->
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 p-6">
|
||||||
|
<h3 class="text-2xl font-display text-white font-bold mb-2 drop-shadow-lg">
|
||||||
|
<%= city.name %>
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center gap-2 text-white/90 text-sm drop-shadow-lg">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
</svg>
|
||||||
|
<%= city.country.name %>, <%= city.region.name %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部信息区域 -->
|
||||||
|
<div class="card-body bg-base-100">
|
||||||
|
<!-- 天气信息和更新时间 -->
|
||||||
|
<div class="flex items-center justify-between text-sm text-base-content/70 mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||||
|
</svg>
|
||||||
|
<%= city.latest_weather_art.description %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<%= city.latest_weather_art.weather_date.strftime("%b %d, %Y") %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 天气详情 -->
|
||||||
|
<div class="grid grid-cols-3 gap-2 mb-4">
|
||||||
|
<div class="stat bg-base-200/50 rounded-lg p-2">
|
||||||
|
<div class="stat-title text-xs">Humidity</div>
|
||||||
|
<div class="stat-value text-lg"><%= city.latest_weather_art.humidity %>%</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat bg-base-200/50 rounded-lg p-2">
|
||||||
|
<div class="stat-title text-xs">Wind</div>
|
||||||
|
<div class="stat-value text-lg"><%= city.latest_weather_art.wind_scale %></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat bg-base-200/50 rounded-lg p-2">
|
||||||
|
<div class="stat-title text-xs">Visibility</div>
|
||||||
|
<div class="stat-value text-lg"><%= city.latest_weather_art.visibility %>km</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<%= link_to city_path(city),
|
||||||
|
class: "btn btn-primary btn-sm gap-2" do %>
|
||||||
|
View Details
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% else %>
|
||||||
|
<!-- 无图片时的备用显示 -->
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 class="card-title font-display text-2xl mb-3"><%= city.name %></h3>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-base-content/70 mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
</svg>
|
||||||
|
<%= city.country.name %>, <%= city.region.name %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-base-200 rounded-lg p-4 mb-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>Lat: <%= city.latitude %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>Long: <%= city.longitude %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<%= link_to city_path(city),
|
||||||
|
class: "btn btn-primary btn-sm gap-2" do %>
|
||||||
|
View Details
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
@ -1,35 +1,126 @@
|
|||||||
<div class="container mx-auto px-4 py-16">
|
<!-- app/views/cities/index.html.erb -->
|
||||||
<div class="text-center mb-16 space-y-4">
|
<div class="min-h-screen">
|
||||||
<h1 class="text-4xl md:text-5xl font-display font-bold">Explore Cities</h1>
|
<!-- 页面标题和背景 -->
|
||||||
<p class="text-xl text-base-content/70 max-w-2xl mx-auto">
|
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).first %>
|
||||||
|
<div class="relative bg-base-100">
|
||||||
|
<!-- 背景图像和渐变 -->
|
||||||
|
<% if featured_art&.image&.attached? %>
|
||||||
|
<div class="absolute inset-0 h-[60vh] overflow-hidden">
|
||||||
|
<%= image_tag featured_art.image,
|
||||||
|
class: "w-full h-full object-cover object-center" %>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- 标题内容 -->
|
||||||
|
<div class="relative pt-24 pb-32">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="max-w-3xl mx-auto text-center space-y-6">
|
||||||
|
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
|
||||||
|
Explore Cities
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl md:text-2xl text-base-content/70 font-light max-w-2xl mx-auto">
|
||||||
Discover AI-generated weather art from cities around the world
|
Discover AI-generated weather art from cities around the world
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- 特色图片信息 -->
|
||||||
|
<% if featured_art %>
|
||||||
|
<div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm">
|
||||||
|
Latest from
|
||||||
|
<span class="font-semibold"><%= featured_art.city.name %></span>,
|
||||||
|
<%= featured_art.city.country.name %>
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选导航 - 使用下拉菜单 -->
|
||||||
|
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="py-3 flex items-center justify-between gap-4">
|
||||||
|
<!-- 左侧筛选器 -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- 区域选择下拉框 -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-ghost gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<%= @current_region&.name || 'All Regions' %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<%= link_to cities_path,
|
||||||
|
class: "#{@current_region ? '' : 'active'}" do %>
|
||||||
|
All Regions
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<% @regions.each do |region| %>
|
||||||
|
<li>
|
||||||
|
<%= link_to region.name,
|
||||||
|
cities_path(region: region.slug),
|
||||||
|
class: "#{@current_region == region ? 'active' : ''}" %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 国家选择下拉框 (如果选择了区域) -->
|
||||||
|
<% if @current_region %>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-ghost gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
|
||||||
|
</svg>
|
||||||
|
<%= @current_country&.name || "All Countries" %>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
|
||||||
|
<li>
|
||||||
|
<%= link_to "All in #{@current_region.name}",
|
||||||
|
cities_path(region: @current_region.slug),
|
||||||
|
class: "#{@current_country ? '' : 'active'}" %>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<% @current_region.countries.order(:name).each do |country| %>
|
||||||
|
<li>
|
||||||
|
<%= link_to country.name,
|
||||||
|
cities_path(region: @current_region.slug, country: country.slug),
|
||||||
|
class: "#{@current_country == country ? 'active' : ''}" %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧结果统计 -->
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
|
||||||
|
<% if @current_country %>
|
||||||
|
in <%= @current_country.name %>
|
||||||
|
<% elsif @current_region %>
|
||||||
|
in <%= @current_region.name %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 城市网格 -->
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<% @cities.each do |city| %>
|
<%= render partial: 'city', collection: @cities %>
|
||||||
<% latest_art = city.weather_arts.last %>
|
|
||||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group">
|
|
||||||
<% if latest_art&.image&.attached? %>
|
|
||||||
<figure class="relative aspect-[16/9] overflow-hidden">
|
|
||||||
<%= image_tag latest_art.image,
|
|
||||||
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
|
||||||
</figure>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="card-body relative">
|
|
||||||
<h2 class="card-title font-display text-2xl"><%= city.localized_name %></h2>
|
|
||||||
<div class="text-base-content/70">
|
|
||||||
<p>Lat: <%= city.latitude %></p>
|
|
||||||
<p>Long: <%= city.longitude %></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-end mt-4">
|
|
||||||
<%= link_to "View Weather Art", city_path(city),
|
|
||||||
class: "btn btn-primary" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
33
app/workers/batch_generate_weather_arts_worker.rb
Normal file
33
app/workers/batch_generate_weather_arts_worker.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
class BatchGenerateWeatherArtsWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
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
|
43
app/workers/generate_weather_art_worker.rb
Normal file
43
app/workers/generate_weather_art_worker.rb
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
class GenerateWeatherArtWorker
|
||||||
|
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
|
41
compose.yaml
Normal file
41
compose.yaml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: songtianlun/today_ai_weather:latest
|
||||||
|
ports:
|
||||||
|
- "2222:3000"
|
||||||
|
environment:
|
||||||
|
- RAILS_ENV=production
|
||||||
|
- DATABASE_URL=postgresql://postgres:xxx@db:5432/db
|
||||||
|
- RAILS_MASTER_KEY=xxx
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
|
||||||
|
sidekiq:
|
||||||
|
image: songtianlun/today_ai_weather:latest
|
||||||
|
command: bundle exec sidekiq
|
||||||
|
environment:
|
||||||
|
- RAILS_ENV=production
|
||||||
|
- DATABASE_URL=postgresql://postgres:xxx@db:5432/db
|
||||||
|
- RAILS_MASTER_KEY=xxx
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
volumes:
|
||||||
|
- ./data/pg:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=xxx
|
||||||
|
- POSTGRES_DB=db
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
command: redis-server --appendonly yes
|
@ -1 +1 @@
|
|||||||
GwoFK6l/nbuNuzqsSphWsWWAkF3rF3LqQJXbBlMhZqleAE/wXGYzxgansBULOsF+/0hMDNT+AMQdLKPd+J+Oo55LNVHd9Dk6iJ4wtRJm5Ya4czrPTqv4dezNwRFm6soC085fUONQ8hxShenYWSNtUD+X9HmjG3xDYOMk8XHJRdz+TzUqzk8+sTee1xFTP3ZFTF1HYHE+3csz22rA0KBekDbSL+HK/rlKvNK3UJK/LM71V1BmaPSBC50X+D7TkGZGatMRLPwM3C3m/6PPvB838zeH7fm2maPx2/Y4zPZhTarUg3/U+R0wGfGbSCdMMBy/A+04C7KxadTn4Ivk3zXI3zloEbgY+s1pq6RZNKuLfQFUDzpDtdnC7tjYRnMBo6C3c8iAgI0gnnYHY0zHPws+umUlS/kdiWBn6jvsNc6zz6bSyBY8xIuL4zm/Aa4wQJc5u1wLTQW00nXKJvYo7bvZsWhusATQlPm+VK+U0rdh6uAyaeatwc951KB3--N7qeSFBaRTq/dtNy--L8fWHRQMpTXu5+6E1HsNaQ==
|
0Upt112zhMfb3aADOgxSWUCTTUNgtTrpoivR9+KJFKcazTjqRauvQ9W+uicYrjJjv497gHMFhusWZysDl1/uwHHaoHvyp7auIjRHiOR1YKfRLgbMSYMgSOKlP4HbiZ66hjhT+4T50JjmwwLcPFtgPaZG3lasTAXejc9ejiSn+OsIGvJje5MXnPREId5pvC/SNW4uCYMe6OEsfhTlsj8hES7odHJzMsGQGGuVkcg1EimogOE9oWK6FS0bj45R+7HCiIYlbcmDkRNraGDMGJI+oaxx2mi+34ENoxqfbYEv4PhsWPYcOROK8FyrpXwvRG/a1J8LFxTLkVloKsBIInLojvd9u8zwpcTOtsaHOxsQV7gLzUt2GbUiT4EZUdzHpIDRboJEuVxm6ioBctcJ744xnkn5rxOoJH+JWRhxf+CVLYojcYMdkcyGEOHv0nXtpWtu/xsJT3fTqJBLuyGWNQUfTDsw3qQDUOrsgSNvXvhEu1iWptoByp4d0adaCDS0+TVt+BFg2xPzSnIU3IAXgSnhyLTVd87jxBMHtcngIj4HAj+nDIs65KT4dk4Y+ns3eWE345Z0CzUz1J6M8ikva0cQiBB6wxMl1yghysCcSfKX4z/KbuJv6StQJIqw4HkDZ9youXEbnmiXTxuEaC0Yg3SE0dFf5k9Rj5govLMdCf1Msj04RQHt0b08s3UdTgegriru+j7dC6yTaDbHkrNorREME3SENzdksI1D5k08D4Z+TYpO/+yyZE5qKELEse9TgJiFZ6g1H2sFs9zCxBDif67ovFSqrRaNgP7qBY8uEfFNVP6Y6eSnfccJlFBGlwflhwwA1osit+5rKlvhqQs37qsmFZRuj9BdrAJbdr/0ogEbLyeTEw3Zq+0m0/Vf4j5ueCv2eDpk/gSQdl869v+5anLTqMUWM4KV1wS1OrPkMC5XkZHLUo1/1+Dl6FKfZhWydRso1R9WL5B0Jk7UEWMPX0qoxxuixR1jwWzYmlehq0iNw7hQPdgwCbDtRxEMPr8MkUnG5Fd0YZUfnhHJ+EWVCZHKhUJclRSB9LnDKVPJr/oFuOHLmJg6Vll4WHCnIK9nCHdr7ed6O89vA5Z8DRlE/6yAXFlPKrCldWYbrL3BAgzQK8MxD5fzgzRkGiwVEtQwhbtbdDHUJ9rdAEmLJZj1UJXLIhX9jFspB9stHqyRxrcef/Y+h9dBvuFdvSIFEXA0TWHintx9YDgsJb591d6rjJn+JYEmZs9UONEEuiijPf9waqTb0oIrUQwnlUOX69bBipiU5ZQ2yQvepuRY3ooK3IGHvngxZPT0F8Mfh5Zcx9GGJd17LZb9DJOs7uOlrpXydToq/3Qm1q6TmgjbFYpC6iyrhDyE5XLL/XwVavh7m2QYqVbUXk2tGk2C0UR6CTE6tsV1+kDs1nylIEgAA5Nr43HQtRAXPmo0k8Txy5wMMIbLE1keqjl7XdGPnLsX9CJi6ue6KZxhigwcdakvW6Y=--H101ebDKrzdM39ll--a5R5klrhU6QgghGPjjWfAQ==
|
@ -29,7 +29,8 @@ Rails.application.configure do
|
|||||||
config.cache_store = :memory_store
|
config.cache_store = :memory_store
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||||
config.active_storage.service = :local
|
# config.active_storage.service = :local
|
||||||
|
config.active_storage.service = :amazon_dev
|
||||||
|
|
||||||
# Don't care if the mailer can't send.
|
# Don't care if the mailer can't send.
|
||||||
config.action_mailer.raise_delivery_errors = false
|
config.action_mailer.raise_delivery_errors = false
|
||||||
|
@ -22,7 +22,7 @@ Rails.application.configure do
|
|||||||
# config.asset_host = "http://assets.example.com"
|
# config.asset_host = "http://assets.example.com"
|
||||||
|
|
||||||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||||
config.active_storage.service = :local
|
config.active_storage.service = ENV["RAILS_BUILD"] ? :build : :amazon
|
||||||
|
|
||||||
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
|
||||||
config.assume_ssl = true
|
config.assume_ssl = true
|
||||||
@ -50,7 +50,8 @@ Rails.application.configure do
|
|||||||
config.cache_store = :solid_cache_store
|
config.cache_store = :solid_cache_store
|
||||||
|
|
||||||
# Replace the default in-process and non-durable queuing backend for Active Job.
|
# Replace the default in-process and non-durable queuing backend for Active Job.
|
||||||
config.active_job.queue_adapter = :solid_queue
|
# config.active_job.queue_adapter = :solid_queue
|
||||||
|
config.active_job.queue_adapter = :sidekiq
|
||||||
config.solid_queue.connects_to = { database: { writing: :queue } }
|
config.solid_queue.connects_to = { database: { writing: :queue } }
|
||||||
|
|
||||||
# Ignore bad email addresses and do not raise email delivery errors.
|
# Ignore bad email addresses and do not raise email delivery errors.
|
||||||
|
20
config/initializers/sidekiq.rb
Normal file
20
config/initializers/sidekiq.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
require "sidekiq"
|
||||||
|
require "sidekiq-scheduler"
|
||||||
|
|
||||||
|
Sidekiq.configure_server do |config|
|
||||||
|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
|
||||||
|
config.logger.level = Logger::INFO
|
||||||
|
config.on(:startup) do
|
||||||
|
schedule_file = "config/sidekiq.yml"
|
||||||
|
if File.exist?(schedule_file)
|
||||||
|
Sidekiq::Scheduler.enabled = true
|
||||||
|
Sidekiq::Scheduler.dynamic = true
|
||||||
|
Sidekiq.schedule = YAML.load_file(schedule_file)
|
||||||
|
Sidekiq::Scheduler.reload_schedule!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidekiq.configure_client do |config|
|
||||||
|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
|
||||||
|
end
|
@ -1,8 +1,10 @@
|
|||||||
|
require "sidekiq/web"
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
root "home#index"
|
root "home#index"
|
||||||
|
|
||||||
resources :cities, only: [ :index, :show ] do
|
resources :cities, only: [ :index, :show ] do
|
||||||
resources :weather_arts, only: [ :show ]
|
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
|
||||||
end
|
end
|
||||||
|
|
||||||
# namespace :admin do
|
# namespace :admin do
|
||||||
@ -17,6 +19,11 @@ Rails.application.routes.draw do
|
|||||||
get "home/index"
|
get "home/index"
|
||||||
devise_for :admin_users, ActiveAdmin::Devise.config
|
devise_for :admin_users, ActiveAdmin::Devise.config
|
||||||
ActiveAdmin.routes(self)
|
ActiveAdmin.routes(self)
|
||||||
|
|
||||||
|
# mount Sidekiq::Web => '/sidekiq'
|
||||||
|
authenticate :admin_user do
|
||||||
|
mount Sidekiq::Web => "/sidekiq"
|
||||||
|
end
|
||||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
|
23
config/schedule.rb
Normal file
23
config/schedule.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Use this file to easily define all of your cron jobs.
|
||||||
|
#
|
||||||
|
# It's helpful, but not entirely necessary to understand cron before proceeding.
|
||||||
|
# http://en.wikipedia.org/wiki/Cron
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# set :output, "/path/to/my/cron_log.log"
|
||||||
|
#
|
||||||
|
# every 2.hours do
|
||||||
|
# command "/usr/bin/some_great_command"
|
||||||
|
# runner "MyModel.some_method"
|
||||||
|
# rake "some:great:rake:task"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# every 4.days do
|
||||||
|
# runner "AnotherModel.prune_old_records"
|
||||||
|
# end
|
||||||
|
# every 2.hour do
|
||||||
|
# runner "BatchGenerateWeatherArtsJob.perform_later"
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Learn more: http://github.com/javan/whenever
|
5
config/sidekiq.yml
Normal file
5
config/sidekiq.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
batch_generate_weather:
|
||||||
|
cron: '0 */2 * * *'
|
||||||
|
class: BatchGenerateWeatherArtsWorker
|
||||||
|
description: "Generate weather arts every 2 hours"
|
||||||
|
enabled: true
|
@ -6,6 +6,10 @@ local:
|
|||||||
service: Disk
|
service: Disk
|
||||||
root: <%= Rails.root.join("storage") %>
|
root: <%= Rails.root.join("storage") %>
|
||||||
|
|
||||||
|
build:
|
||||||
|
service: Disk
|
||||||
|
root: <%= Rails.root.join("tmp/storage") %>
|
||||||
|
|
||||||
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
||||||
# amazon:
|
# amazon:
|
||||||
# service: S3
|
# service: S3
|
||||||
@ -13,6 +17,21 @@ local:
|
|||||||
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
||||||
# region: us-east-1
|
# region: us-east-1
|
||||||
# bucket: your_own_bucket-<%= Rails.env %>
|
# bucket: your_own_bucket-<%= Rails.env %>
|
||||||
|
amazon_dev:
|
||||||
|
service: S3
|
||||||
|
access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)) %>
|
||||||
|
secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)) %>
|
||||||
|
region: <%= ENV.fetch("AWS_REGION", "wnam") %>
|
||||||
|
bucket: <%= ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)) %>
|
||||||
|
endpoint: <%= ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)) %>
|
||||||
|
|
||||||
|
amazon:
|
||||||
|
service: S3
|
||||||
|
access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)) %>
|
||||||
|
secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)) %>
|
||||||
|
region: <%= ENV.fetch("AWS_REGION", "wnam") %>
|
||||||
|
bucket: <%= ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) %>
|
||||||
|
endpoint: <%= ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)) %>
|
||||||
|
|
||||||
# Remember not to checkin your GCS keyfile to a repository
|
# Remember not to checkin your GCS keyfile to a repository
|
||||||
# google:
|
# google:
|
||||||
|
6
db/migrate/20250122053220_add_slug_to_weather_arts.rb
Normal file
6
db/migrate/20250122053220_add_slug_to_weather_arts.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
class AddSlugToWeatherArts < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :weather_arts, :slug, :string
|
||||||
|
add_index :weather_arts, :slug, unique: true
|
||||||
|
end
|
||||||
|
end
|
17
db/schema.rb
generated
17
db/schema.rb
generated
@ -10,17 +10,14 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
|
ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
|
||||||
enable_extension "pg_catalog.plpgsql"
|
|
||||||
|
|
||||||
create_table "active_admin_comments", force: :cascade do |t|
|
create_table "active_admin_comments", force: :cascade do |t|
|
||||||
t.string "namespace"
|
t.string "namespace"
|
||||||
t.text "body"
|
t.text "body"
|
||||||
t.string "resource_type"
|
t.string "resource_type"
|
||||||
t.bigint "resource_id"
|
t.integer "resource_id"
|
||||||
t.string "author_type"
|
t.string "author_type"
|
||||||
t.bigint "author_id"
|
t.integer "author_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["author_type", "author_id"], name: "index_active_admin_comments_on_author"
|
t.index ["author_type", "author_id"], name: "index_active_admin_comments_on_author"
|
||||||
@ -80,7 +77,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "slug"
|
t.string "slug"
|
||||||
t.bigint "country_id", null: false
|
t.integer "country_id", null: false
|
||||||
t.index ["country_id"], name: "index_cities_on_country_id"
|
t.index ["country_id"], name: "index_cities_on_country_id"
|
||||||
t.index ["slug"], name: "index_cities_on_slug", unique: true
|
t.index ["slug"], name: "index_cities_on_slug", unique: true
|
||||||
end
|
end
|
||||||
@ -89,7 +86,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
|
|||||||
t.string "name"
|
t.string "name"
|
||||||
t.string "code"
|
t.string "code"
|
||||||
t.string "slug"
|
t.string "slug"
|
||||||
t.bigint "region_id", null: false
|
t.integer "region_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["code"], name: "index_countries_on_code", unique: true
|
t.index ["code"], name: "index_countries_on_code", unique: true
|
||||||
@ -119,7 +116,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "weather_arts", force: :cascade do |t|
|
create_table "weather_arts", force: :cascade do |t|
|
||||||
t.bigint "city_id", null: false
|
t.integer "city_id", null: false
|
||||||
t.date "weather_date"
|
t.date "weather_date"
|
||||||
t.string "description"
|
t.string "description"
|
||||||
t.decimal "temperature"
|
t.decimal "temperature"
|
||||||
@ -134,7 +131,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_21_020653) do
|
|||||||
t.text "prompt"
|
t.text "prompt"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.string "slug"
|
||||||
t.index ["city_id"], name: "index_weather_arts_on_city_id"
|
t.index ["city_id"], name: "index_weather_arts_on_city_id"
|
||||||
|
t.index ["slug"], name: "index_weather_arts_on_slug", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
|
17
db/seeds.rb
17
db/seeds.rb
@ -7,7 +7,8 @@
|
|||||||
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
||||||
# MovieGenre.find_or_create_by!(name: genre_name)
|
# MovieGenre.find_or_create_by!(name: genre_name)
|
||||||
# end
|
# end
|
||||||
AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development?
|
# AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development?
|
||||||
|
AdminUser.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password')
|
||||||
|
|
||||||
WeatherArt.delete_all
|
WeatherArt.delete_all
|
||||||
City.delete_all
|
City.delete_all
|
||||||
@ -139,19 +140,7 @@ Country.create!([
|
|||||||
Dir[Rails.root.join('db/seeds/cities/*.rb')].sort.each do |file|
|
Dir[Rails.root.join('db/seeds/cities/*.rb')].sort.each do |file|
|
||||||
require file
|
require file
|
||||||
end
|
end
|
||||||
|
guangzhou = City.find_by name: 'Guangzhou'
|
||||||
china = Country.find_by(code: 'CN')
|
|
||||||
guangzhou = City.create!(
|
|
||||||
name: 'Guangzhou',
|
|
||||||
latitude: 23.1291,
|
|
||||||
longitude: 113.2644,
|
|
||||||
priority: 50,
|
|
||||||
country: china,
|
|
||||||
timezone: 'Asia/Shanghai',
|
|
||||||
active: true,
|
|
||||||
last_weather_fetch: 10.hours.ago,
|
|
||||||
last_image_generation: 10.hours.ago
|
|
||||||
)
|
|
||||||
|
|
||||||
guangzhou_weather_art = WeatherArt.create!(
|
guangzhou_weather_art = WeatherArt.create!(
|
||||||
city: guangzhou,
|
city: guangzhou,
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Australia/Sydney',
|
timezone: 'Australia/Sydney',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 80,
|
priority: 80,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Melbourne',
|
name: 'Melbourne',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Australia/Melbourne',
|
timezone: 'Australia/Melbourne',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 75,
|
priority: 75,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Dhaka',
|
timezone: 'Asia/Dhaka',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 85,
|
priority: 85,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'America/Sao_Paulo',
|
timezone: 'America/Sao_Paulo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 80,
|
priority: 80,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -7,6 +7,6 @@ City.create!(
|
|||||||
country: canada,
|
country: canada,
|
||||||
timezone: 'America/Toronto',
|
timezone: 'America/Toronto',
|
||||||
active: true,
|
active: true,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
)
|
)
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Beijing',
|
name: 'Beijing',
|
||||||
@ -20,8 +20,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shenzhen',
|
name: 'Shenzhen',
|
||||||
@ -31,8 +31,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Guangzhou',
|
name: 'Guangzhou',
|
||||||
@ -42,8 +42,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chengdu',
|
name: 'Chengdu',
|
||||||
@ -53,8 +53,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tianjin',
|
name: 'Tianjin',
|
||||||
@ -64,8 +64,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Wuhan',
|
name: 'Wuhan',
|
||||||
@ -75,8 +75,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Dongguan',
|
name: 'Dongguan',
|
||||||
@ -86,8 +86,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chongqing',
|
name: 'Chongqing',
|
||||||
@ -97,8 +97,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Xi'an",
|
name: "Xi'an",
|
||||||
@ -108,8 +108,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hangzhou',
|
name: 'Hangzhou',
|
||||||
@ -119,8 +119,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Foshan',
|
name: 'Foshan',
|
||||||
@ -130,8 +130,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nanjing',
|
name: 'Nanjing',
|
||||||
@ -141,8 +141,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hong Kong',
|
name: 'Hong Kong',
|
||||||
@ -152,8 +152,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Hong_Kong',
|
timezone: 'Asia/Hong_Kong',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shenyang',
|
name: 'Shenyang',
|
||||||
@ -163,8 +163,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zhengzhou',
|
name: 'Zhengzhou',
|
||||||
@ -174,8 +174,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Qingdao',
|
name: 'Qingdao',
|
||||||
@ -185,8 +185,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Suzhou',
|
name: 'Suzhou',
|
||||||
@ -196,8 +196,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Changsha',
|
name: 'Changsha',
|
||||||
@ -207,8 +207,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Jinan',
|
name: 'Jinan',
|
||||||
@ -218,8 +218,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Kunming',
|
name: 'Kunming',
|
||||||
@ -229,8 +229,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Harbin',
|
name: 'Harbin',
|
||||||
@ -240,8 +240,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shijiazhuang',
|
name: 'Shijiazhuang',
|
||||||
@ -251,8 +251,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hefei',
|
name: 'Hefei',
|
||||||
@ -262,8 +262,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Dalian',
|
name: 'Dalian',
|
||||||
@ -273,8 +273,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Xiamen',
|
name: 'Xiamen',
|
||||||
@ -284,8 +284,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nanning',
|
name: 'Nanning',
|
||||||
@ -295,8 +295,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Changchun',
|
name: 'Changchun',
|
||||||
@ -306,8 +306,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Taiyuan',
|
name: 'Taiyuan',
|
||||||
@ -317,8 +317,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'New Taipei City',
|
name: 'New Taipei City',
|
||||||
@ -328,8 +328,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Taipei',
|
timezone: 'Asia/Taipei',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Guiyang',
|
name: 'Guiyang',
|
||||||
@ -339,8 +339,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Wuxi',
|
name: 'Wuxi',
|
||||||
@ -350,8 +350,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Shantou',
|
name: 'Shantou',
|
||||||
@ -361,8 +361,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ürümqi',
|
name: 'Ürümqi',
|
||||||
@ -372,8 +372,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Zhongshan',
|
name: 'Zhongshan',
|
||||||
@ -383,8 +383,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ningbo',
|
name: 'Ningbo',
|
||||||
@ -394,8 +394,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Fuzhou',
|
name: 'Fuzhou',
|
||||||
@ -405,8 +405,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Nanchang',
|
name: 'Nanchang',
|
||||||
@ -416,7 +416,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Shanghai',
|
timezone: 'Asia/Shanghai',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Africa/Cairo',
|
timezone: 'Africa/Cairo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Europe/Paris',
|
timezone: 'Europe/Paris',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Europe/Berlin',
|
timezone: 'Europe/Berlin',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Berlin',
|
name: 'Berlin',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Europe/Berlin',
|
timezone: 'Europe/Berlin',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Kolkata',
|
timezone: 'Asia/Kolkata',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Bengaluru',
|
name: 'Bengaluru',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Kolkata',
|
timezone: 'Asia/Kolkata',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Tokyo',
|
timezone: 'Asia/Tokyo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Yokohama',
|
name: 'Yokohama',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Tokyo',
|
timezone: 'Asia/Tokyo',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'America/Mexico_City',
|
timezone: 'America/Mexico_City',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Africa/Lagos',
|
timezone: 'Africa/Lagos',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Karachi',
|
timezone: 'Asia/Karachi',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Europe/Moscow',
|
timezone: 'Europe/Moscow',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sankt Petersburg',
|
name: 'Sankt Petersburg',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Europe/Moscow',
|
timezone: 'Europe/Moscow',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Riyadh',
|
timezone: 'Asia/Riyadh',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Singapore',
|
timezone: 'Asia/Singapore',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Seoul',
|
timezone: 'Asia/Seoul',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Bangkok',
|
timezone: 'Asia/Bangkok',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Europe/Istanbul',
|
timezone: 'Europe/Istanbul',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ankara',
|
name: 'Ankara',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Europe/Istanbul',
|
timezone: 'Europe/Istanbul',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,7 +9,7 @@ City.create!([
|
|||||||
timezone: 'Europe/London',
|
timezone: 'Europe/London',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'America/Los_Angeles',
|
timezone: 'America/Los_Angeles',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Chicago',
|
name: 'Chicago',
|
||||||
@ -20,8 +20,8 @@ City.create!([
|
|||||||
timezone: 'America/Chicago',
|
timezone: 'America/Chicago',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'New York City',
|
name: 'New York City',
|
||||||
@ -31,8 +31,8 @@ City.create!([
|
|||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Los Angeles',
|
name: 'Los Angeles',
|
||||||
@ -42,7 +42,7 @@ City.create!([
|
|||||||
timezone: 'America/Los_Angeles',
|
timezone: 'America/Los_Angeles',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -9,8 +9,8 @@ City.create!([
|
|||||||
timezone: 'Asia/Ho_Chi_Minh',
|
timezone: 'Asia/Ho_Chi_Minh',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Hanoi',
|
name: 'Hanoi',
|
||||||
@ -20,7 +20,7 @@ City.create!([
|
|||||||
timezone: 'Asia/Ho_Chi_Minh',
|
timezone: 'Asia/Ho_Chi_Minh',
|
||||||
active: true,
|
active: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
last_weather_fetch: 10.hours.ago,
|
last_weather_fetch: 10.days.ago,
|
||||||
last_image_generation: 10.hours.ago
|
last_image_generation: 10.days.ago
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
7
test/jobs/batch_generate_weather_arts_job_test.rb
Normal file
7
test/jobs/batch_generate_weather_arts_job_test.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class BatchGenerateWeatherArtsJobTest < ActiveJob::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
7
test/jobs/generate_weather_art_job_test.rb
Normal file
7
test/jobs/generate_weather_art_job_test.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class GenerateWeatherArtJobTest < ActiveJob::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user