diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..d3466c33d
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,105 @@
+# === Core Service Ports ===
+SERVER_PORT=8080
+FRONTEND_PORT=3000
+WEBSOCKET_PORT=8082
+MYSQL_PORT=3306
+REDIS_PORT=6379
+RABBITMQ_PORT=5672
+RABBITMQ_MANAGEMENT_PORT=15672
+
+# === OpenSearch Configuration ===
+OPENSEARCH_PORT=9200
+OPENSEARCH_METRICS_PORT=9600
+OPENSEARCH_DASHBOARDS_PORT=5601
+OPENSEARCH_ENABLED=true
+OPENSEARCH_SCHEME=http
+OPENSEARCH_USERNAME=
+OPENSEARCH_PASSWORD=
+OPENSEARCH_HOST=opensearch
+
+# === Database Configuration ===
+MYSQL_DATABASE=openisle
+MYSQL_ROOT_PASSWORD=openisle
+MYSQL_USER=openisle
+MYSQL_PASSWORD=openisle
+MYSQL_HOST=mysql
+
+# === Redis Configuration ===
+REDIS_HOST=redis
+REDIS_DATABASE=0
+
+# === RabbitMQ Configuration ===
+RABBITMQ_HOST=rabbitmq
+RABBITMQ_USERNAME=nagisa
+RABBITMQ_PASSWORD=nagisa
+
+# === Backend Application Secrets ===
+JWT_SECRET=change-me-jwt-secret
+JWT_REASON_SECRET=change-me-jwt-reason-secret
+JWT_RESET_SECRET=change-me-jwt-reset-secret
+JWT_INVITE_SECRET=change-me-jwt-invite-secret
+JWT_EXPIRATION=2592000000
+PASSWORD_STRENGTH=LOW
+POST_PUBLISH_MODE=DIRECT
+REGISTER_MODE=WHITELIST
+UPLOAD_CHECK_TYPE=true
+UPLOAD_MAX_SIZE=5242880
+AVATAR_STYLE=pixel-art-neutral
+AVATAR_SIZE=128
+AVATAR_BASE_URL=https://api.dicebear.com/6.x
+USER_POSTS_LIMIT=10
+USER_REPLIES_LIMIT=50
+SNIPPET_LENGTH=200
+SEARCH_INDEX_PREFIX=openisle
+SEARCH_HIGHLIGHT_FRAGMENT_SIZE=200
+SEARCH_REINDEX_ON_STARTUP=true
+SEARCH_REINDEX_BATCH_SIZE=500
+CAPTCHA_ENABLED=false
+RECAPTCHA_SECRET_KEY=
+CAPTCHA_REGISTER_ENABLED=false
+CAPTCHA_LOGIN_ENABLED=false
+CAPTCHA_POST_ENABLED=false
+CAPTCHA_COMMENT_ENABLED=false
+RESEND_API_KEY=
+RESEND_FROM_EMAIL=
+COS_BASE_URL=https://<你的cos>.cos.accelerate.myqcloud.com
+COS_SECRET_ID=
+COS_SECRET_KEY=
+COS_REGION=ap-guangzhou
+COS_BUCKET_NAME=
+GITHUB_CLIENT_SECRET=
+DISCORD_CLIENT_SECRET=
+TWITTER_CLIENT_SECRET=
+TELEGRAM_BOT_TOKEN=
+OPENAI_API_KEY=
+OPENAI_MODEL=gpt-4o
+AI_FORMAT_LIMIT=3
+WEBSITE_URL=http://localhost:3000
+WEBPUSH_PUBLIC_KEY=
+WEBPUSH_PRIVATE_KEY=
+LOG_LEVEL=INFO
+
+# === Frontend (Nuxt) ===
+
+NUXT_PUBLIC_API_BASE_URL=http://localhost:8080
+# NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
+# NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
+
+NUXT_PUBLIC_WEBSOCKET_URL=http://localhost:8082
+# NUXT_PUBLIC_WEBSOCKET_URL=https://www.open-isle.com
+# NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com
+
+NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000
+# 线上 & 本地均可使用
+NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
+# 线上
+NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
+# 本地
+# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
+# 线上 & 本地均可使用
+NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
+# 线上 & 本地均可使用
+NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
+# 线上
+NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
+
diff --git a/backend/open-isle.env.example b/backend/open-isle.env.example
index a62ac877f..261c45b17 100644
--- a/backend/open-isle.env.example
+++ b/backend/open-isle.env.example
@@ -1,3 +1,6 @@
+# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
+# 此文件保留作参考用途,如需在 Docker 之外手动配置,可按需复制。
+
# === Spring Boot ===
SERVER_PORT=8080
diff --git a/backend/pom.xml b/backend/pom.xml
index a0f1c9b7e..1908a5c1b 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -132,6 +132,10 @@
springdoc-openapi-starter-webmvc-api
2.2.0
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
org.opensearch.client
diff --git a/backend/src/main/java/com/openisle/config/SecurityConfig.java b/backend/src/main/java/com/openisle/config/SecurityConfig.java
index 4bd8e735f..6a5dc8fb2 100644
--- a/backend/src/main/java/com/openisle/config/SecurityConfig.java
+++ b/backend/src/main/java/com/openisle/config/SecurityConfig.java
@@ -97,6 +97,8 @@ public class SecurityConfig {
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:3000",
+ "http://frontend_dev:3000",
+ "http://frontend_service:3000",
"http://localhost:3001",
"http://localhost",
"http://30.211.97.238:3000",
@@ -177,6 +179,8 @@ public class SecurityConfig {
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/point-goods")
.permitAll()
+ .requestMatchers("/actuator/**")
+ .permitAll()
.requestMatchers(HttpMethod.POST, "/api/categories/**")
.hasAuthority("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/tags/**")
@@ -230,6 +234,7 @@ public class SecurityConfig {
uri.startsWith("/api/channels") ||
uri.startsWith("/api/sitemap.xml") ||
uri.startsWith("/api/medals") ||
+ uri.startsWith("/actuator") ||
uri.startsWith("/api/rss"));
if (authHeader != null && authHeader.startsWith("Bearer ")) {
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index 479fc683c..6d7c5bbe5 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -4,7 +4,7 @@ server.port=${SERVER_PORT:8080}
# for mysql
logging.level.root=${LOG_LEVEL:INFO}
logging.level.com.openisle.service.CosImageUploader=DEBUG
-spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost:3306/openisle}
+spring.datasource.url=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}
spring.datasource.username=${MYSQL_USER:root}
spring.datasource.password=${MYSQL_PASSWORD:password}
spring.jpa.hibernate.ddl-auto=update
@@ -47,11 +47,11 @@ app.snippet-length=${SNIPPET_LENGTH:200}
# OpenSearch integration
app.search.enabled=${SEARCH_ENABLED:true}
-app.search.host=${SEARCH_HOST:localhost}
-app.search.port=${SEARCH_PORT:9200}
-app.search.scheme=${SEARCH_SCHEME:http}
-app.search.username=${SEARCH_USERNAME:}
-app.search.password=${SEARCH_PASSWORD:}
+app.search.host=${OPENSEARCH_HOST:opensearch}
+app.search.port=${OPENSEARCH_PORT:9200}
+app.search.scheme=${OPENSEARCH_SCHEME:http}
+app.search.username=${OPENSEARCH_USERNAME:}
+app.search.password=${OPENSEARCH_PASSWORD:}
app.search.index-prefix=${SEARCH_INDEX_PREFIX:openisle}
app.search.highlight-fragment-size=${SEARCH_HIGHLIGHT_FRAGMENT_SIZE:${SNIPPET_LENGTH:200}}
app.search.reindex-on-startup=${SEARCH_REINDEX_ON_STARTUP:true}
@@ -81,15 +81,15 @@ cos.bucket-name=${COS_BUCKET_NAME:}
# your image upload services: ...
# Google OAuth configuration
-google.client-id=${GOOGLE_CLIENT_ID:}
+google.client-id=${NUXT_PUBLIC_GOOGLE_CLIENT_ID:}
# GitHub OAuth configuration
-github.client-id=${GITHUB_CLIENT_ID:}
+github.client-id=${NUXT_PUBLIC_GITHUB_CLIENT_ID:}
github.client-secret=${GITHUB_CLIENT_SECRET:}
# Discord OAuth configuration
-discord.client-id=${DISCORD_CLIENT_ID:}
+discord.client-id=${NUXT_PUBLIC_DISCORD_CLIENT_ID:}
discord.client-secret=${DISCORD_CLIENT_SECRET:}
# Twitter OAuth configuration
-twitter.client-id=${TWITTER_CLIENT_ID:}
+twitter.client-id=${NUXT_PUBLIC_TWITTER_CLIENT_ID:}
twitter.client-secret=${TWITTER_CLIENT_SECRET:}
# Telegram login configuration
telegram.bot-token=${TELEGRAM_BOT_TOKEN:}
@@ -129,3 +129,6 @@ springdoc.info.description=OpenIsle Open API Documentation
springdoc.info.version=0.0.1
springdoc.info.scheme=Bearer
springdoc.info.header=Authorization
+
+management.endpoints.web.exposure.include=health,info
+management.endpoint.health.probes.enabled=true
\ No newline at end of file
diff --git a/backend/src/main/resources/db/init/00_init_db_and_user.sql b/backend/src/main/resources/db/init/00_init_db_and_user.sql
new file mode 100644
index 000000000..d8cf198ef
--- /dev/null
+++ b/backend/src/main/resources/db/init/00_init_db_and_user.sql
@@ -0,0 +1,13 @@
+SET NAMES utf8mb4;
+SET CHARACTER SET utf8mb4;
+SET collation_connection = utf8mb4_0900_ai_ci;
+
+CREATE DATABASE IF NOT EXISTS `openisle`
+ CHARACTER SET utf8mb4
+ COLLATE utf8mb4_0900_ai_ci;
+
+CREATE USER IF NOT EXISTS 'openisle'@'%' IDENTIFIED BY 'openisle';
+GRANT ALL PRIVILEGES ON `openisle`.* TO 'openisle'@'%';
+FLUSH PRIVILEGES;
+
+USE `openisle`;
diff --git a/backend/src/main/resources/db/init/01_schema.sql b/backend/src/main/resources/db/init/01_schema.sql
new file mode 100644
index 000000000..87e1e22da
--- /dev/null
+++ b/backend/src/main/resources/db/init/01_schema.sql
@@ -0,0 +1,54 @@
+USE `openisle`;
+SET NAMES utf8mb4;
+
+SET FOREIGN_KEY_CHECKS = 0;
+
+CREATE TABLE IF NOT EXISTS `users` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `approved` bit(1) DEFAULT NULL,
+ `avatar` varchar(255) DEFAULT NULL,
+ `created_at` datetime(6) DEFAULT NULL,
+ `display_medal` varchar(255) DEFAULT NULL,
+ `email` varchar(255) NOT NULL,
+ `experience` int DEFAULT NULL,
+ `introduction` text,
+ `password` varchar(255) NOT NULL,
+ `password_reset_code` varchar(255) DEFAULT NULL,
+ `point` int DEFAULT NULL,
+ `register_reason` text,
+ `role` varchar(20) DEFAULT 'USER',
+ `username` varchar(50) NOT NULL,
+ `verification_code` varchar(255) DEFAULT NULL,
+ `verified` bit(1) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `UK_users_email` (`email`),
+ UNIQUE KEY `UK_users_username` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+
+CREATE TABLE IF NOT EXISTS `categories` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `description` text,
+ `icon` varchar(255) DEFAULT NULL,
+ `name` varchar(50) NOT NULL,
+ `small_icon` varchar(255) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `UK_categories_name` (`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+
+CREATE TABLE IF NOT EXISTS `tags` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `approved` bit(1) DEFAULT NULL,
+ `created_at` datetime(6) DEFAULT NULL,
+ `description` text,
+ `icon` varchar(255) DEFAULT NULL,
+ `name` varchar(50) NOT NULL,
+ `small_icon` varchar(255) DEFAULT NULL,
+ `creator_id` bigint DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `UK_tags_name` (`name`),
+ KEY `FK_tags_creator` (`creator_id`),
+ CONSTRAINT `FK_tags_creator` FOREIGN KEY (`creator_id`) REFERENCES `users` (`id`)
+ ON DELETE SET NULL ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/backend/src/main/resources/db/init/02_seed_data.sql b/backend/src/main/resources/db/init/02_seed_data.sql
new file mode 100644
index 000000000..f3f375b7a
--- /dev/null
+++ b/backend/src/main/resources/db/init/02_seed_data.sql
@@ -0,0 +1,26 @@
+USE `openisle`;
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+DELETE FROM `tags`;
+DELETE FROM `categories`;
+DELETE FROM `users`;
+
+-- 插入用户,两个普通用户,一个管理员
+-- username:admin/user1/user2 password:123456
+INSERT INTO `users` (`id`, `approved`, `avatar`, `created_at`, `display_medal`, `email`, `experience`, `introduction`, `password`, `password_reset_code`, `point`, `register_reason`, `role`, `username`, `verification_code`, `verified`) VALUES
+(1, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-01 16:08:17.426430', 'PIONEER', 'adminmail@openisle.com', 70, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 110, '测试测试测试……', 'ADMIN', 'admin', NULL, b'1'),
+(2, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-03 16:08:17.426430', 'PIONEER', 'usermail2@openisle.com', 70, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 110, '测试测试测试……', 'USER', 'user1', NULL, b'1'),
+(3, b'1', 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png', '2025-09-02 17:21:21.617666', 'PIONEER', 'usermail1@openisle.com', 40, NULL, '$2a$10$x7HXjUyJTmrvqjnBlBQZH.vmfsC56NzTSWqQ6WqZqRjUO859EhviS', NULL, 40, '测试测试测试……', 'USER', 'user2', NULL, b'1');
+
+INSERT INTO `categories` (`id`,`description`,`icon`,`name`,`small_icon`) VALUES
+(1,'测试用分类1','star','测试用分类1',NULL),
+(2,'测试用分类2','star','测试用分类2',NULL),
+(3,'测试用分类3','star','测试用分类3',NULL);
+
+INSERT INTO `tags` (`id`,`approved`,`created_at`,`description`,`icon`,`name`,`small_icon`,`creator_id`) VALUES
+(1,b'1','2025-09-02 10:51:56.000000','测试用标签1',NULL,'测试用标签1',NULL,NULL),
+(2,b'1','2025-09-02 10:51:56.000000','测试用标签2',NULL,'测试用标签2',NULL,NULL),
+(3,b'1','2025-09-02 10:51:56.000000','测试用标签3',NULL,'测试用标签3',NULL,NULL);
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/backend/src/main/resources/db/init/init_script.sql b/backend/src/main/resources/db/init/init_script.sql
deleted file mode 100644
index 3ea4bd2c1..000000000
--- a/backend/src/main/resources/db/init/init_script.sql
+++ /dev/null
@@ -1,81 +0,0 @@
--- 2025-09-02
--- 本地化开发,初始化脚本
--- 抽奖的时候奖品图片是必须的,把相关代码注释掉即可跳过check
-
--- 设置字符集和排序规则
-SET NAMES utf8;
-SET CHARACTER SET utf8;
-SET collation_connection = utf8_general_ci;
-
--- 创建 users 表(如果不存在)
-CREATE TABLE IF NOT EXISTS `users` (
- `id` bigint NOT NULL AUTO_INCREMENT,
- `approved` bit(1) DEFAULT NULL,
- `avatar` varchar(255) DEFAULT NULL,
- `created_at` datetime(6) DEFAULT NULL,
- `display_medal` varchar(255) DEFAULT NULL,
- `email` varchar(255) NOT NULL,
- `experience` int DEFAULT NULL,
- `introduction` text,
- `password` varchar(255) NOT NULL,
- `password_reset_code` varchar(255) DEFAULT NULL,
- `point` int DEFAULT NULL,
- `register_reason` text,
- `role` varchar(20) DEFAULT 'USER',
- `username` varchar(50) NOT NULL,
- `verification_code` varchar(255) DEFAULT NULL,
- `verified` bit(1) DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `UK_users_email` (`email`),
- UNIQUE KEY `UK_users_username` (`username`)
-);
-
--- 清空users表
-DELETE FROM `users`;
--- 插入用户,两个普通用户,一个管理员
--- username:admin/user1/user2 password:123321
-INSERT INTO `users` (`id`, `approved`, `avatar`, `created_at`, `display_medal`, `email`, `experience`, `introduction`, `password`, `password_reset_code`, `point`, `register_reason`, `role`, `username`, `verification_code`, `verified`) VALUES
- (1, b'1', '', '2025-09-01 16:08:17.426430', 'PIONEER', 'adminmail@openisle.com', 70, NULL, '$2a$10$dux.NXwW09cCsdZ05BgcnOtxVqqjcmnbj3.8xcxGl/iiIlv06y7Oe', NULL, 110, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'ADMIN', 'admin', NULL, b'1'),
- (2, b'1', '', '2025-09-03 16:08:17.426430', 'PIONEER', 'usermail2@openisle.com', 70, NULL, '$2a$10$dux.NXwW09cCsdZ05BgcnOtxVqqjcmnbj3.8xcxGl/iiIlv06y7Oe', NULL, 110, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'USER', 'user1', NULL, b'1'),
- (3, b'1', '', '2025-09-02 17:21:21.617666', 'PIONEER', 'usermail1@openisle.com', 40, NULL, '$2a$10$dux.NXwW09cCsdZ05BgcnOtxVqqjcmnbj3.8xcxGl/iiIlv06y7Oe', NULL, 40, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'USER', 'user2', NULL, b'1');
-
--- 创建 tags 表(如果不存在)
-CREATE TABLE IF NOT EXISTS `tags` (
- `id` bigint NOT NULL AUTO_INCREMENT,
- `approved` bit(1) DEFAULT NULL,
- `created_at` datetime(6) DEFAULT NULL,
- `description` text,
- `icon` varchar(255) DEFAULT NULL,
- `name` varchar(50) NOT NULL,
- `small_icon` varchar(255) DEFAULT NULL,
- `creator_id` bigint DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `UK_tags_name` (`name`),
- KEY `FK_tags_creator` (`creator_id`),
- CONSTRAINT `FK_tags_creator` FOREIGN KEY (`creator_id`) REFERENCES `users` (`id`)
-);
--- 清空tags表
-DELETE FROM `tags`;
--- 插入标签,三个测试用标签
-INSERT INTO `tags` (`id`, `approved`, `created_at`, `description`, `icon`, `name`, `small_icon`, `creator_id`) VALUES
- (1, b'1', '2025-09-02 10:51:56.000000', '测试用标签1', NULL, '测试用标签1', NULL, NULL),
- (2, b'1', '2025-09-02 10:51:56.000000', '测试用标签2', NULL, '测试用标签2', NULL, NULL),
- (3, b'1', '2025-09-02 10:51:56.000000', '测试用标签3', NULL, '测试用标签3', NULL, NULL);
-
--- 创建 categories 表(如果不存在)
-CREATE TABLE IF NOT EXISTS `categories` (
- `id` bigint NOT NULL AUTO_INCREMENT,
- `description` text,
- `icon` varchar(255) DEFAULT NULL,
- `name` varchar(50) NOT NULL,
- `small_icon` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `UK_categories_name` (`name`)
-);
--- 清空categories表
-DELETE FROM `categories`;
--- 插入分类,三个测试用分类
-INSERT INTO `categories` (`id`, `description`, `icon`, `name`, `small_icon`) VALUES
- (1, '测试用分类1', '1', '测试用分类1', NULL),
- (2, '测试用分类2', '2', '测试用分类2', NULL),
- (3, '测试用分类3', '3', '测试用分类3', NULL);
\ No newline at end of file
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100644
index 000000000..d371dfec6
--- /dev/null
+++ b/deploy/deploy.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# 可用法:
+# ./deploy.sh
+# ./deploy.sh feature/docker
+deploy_branch="${1:-feature/docker}"
+
+repo_dir="/opt/openisle/OpenIsle"
+compose_file="${repo_dir}/docker/docker-compose.yaml"
+env_file="${repo_dir}/.env"
+project="openisle"
+
+echo "👉 Enter repo..."
+cd "$repo_dir"
+
+echo "👉 Syncing code & switching to branch: $deploy_branch"
+git fetch --all --prune
+git checkout -B "$deploy_branch" "origin/$deploy_branch"
+git reset --hard "origin/$deploy_branch"
+
+echo "👉 Ensuring env file: $env_file"
+if [ ! -f "$env_file" ]; then
+ echo "❌ ${env_file} not found. Create it based on .env.example (with domains)."
+ exit 1
+fi
+
+export COMPOSE_PROJECT_NAME="$project"
+# 供 compose 内各 service 的 env_file 使用
+export ENV_FILE="$env_file"
+
+echo "👉 Validate compose..."
+docker compose -f "$compose_file" --env-file "$env_file" config >/dev/null
+
+echo "👉 Pull base images (for image-based services)..."
+docker compose -f "$compose_file" --env-file "$env_file" pull --ignore-pull-failures
+
+echo "👉 Build images ..."
+# 前端 + OpenSearch 都是自建镜像;--pull 更新其基础镜像
+docker compose -f "$compose_file" --env-file "$env_file" \
+ build --pull \
+ --build-arg NUXT_ENV=production \
+ frontend_service opensearch
+
+echo "👉 Recreate & start all target services (no dev profile)..."
+docker compose -f "$compose_file" --env-file "$env_file" \
+ up -d --force-recreate --remove-orphans \
+ mysql redis rabbitmq opensearch dashboards websocket-service springboot frontend_service
+
+echo "👉 Current status:"
+docker compose -f "$compose_file" --env-file "$env_file" ps
+
+echo "👉 Pruning dangling images..."
+docker image prune -f
+
+echo "✅ Stack deployed at $(date)"
\ No newline at end of file
diff --git a/deploy/deploy_staging.sh b/deploy/deploy_staging.sh
new file mode 100644
index 000000000..f2d623259
--- /dev/null
+++ b/deploy/deploy_staging.sh
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# 可用法:
+# ./deploy-staging.sh
+# ./deploy-staging.sh feature/docker
+deploy_branch="${1:-feature/docker}"
+
+repo_dir="/opt/openisle/OpenIsle-staging"
+compose_file="${repo_dir}/docker/docker-compose.yaml"
+# 使用仓库根目录的 .env(CI 预先写好),也可以改成绝对路径
+env_file="${repo_dir}/.env"
+project="openisle_staging"
+
+echo "👉 Enter repo..."
+cd "$repo_dir"
+
+echo "👉 Syncing code & switching to branch: $deploy_branch"
+git fetch --all --prune
+git checkout -B "$deploy_branch" "origin/$deploy_branch"
+git reset --hard "origin/$deploy_branch"
+
+echo "👉 Ensuring env file: $env_file"
+if [ ! -f "$env_file" ]; then
+ echo "❌ ${env_file} not found. Create it based on .env.example (with staging domains)."
+ exit 1
+fi
+
+export COMPOSE_PROJECT_NAME="$project"
+# 供 compose 内各 service 的 env_file 使用
+export ENV_FILE="$env_file"
+
+echo "👉 Validate compose..."
+docker compose -f "$compose_file" --env-file "$env_file" config >/dev/null
+
+echo "👉 Pull base images (for image-based services)..."
+docker compose -f "$compose_file" --env-file "$env_file" pull --ignore-pull-failures
+
+echo "👉 Build images (staging)..."
+# 前端 + OpenSearch 都是自建镜像;--pull 更新其基础镜像
+docker compose -f "$compose_file" --env-file "$env_file" \
+ build --pull \
+ --build-arg NUXT_ENV=staging \
+ frontend_service opensearch
+
+echo "👉 Recreate & start all target services (no dev profile)..."
+docker compose -f "$compose_file" --env-file "$env_file" \
+ up -d --force-recreate --remove-orphans \
+ mysql redis rabbitmq opensearch dashboards websocket-service springboot frontend_service
+
+echo "👉 Current status:"
+docker compose -f "$compose_file" --env-file "$env_file" ps
+
+echo "👉 Pruning dangling images..."
+docker image prune -f
+
+echo "✅ Staging stack deployed at $(date)"
\ No newline at end of file
diff --git a/docker/.env.example b/docker/.env.example
index 0ad80a93c..a798793ea 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -1,16 +1,4 @@
-# 前端访问端口
-SERVER_PORT=8080
-
-# OpenSearch 配置
-OPENSEARCH_PORT=9200
-OPENSEARCH_METRICS_PORT=9600
-OPENSEARCH_DASHBOARDS_PORT=5601
-
-# MySQL 配置
-MYSQL_ROOT_PASSWORD=toor
-
-# 会覆盖 `open-isle.env`
-MYSQL_PORT=3306
-MYSQL_DATABASE=openisle
-MYSQL_USER=<数据库用户名>
-MYSQL_PASSWORD=<数据库密码>
+# 已迁移到仓库根目录的 .env.*.example 文件。
+# 请复制对应环境的示例文件到项目根目录,例如:
+# cp ../.env.dev.example ../.env
+# docker-compose 将自动读取 ../.env。
diff --git a/docker/.gitignore b/docker/.gitignore
new file mode 100644
index 000000000..6320cd248
--- /dev/null
+++ b/docker/.gitignore
@@ -0,0 +1 @@
+data
\ No newline at end of file
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 50a60557d..a1307b786 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -2,25 +2,37 @@ services:
# MySQL service
mysql:
image: mysql:8.0
- container_name: openisle-mysql
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-mysql
restart: always
env_file:
- - ../backend/open-isle.env
- - ./.env
+ - ${ENV_FILE:-../.env}
+ command: >
+ --character-set-server=utf8mb4
+ --collation-server=utf8mb4_0900_ai_ci
+ --default-time-zone=+08:00
+ --skip-character-set-client-handshake
ports:
- - "${MYSQL_PORT}:3306"
+ - "${MYSQL_PORT:-3306}:3306"
volumes:
- mysql-data:/var/lib/mysql
- - ../backend/src/main/resources/db/init:/docker-entrypoint-initdb.d
+ - ../backend/src/main/resources/db/init:/docker-entrypoint-initdb.d:ro
+ - ./mysql/conf.d:/etc/mysql/conf.d:ro
networks:
- openisle-network
-
+ healthcheck:
+ test: ["CMD","mysqladmin","ping","-h","127.0.0.1","-u","root","-p$MYSQL_ROOT_PASSWORD"]
+ interval: 5s
+ timeout: 3s
+ retries: 30
+ start_period: 20s
+
# OpenSearch Service
opensearch:
+ user: "1000:1000"
build:
context: .
- dockerfile: Dockerfile
- container_name: opensearch
+ dockerfile: opensearch.Dockerfile
+ container_name: ${COMPOSE_PROJECT_NAME}-opensearch
environment:
- cluster.name=os-single
- node.name=os-node-1
@@ -31,53 +43,253 @@ services:
- cluster.blocks.create_index=false
ulimits:
memlock: { soft: -1, hard: -1 }
- nofile: { soft: 65536, hard: 65536 }
+ nofile: { soft: 65536, hard: 65536 }
volumes:
- - ./data:/usr/share/opensearch/data
- - ./snapshots:/snapshots
+ - ${OPENSEARCH_DATA_DIR:-./data}:/usr/share/opensearch/data
+ - ${OPENSEARCH_SNAPSHOT_DIR:-./snapshots}:/snapshots
ports:
- "${OPENSEARCH_PORT:-9200}:9200"
- "${OPENSEARCH_METRICS_PORT:-9600}:9600"
restart: unless-stopped
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - curl -fsS http://127.0.0.1:9200/_cluster/health >/dev/null
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 60s
+ networks:
+ - openisle-network
dashboards:
image: opensearchproject/opensearch-dashboards:3.0.0
- container_name: os-dashboards
+ container_name: ${COMPOSE_PROJECT_NAME}-os-dashboards
environment:
- - OPENSEARCH_HOSTS=["http://opensearch:9200"]
- - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
+ OPENSEARCH_HOSTS: '["http://opensearch:9200"]'
+ DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true"
ports:
- "${OPENSEARCH_DASHBOARDS_PORT:-5601}:5601"
depends_on:
- opensearch
restart: unless-stopped
+ networks:
+ - openisle-network
+ rabbitmq:
+ image: rabbitmq:3.13-management
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-rabbitmq
+ restart: unless-stopped
+ environment:
+ RABBITMQ_DEFAULT_VHOST: "${RABBITMQ_VHOST:-/}"
+ ports:
+ - "${RABBITMQ_PORT:-5672}:5672"
+ - "${RABBITMQ_MANAGEMENT_PORT:-15672}:15672"
+ volumes:
+ - rabbitmq-data:/var/lib/rabbitmq
+ - ./rabbitmq/conf/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
+ - ./rabbitmq/conf/enabled_plugins:/etc/rabbitmq/enabled_plugins:ro
+ - ./rabbitmq/definitions.json:/etc/rabbitmq/definitions.json:ro
+ healthcheck:
+ test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 30s
+ networks:
+ - openisle-network
- # Java spring boot service
+ redis:
+ image: redis:7
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-redis
+ restart: unless-stopped
+ env_file:
+ - ${ENV_FILE:-../.env}
+ ports:
+ - "${REDIS_PORT:-6379}:6379"
+ volumes:
+ - redis-data:/data
+ networks:
+ - openisle-network
+
+ # Java spring boot service (开发便捷镜像,后续可换成打包镜像)
springboot:
image: maven:3.9-eclipse-temurin-17
- container_name: openisle-springboot
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-springboot
working_dir: /app
env_file:
- - ../backend/open-isle.env
- - ./.env
+ - ${ENV_FILE:-../.env}
environment:
- - MYSQL_URL=jdbc:mysql://mysql:${MYSQL_PORT}/${MYSQL_DATABASE}?useUnicode=yes&characterEncoding=UTF-8&useInformationSchema=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
+ TZ: "Asia/Shanghai"
+ SPRING_HEALTH_PATH: ${SPRING_HEALTH_PATH:-/actuator/health}
+ SERVER_PORT: ${SERVER_PORT:-8080}
+ RABBITMQ_PORT: 5672
+ OPENSEARCH_PORT: 9200
+ MYSQL_PORT: 3306
+ REDIS_PORT: 6379
+ JAVA_OPTS: "-Duser.timezone=Asia/Shanghai"
ports:
- - "${SERVER_PORT}:8080"
+ - "${SERVER_PORT:-8080}:${SERVER_PORT:-8080}"
volumes:
- ../backend:/app
- maven-repo:/root/.m2
depends_on:
- - mysql
- command: mvn clean spring-boot:run -Dmaven.test.skip=true
+ mysql:
+ condition: service_healthy
+ redis:
+ condition: service_started
+ rabbitmq:
+ condition: service_started
+ websocket-service:
+ condition: service_healthy
+ opensearch:
+ condition: service_healthy
+ command: >
+ sh -c "apt-get update && apt-get install -y --no-install-recommends curl &&
+ mvn clean spring-boot:run -Dmaven.test.skip=true"
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:${SERVER_PORT:-8080}${SPRING_HEALTH_PATH:-/actuator/health} || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 60s
networks:
- openisle-network
+ websocket-service:
+ image: maven:3.9-eclipse-temurin-17
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-websocket
+ working_dir: /app
+ env_file:
+ - ${ENV_FILE:-../.env}
+ environment:
+ WS_HEALTH_PATH: ${WS_HEALTH_PATH:-/actuator/health}
+ WEBSOCKET_PORT: ${WEBSOCKET_PORT:-8082}
+ SERVER_PORT: ${WEBSOCKET_PORT:-8082}
+ RABBITMQ_PORT: 5672
+ ports:
+ - "${WEBSOCKET_PORT:-8082}:${WEBSOCKET_PORT:-8082}"
+ volumes:
+ - ../websocket_service:/app
+ - websocket-maven-repo:/root/.m2
+ depends_on:
+ rabbitmq:
+ condition: service_healthy
+ command: >
+ sh -c "apt-get update && apt-get install -y --no-install-recommends curl &&
+ mvn clean spring-boot:run -Dmaven.test.skip=true"
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:${WEBSOCKET_PORT:-8082}${WS_HEALTH_PATH:-/actuator/health} || exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ start_period: 60s
+ networks:
+ - openisle-network
+
+ frontend_dev:
+ image: node:20
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-frontend-dev
+ working_dir: /app
+ env_file:
+ - ${ENV_FILE:-../.env}
+ command: sh -c "npm install && npm run dev"
+ volumes:
+ - ../frontend_nuxt:/app
+ - frontend-node-modules:/app/node_modules
+ ports:
+ - "${FRONTEND_PORT:-3000}:3000"
+ depends_on:
+ springboot:
+ condition: service_healthy
+ websocket-service:
+ condition: service_healthy
+ networks:
+ - openisle-network
+ profiles:
+ - dev
+
+ frontend_service:
+ build:
+ context: ..
+ dockerfile: docker/frontend-service.Dockerfile
+ args:
+ NUXT_ENV: ${NUXT_ENV:-staging}
+ container_name: ${COMPOSE_PROJECT_NAME}-openisle-frontend
+ env_file:
+ - ${ENV_FILE:-../.env}
+ ports:
+ - "${FRONTEND_PORT:-3000}:3000"
+ depends_on:
+ springboot:
+ condition: service_healthy
+ websocket-service:
+ condition: service_healthy
+ restart: unless-stopped
+
+ loopback_8080:
+ image: alpine/socat
+ container_name: ${COMPOSE_PROJECT_NAME}-loopback-8080
+ # 监听“frontend_dev 容器自身的” 127.0.0.1:8080 → 转发到 springboot:8080
+ command:
+ - -d
+ - -d
+ - -ly
+ - TCP4-LISTEN:8080,bind=127.0.0.1,reuseaddr,fork
+ - TCP4:springboot:8080
+ depends_on:
+ springboot:
+ condition: service_healthy
+ network_mode: "service:frontend_dev"
+ profiles: ["dev"]
+ healthcheck:
+ test: ["CMD", "sh", "-c", "nc -z 127.0.0.1 8080"]
+ interval: 5s
+ timeout: 3s
+ retries: 20
+ start_period: 10s
+
+ loopback_8082:
+ image: alpine/socat
+ container_name: ${COMPOSE_PROJECT_NAME}-loopback-8082
+ # 监听 127.0.0.1:8082 → 转发到 websocket-service:8082(WS 纯 TCP 可直接过)
+ command:
+ - -d
+ - -d
+ - -ly
+ - TCP4-LISTEN:8082,bind=127.0.0.1,reuseaddr,fork
+ - TCP4:websocket-service:8082
+ depends_on:
+ websocket-service:
+ condition: service_healthy
+ network_mode: "service:frontend_dev"
+ profiles: ["dev"]
+ healthcheck:
+ test: ["CMD", "sh", "-c", "nc -z 127.0.0.1 8082"]
+ interval: 5s
+ timeout: 3s
+ retries: 20
+ start_period: 10s
+
networks:
openisle-network:
+ name: "${COMPOSE_PROJECT_NAME}_net"
driver: bridge
volumes:
mysql-data:
+ name: "${COMPOSE_PROJECT_NAME}_mysql-data"
maven-repo:
+ name: "${COMPOSE_PROJECT_NAME}_maven-repo"
+ redis-data:
+ name: "${COMPOSE_PROJECT_NAME}_redis-data"
+ rabbitmq-data:
+ name: "${COMPOSE_PROJECT_NAME}_rabbitmq-data"
+ websocket-maven-repo:
+ name: "${COMPOSE_PROJECT_NAME}_websocket-maven-repo"
+ frontend-node-modules:
+ name: "${COMPOSE_PROJECT_NAME}_frontend-node-modules"
+ frontend-service-node-modules:
+ name: "${COMPOSE_PROJECT_NAME}_frontend-service-node-modules"
+ frontend-static:
+ name: "${COMPOSE_PROJECT_NAME}_frontend-static"
diff --git a/docker/frontend-service.Dockerfile b/docker/frontend-service.Dockerfile
new file mode 100644
index 000000000..132e185e0
--- /dev/null
+++ b/docker/frontend-service.Dockerfile
@@ -0,0 +1,39 @@
+# ==== builder ====
+FROM node:20-bullseye AS builder
+WORKDIR /app
+
+# 通过构建参数选择环境:staging / production(默认 staging)
+ARG NUXT_ENV=staging
+ENV NODE_ENV=production \
+ NUXT_TELEMETRY_DISABLED=1
+
+# 复制源代码(假设仓库根目录包含 frontend_nuxt)
+# 构建上下文由 docker-compose 指向仓库根目录
+COPY ./frontend_nuxt/package*.json /app/
+RUN npm ci
+
+# 拷贝剩余代码
+COPY ./frontend_nuxt/ /app/
+
+# 若存在环境样例文件,则在构建期复制为 .env(你也可以用 --build-arg 覆盖)
+RUN if [ -f ".env.${NUXT_ENV}.example" ]; then cp ".env.${NUXT_ENV}.example" .env; fi
+
+# 构建 SSR:产物在 .output
+RUN npm run build
+
+# ==== runner ====
+FROM node:20-alpine AS runner
+WORKDIR /app
+ENV NODE_ENV=production \
+ NUXT_TELEMETRY_DISABLED=1 \
+ PORT=3000 \
+ HOST=0.0.0.0
+
+# 复制构建产物
+COPY --from=builder /app/.output /app/.output
+
+# 健康检查(简洁起见,探测首页)
+HEALTHCHECK --interval=10s --timeout=5s --retries=30 CMD wget -qO- http://127.0.0.1:${PORT}/ >/dev/null 2>&1 || exit 1
+
+EXPOSE 3000
+CMD ["node", ".output/server/index.mjs"]
diff --git a/docker/mysql/conf.d/charset.cnf b/docker/mysql/conf.d/charset.cnf
new file mode 100644
index 000000000..2464cce02
--- /dev/null
+++ b/docker/mysql/conf.d/charset.cnf
@@ -0,0 +1,10 @@
+[mysqld]
+character-set-server = utf8mb4
+collation-server = utf8mb4_0900_ai_ci
+skip-character-set-client-handshake
+
+[client]
+default-character-set = utf8mb4
+
+[mysql]
+default-character-set = utf8mb4
diff --git a/docker/DockerFile b/docker/opensearch.Dockerfile
similarity index 100%
rename from docker/DockerFile
rename to docker/opensearch.Dockerfile
diff --git a/docker/rabbitmq/conf/enabled_plugins b/docker/rabbitmq/conf/enabled_plugins
new file mode 100644
index 000000000..5b44fea2e
--- /dev/null
+++ b/docker/rabbitmq/conf/enabled_plugins
@@ -0,0 +1 @@
+[rabbitmq_management, rabbitmq_prometheus].
diff --git a/docker/rabbitmq/conf/rabbitmq.conf b/docker/rabbitmq/conf/rabbitmq.conf
new file mode 100644
index 000000000..7734d03e0
--- /dev/null
+++ b/docker/rabbitmq/conf/rabbitmq.conf
@@ -0,0 +1,6 @@
+# 管理插件加载 definitions(仅空库时生效)
+management.load_definitions = /etc/rabbitmq/definitions.json
+
+# (可选)禁用管理老式统计采集,转 Prometheus,避免弃用告警
+management_agent.disable_metrics_collector = true
+management.disable_stats = true
diff --git a/docker/rabbitmq/definitions.json b/docker/rabbitmq/definitions.json
new file mode 100644
index 000000000..bf8fc561f
--- /dev/null
+++ b/docker/rabbitmq/definitions.json
@@ -0,0 +1,31 @@
+{
+ "users": [
+ { "name": "nagisa", "password": "nagisa", "tags": "administrator" }
+ ],
+ "vhosts": [{ "name": "/" }],
+ "permissions": [
+ { "user": "nagisa", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*" }
+ ],
+ "queues": [
+ { "name": "notifications-queue", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-0", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-1", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-2", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-3", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-4", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-5", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-6", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-7", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-8", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-9", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-a", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-b", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-c", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-d", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-e", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} },
+ { "name": "notifications-queue-f", "vhost": "/", "durable": true, "auto_delete": false, "arguments": {} }
+ ],
+ "exchanges": [],
+ "bindings": []
+ }
+
\ No newline at end of file
diff --git a/frontend_nuxt/.env.dev.example b/frontend_nuxt/.env.dev.example
index bb0ee2635..9a41ba7af 100644
--- a/frontend_nuxt/.env.dev.example
+++ b/frontend_nuxt/.env.dev.example
@@ -1,12 +1,3 @@
-; 本地部署后端
-NUXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8080
-NUXT_PUBLIC_WEBSOCKET_URL=https://127.0.0.1:8082
-NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000
-
-NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
-# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
-; 本地
-NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
-NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
-NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
-NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
+# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
+# 如需在本地运行 Nuxt,请复制对应的示例文件到项目根目录:
+# cp ../.env.dev.example ../.env
diff --git a/frontend_nuxt/.env.example b/frontend_nuxt/.env.example
index dadb36387..bcbdb7bf6 100644
--- a/frontend_nuxt/.env.example
+++ b/frontend_nuxt/.env.example
@@ -1,19 +1,5 @@
-; 本地部署后端
-; NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081
-; 预发环境后端
-; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com
-; 生产环境后端
-NUXT_PUBLIC_API_BASE_URL=https://open-isle.com
-
-; 生产环境ws后端
-NUXT_PUBLIC_WEBSOCKET_URL=https://open-isle.com/websocket
-
-; 预发环境
-; NUXT_PUBLIC_WEBSITE_BASE_URL=https://staging.open-isle.com
-; 正式环境/生产环境
-NUXT_PUBLIC_WEBSITE_BASE_URL=https://open-isle.com
-NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
-NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
-NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
-NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
-NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
+# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
+# 根据环境选择对应文件复制至项目根目录:
+# cp ../.env.dev.example ../.env
+# cp ../.env.staging.example ../.env
+# cp ../.env.production.example ../.env
diff --git a/frontend_nuxt/.env.production.example b/frontend_nuxt/.env.production.example
index a1bff6842..ee3573dd6 100644
--- a/frontend_nuxt/.env.production.example
+++ b/frontend_nuxt/.env.production.example
@@ -1,13 +1,3 @@
-
-; 生产环境后端
-NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
-; 正式环境/生产环境
-NUXT_PUBLIC_WEBSITE_BASE_URL=https://www.open-isle.com
-; 生产环境ws后端
-NUXT_PUBLIC_WEBSOCKET_URL=https://www.open-isle.com/websocket
-
-NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
-NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
-NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
-NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
-NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
+# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
+# 如需配置生产环境,请复制并修改对应示例文件:
+# cp ../.env.production.example ../.env
diff --git a/frontend_nuxt/.env.staging.example b/frontend_nuxt/.env.staging.example
index 0cc8ef140..dde477e44 100644
--- a/frontend_nuxt/.env.staging.example
+++ b/frontend_nuxt/.env.staging.example
@@ -1,17 +1,3 @@
-; 本地部署后端
-; NUXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8080
-
-; 预发环境后端
-NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com
-
-; 预发环境ws后端
-NUXT_PUBLIC_WEBSOCKET_URL=https://staging.open-isle.com/websocket
-
-; 预发环境
-NUXT_PUBLIC_WEBSITE_BASE_URL=https://staging.open-isle.com
-
-NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
-NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
-NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
-NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
-NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
+# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
+# 如需配置预发环境,请复制并修改对应示例文件:
+# cp ../.env.staging.example ../.env
diff --git a/frontend_nuxt/nuxt.config.ts b/frontend_nuxt/nuxt.config.ts
index 0c5bea8ac..b2c7784b7 100644
--- a/frontend_nuxt/nuxt.config.ts
+++ b/frontend_nuxt/nuxt.config.ts
@@ -9,7 +9,9 @@ export default defineNuxtConfig({
modules: ['@nuxt/image'],
runtimeConfig: {
public: {
- apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || '',
+ apiBaseUrl: process.server
+ ? process.env.NUXT_PUBLIC_API_BASE_URL_SSR
+ : process.env.NUXT_PUBLIC_API_BASE_URL,
websocketUrl: process.env.NUXT_PUBLIC_WEBSOCKET_URL || '',
websiteBaseUrl: process.env.NUXT_PUBLIC_WEBSITE_BASE_URL || '',
googleClientId: process.env.NUXT_PUBLIC_GOOGLE_CLIENT_ID || '',
diff --git a/websocket_service/pom.xml b/websocket_service/pom.xml
index 37d29413c..4091615b2 100644
--- a/websocket_service/pom.xml
+++ b/websocket_service/pom.xml
@@ -51,10 +51,10 @@
lombok
true
-
-
-
-
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
org.springframework.boot
spring-boot-starter-test
diff --git a/websocket_service/src/main/java/com/openisle/websocket/security/SecurityConfig.java b/websocket_service/src/main/java/com/openisle/websocket/security/SecurityConfig.java
index 06b924bd8..a0083ea6f 100644
--- a/websocket_service/src/main/java/com/openisle/websocket/security/SecurityConfig.java
+++ b/websocket_service/src/main/java/com/openisle/websocket/security/SecurityConfig.java
@@ -43,6 +43,8 @@ public class SecurityConfig {
"http://30.211.97.238",
"http://192.168.7.98",
"http://192.168.7.98:3000",
+ "http://frontend_dev:3000",
+ "http://frontend_service:3000",
websiteUrl,
websiteUrl.replace("://www.", "://")
));
diff --git a/websocket_service/src/main/resources/application.properties b/websocket_service/src/main/resources/application.properties
index 0bce91d31..bd7de2ef9 100644
--- a/websocket_service/src/main/resources/application.properties
+++ b/websocket_service/src/main/resources/application.properties
@@ -1,4 +1,4 @@
-server.port=${SERVER_PORT:8082}
+server.port=${WEBSOCKET_PORT:8082}
# 服务器配置
spring.application.name=websocket-service
@@ -19,4 +19,7 @@ logging.level.org.springframework.messaging=${MESSAGING_LOG_LEVEL:DEBUG}
logging.level.org.springframework.web.socket=${WEBSOCKET_LOG_LEVEL:DEBUG}
# 网站 URL 配置
-app.website-url=${WEBSITE_URL:https://www.open-isle.com}
\ No newline at end of file
+app.website-url=${WEBSITE_URL:https://www.open-isle.com}
+
+management.endpoints.web.exposure.include=health,info
+management.endpoint.health.probes.enabled=true
\ No newline at end of file
diff --git a/websocket_service/websocket_service.env.example b/websocket_service/websocket_service.env.example
index 9f6304269..79e51ccdf 100644
--- a/websocket_service/websocket_service.env.example
+++ b/websocket_service/websocket_service.env.example
@@ -1,3 +1,5 @@
+# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
+# 如需在独立环境中运行,可参考以下字段:
SERVER_PORT=
# RabbitMQ 配置