mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-19 13:30:55 +08:00
Compare commits
55 Commits
feature/op
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0033374481 | ||
|
|
8f36422609 | ||
|
|
b98871bed9 | ||
|
|
2cb8c12f65 | ||
|
|
87a256ba0c | ||
|
|
737157e557 | ||
|
|
6f9570dc95 | ||
|
|
12bc405856 | ||
|
|
a2b0cd1a47 | ||
|
|
25a7f1e138 | ||
|
|
a6dd2bfbc2 | ||
|
|
a0ea63700f | ||
|
|
b49e20d010 | ||
|
|
e44443a605 | ||
|
|
0a3bfb9451 | ||
|
|
adfc05b9b2 | ||
|
|
18a6953ff7 | ||
|
|
181ac7bc8f | ||
|
|
9dc9ca9bd8 | ||
|
|
2457efd11d | ||
|
|
b62b9c691f | ||
|
|
180c45bf2d | ||
|
|
263f2deeb1 | ||
|
|
22b813e40b | ||
|
|
d00dbbbd03 | ||
|
|
3b92bdaf2a | ||
|
|
7ce5de7f7c | ||
|
|
28618c7452 | ||
|
|
f8a2ee6ee9 | ||
|
|
ca26b931da | ||
|
|
24fe90cfc6 | ||
|
|
5971700e8a | ||
|
|
f872a32410 | ||
|
|
fffd335ebb | ||
|
|
287d52df10 | ||
|
|
73790d1992 | ||
|
|
3d5cee6e68 | ||
|
|
2f509cc2d8 | ||
|
|
35c503eb6c | ||
|
|
0cf8113691 | ||
|
|
b2a29913aa | ||
|
|
2b6d7c5ab9 | ||
|
|
e9878487e8 | ||
|
|
201af061e4 | ||
|
|
4080f60f60 | ||
|
|
06d76438e8 | ||
|
|
bb955c98ba | ||
|
|
a12368602d | ||
|
|
208c875868 | ||
|
|
39ae8c02cb | ||
|
|
0119605649 | ||
|
|
0d7dc93a67 | ||
|
|
774611f3a8 | ||
|
|
61f6e7c90a | ||
|
|
892aa6a7c6 |
105
.env.example
Normal file
105
.env.example
Normal file
@@ -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
|
||||||
|
|
||||||
4
.github/workflows/deploy.yml
vendored
4
.github/workflows/deploy.yml
vendored
@@ -2,8 +2,8 @@ name: CI & CD
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
# schedule:
|
||||||
- cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
# - cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
|
||||||
|
# 此文件保留作参考用途,如需在 Docker 之外手动配置,可按需复制。
|
||||||
|
|
||||||
# === Spring Boot ===
|
# === Spring Boot ===
|
||||||
SERVER_PORT=8080
|
SERVER_PORT=8080
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,10 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
||||||
<version>2.2.0</version>
|
<version>2.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- 高阶 Java 客户端 -->
|
<!-- 高阶 Java 客户端 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.opensearch.client</groupId>
|
<groupId>org.opensearch.client</groupId>
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ public class SecurityConfig {
|
|||||||
"http://localhost:8081",
|
"http://localhost:8081",
|
||||||
"http://localhost:8082",
|
"http://localhost:8082",
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
|
"http://frontend_dev:3000",
|
||||||
|
"http://frontend_service:3000",
|
||||||
"http://localhost:3001",
|
"http://localhost:3001",
|
||||||
"http://localhost",
|
"http://localhost",
|
||||||
"http://30.211.97.238:3000",
|
"http://30.211.97.238:3000",
|
||||||
@@ -177,6 +179,8 @@ public class SecurityConfig {
|
|||||||
.permitAll()
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/point-goods")
|
.requestMatchers(HttpMethod.POST, "/api/point-goods")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
.requestMatchers("/actuator/**")
|
||||||
|
.permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/categories/**")
|
.requestMatchers(HttpMethod.POST, "/api/categories/**")
|
||||||
.hasAuthority("ADMIN")
|
.hasAuthority("ADMIN")
|
||||||
.requestMatchers(HttpMethod.POST, "/api/tags/**")
|
.requestMatchers(HttpMethod.POST, "/api/tags/**")
|
||||||
@@ -230,6 +234,7 @@ public class SecurityConfig {
|
|||||||
uri.startsWith("/api/channels") ||
|
uri.startsWith("/api/channels") ||
|
||||||
uri.startsWith("/api/sitemap.xml") ||
|
uri.startsWith("/api/sitemap.xml") ||
|
||||||
uri.startsWith("/api/medals") ||
|
uri.startsWith("/api/medals") ||
|
||||||
|
uri.startsWith("/actuator") ||
|
||||||
uri.startsWith("/api/rss"));
|
uri.startsWith("/api/rss"));
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.openisle.controller;
|
||||||
|
|
||||||
|
import com.openisle.dto.AdminGrantPointRequest;
|
||||||
|
import com.openisle.service.PointService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/points")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminPointController {
|
||||||
|
|
||||||
|
private final PointService pointService;
|
||||||
|
|
||||||
|
@PostMapping("/grant")
|
||||||
|
@SecurityRequirement(name = "JWT")
|
||||||
|
@Operation(summary = "Grant points", description = "Grant points to a user as administrator")
|
||||||
|
@ApiResponse(responseCode = "200", description = "Points granted")
|
||||||
|
public Map<String, Object> grant(
|
||||||
|
@RequestBody AdminGrantPointRequest request,
|
||||||
|
Authentication auth
|
||||||
|
) {
|
||||||
|
String username = request.getUsername();
|
||||||
|
int balance = pointService.grantPointByAdmin(auth.getName(), username, request.getAmount());
|
||||||
|
return Map.of("username", username.trim(), "point", balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class AdminGrantPointRequest {
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
private int amount;
|
||||||
|
}
|
||||||
@@ -13,4 +13,5 @@ public enum PointHistoryType {
|
|||||||
REDEEM,
|
REDEEM,
|
||||||
LOTTERY_JOIN,
|
LOTTERY_JOIN,
|
||||||
LOTTERY_REWARD,
|
LOTTERY_REWARD,
|
||||||
|
ADMIN_GRANT,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,22 @@ public class PointService {
|
|||||||
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int grantPointByAdmin(String adminName, String targetUsername, int amount) {
|
||||||
|
if (amount <= 0) {
|
||||||
|
throw new FieldException("amount", "积分必须为正数");
|
||||||
|
}
|
||||||
|
if (targetUsername == null || targetUsername.isBlank()) {
|
||||||
|
throw new FieldException("username", "用户名不能为空");
|
||||||
|
}
|
||||||
|
String normalizedUsername = targetUsername.trim();
|
||||||
|
User admin = userRepository.findByUsername(adminName).orElseThrow();
|
||||||
|
User target = userRepository
|
||||||
|
.findByUsername(normalizedUsername)
|
||||||
|
.orElseThrow(() -> new FieldException("username", "用户不存在"));
|
||||||
|
addPoint(target, amount, PointHistoryType.ADMIN_GRANT, null, null, admin);
|
||||||
|
return target.getPoint();
|
||||||
|
}
|
||||||
|
|
||||||
public void processLotteryJoin(User participant, LotteryPost post) {
|
public void processLotteryJoin(User participant, LotteryPost post) {
|
||||||
int cost = post.getPointCost();
|
int cost = post.getPointCost();
|
||||||
if (cost > 0) {
|
if (cost > 0) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ server.port=${SERVER_PORT:8080}
|
|||||||
# for mysql
|
# for mysql
|
||||||
logging.level.root=${LOG_LEVEL:INFO}
|
logging.level.root=${LOG_LEVEL:INFO}
|
||||||
logging.level.com.openisle.service.CosImageUploader=DEBUG
|
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}?useUnicode=yes&characterEncoding=UTF-8&useInformationSchema=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
|
||||||
spring.datasource.username=${MYSQL_USER:root}
|
spring.datasource.username=${MYSQL_USER:root}
|
||||||
spring.datasource.password=${MYSQL_PASSWORD:password}
|
spring.datasource.password=${MYSQL_PASSWORD:password}
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
@@ -47,11 +47,11 @@ app.snippet-length=${SNIPPET_LENGTH:200}
|
|||||||
|
|
||||||
# OpenSearch integration
|
# OpenSearch integration
|
||||||
app.search.enabled=${SEARCH_ENABLED:true}
|
app.search.enabled=${SEARCH_ENABLED:true}
|
||||||
app.search.host=${SEARCH_HOST:localhost}
|
app.search.host=${OPENSEARCH_HOST:opensearch}
|
||||||
app.search.port=${SEARCH_PORT:9200}
|
app.search.port=${OPENSEARCH_PORT:9200}
|
||||||
app.search.scheme=${SEARCH_SCHEME:http}
|
app.search.scheme=${OPENSEARCH_SCHEME:http}
|
||||||
app.search.username=${SEARCH_USERNAME:}
|
app.search.username=${OPENSEARCH_USERNAME:}
|
||||||
app.search.password=${SEARCH_PASSWORD:}
|
app.search.password=${OPENSEARCH_PASSWORD:}
|
||||||
app.search.index-prefix=${SEARCH_INDEX_PREFIX:openisle}
|
app.search.index-prefix=${SEARCH_INDEX_PREFIX:openisle}
|
||||||
app.search.highlight-fragment-size=${SEARCH_HIGHLIGHT_FRAGMENT_SIZE:${SNIPPET_LENGTH:200}}
|
app.search.highlight-fragment-size=${SEARCH_HIGHLIGHT_FRAGMENT_SIZE:${SNIPPET_LENGTH:200}}
|
||||||
app.search.reindex-on-startup=${SEARCH_REINDEX_ON_STARTUP:true}
|
app.search.reindex-on-startup=${SEARCH_REINDEX_ON_STARTUP:true}
|
||||||
@@ -81,15 +81,15 @@ cos.bucket-name=${COS_BUCKET_NAME:}
|
|||||||
# your image upload services: ...
|
# your image upload services: ...
|
||||||
|
|
||||||
# Google OAuth configuration
|
# Google OAuth configuration
|
||||||
google.client-id=${GOOGLE_CLIENT_ID:}
|
google.client-id=${NUXT_PUBLIC_GOOGLE_CLIENT_ID:}
|
||||||
# GitHub OAuth configuration
|
# GitHub OAuth configuration
|
||||||
github.client-id=${GITHUB_CLIENT_ID:}
|
github.client-id=${NUXT_PUBLIC_GITHUB_CLIENT_ID:}
|
||||||
github.client-secret=${GITHUB_CLIENT_SECRET:}
|
github.client-secret=${GITHUB_CLIENT_SECRET:}
|
||||||
# Discord OAuth configuration
|
# Discord OAuth configuration
|
||||||
discord.client-id=${DISCORD_CLIENT_ID:}
|
discord.client-id=${NUXT_PUBLIC_DISCORD_CLIENT_ID:}
|
||||||
discord.client-secret=${DISCORD_CLIENT_SECRET:}
|
discord.client-secret=${DISCORD_CLIENT_SECRET:}
|
||||||
# Twitter OAuth configuration
|
# Twitter OAuth configuration
|
||||||
twitter.client-id=${TWITTER_CLIENT_ID:}
|
twitter.client-id=${NUXT_PUBLIC_TWITTER_CLIENT_ID:}
|
||||||
twitter.client-secret=${TWITTER_CLIENT_SECRET:}
|
twitter.client-secret=${TWITTER_CLIENT_SECRET:}
|
||||||
# Telegram login configuration
|
# Telegram login configuration
|
||||||
telegram.bot-token=${TELEGRAM_BOT_TOKEN:}
|
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.version=0.0.1
|
||||||
springdoc.info.scheme=Bearer
|
springdoc.info.scheme=Bearer
|
||||||
springdoc.info.header=Authorization
|
springdoc.info.header=Authorization
|
||||||
|
|
||||||
|
management.endpoints.web.exposure.include=health,info
|
||||||
|
management.endpoint.health.probes.enabled=true
|
||||||
13
backend/src/main/resources/db/init/00_init_db_and_user.sql
Normal file
13
backend/src/main/resources/db/init/00_init_db_and_user.sql
Normal file
@@ -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`;
|
||||||
54
backend/src/main/resources/db/init/01_schema.sql
Normal file
54
backend/src/main/resources/db/init/01_schema.sql
Normal file
@@ -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;
|
||||||
26
backend/src/main/resources/db/init/02_seed_data.sql
Normal file
26
backend/src/main/resources/db/init/02_seed_data.sql
Normal file
@@ -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;
|
||||||
@@ -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);
|
|
||||||
56
deploy/deploy.sh
Normal file
56
deploy/deploy.sh
Normal file
@@ -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)"
|
||||||
57
deploy/deploy_staging.sh
Normal file
57
deploy/deploy_staging.sh
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 可用法:
|
||||||
|
# ./deploy-staging.sh
|
||||||
|
# ./deploy-staging.sh feature/docker
|
||||||
|
deploy_branch="${1:-main}"
|
||||||
|
|
||||||
|
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)"
|
||||||
@@ -1,16 +1,4 @@
|
|||||||
# 前端访问端口
|
# 已迁移到仓库根目录的 .env.*.example 文件。
|
||||||
SERVER_PORT=8080
|
# 请复制对应环境的示例文件到项目根目录,例如:
|
||||||
|
# cp ../.env.dev.example ../.env
|
||||||
# OpenSearch 配置
|
# docker-compose 将自动读取 ../.env。
|
||||||
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=<数据库密码>
|
|
||||||
|
|||||||
1
docker/.gitignore
vendored
Normal file
1
docker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
data
|
||||||
@@ -2,25 +2,37 @@ services:
|
|||||||
# MySQL service
|
# MySQL service
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: openisle-mysql
|
container_name: ${COMPOSE_PROJECT_NAME}-openisle-mysql
|
||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
- ../backend/open-isle.env
|
- ${ENV_FILE:-../.env}
|
||||||
- ./.env
|
command: >
|
||||||
|
--character-set-server=utf8mb4
|
||||||
|
--collation-server=utf8mb4_0900_ai_ci
|
||||||
|
--default-time-zone=+08:00
|
||||||
|
--skip-character-set-client-handshake
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_PORT}:3306"
|
- "${MYSQL_PORT:-3306}:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- mysql-data:/var/lib/mysql
|
- 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:
|
networks:
|
||||||
- openisle-network
|
- 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 Service
|
||||||
opensearch:
|
opensearch:
|
||||||
|
user: "1000:1000"
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: opensearch.Dockerfile
|
||||||
container_name: opensearch
|
container_name: ${COMPOSE_PROJECT_NAME}-opensearch
|
||||||
environment:
|
environment:
|
||||||
- cluster.name=os-single
|
- cluster.name=os-single
|
||||||
- node.name=os-node-1
|
- node.name=os-node-1
|
||||||
@@ -31,53 +43,253 @@ services:
|
|||||||
- cluster.blocks.create_index=false
|
- cluster.blocks.create_index=false
|
||||||
ulimits:
|
ulimits:
|
||||||
memlock: { soft: -1, hard: -1 }
|
memlock: { soft: -1, hard: -1 }
|
||||||
nofile: { soft: 65536, hard: 65536 }
|
nofile: { soft: 65536, hard: 65536 }
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/usr/share/opensearch/data
|
- ${OPENSEARCH_DATA_DIR:-./data}:/usr/share/opensearch/data
|
||||||
- ./snapshots:/snapshots
|
- ${OPENSEARCH_SNAPSHOT_DIR:-./snapshots}:/snapshots
|
||||||
ports:
|
ports:
|
||||||
- "${OPENSEARCH_PORT:-9200}:9200"
|
- "${OPENSEARCH_PORT:-9200}:9200"
|
||||||
- "${OPENSEARCH_METRICS_PORT:-9600}:9600"
|
- "${OPENSEARCH_METRICS_PORT:-9600}:9600"
|
||||||
restart: unless-stopped
|
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:
|
dashboards:
|
||||||
image: opensearchproject/opensearch-dashboards:3.0.0
|
image: opensearchproject/opensearch-dashboards:3.0.0
|
||||||
container_name: os-dashboards
|
container_name: ${COMPOSE_PROJECT_NAME}-os-dashboards
|
||||||
environment:
|
environment:
|
||||||
- OPENSEARCH_HOSTS=["http://opensearch:9200"]
|
OPENSEARCH_HOSTS: '["http://opensearch:9200"]'
|
||||||
- DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
|
DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true"
|
||||||
ports:
|
ports:
|
||||||
- "${OPENSEARCH_DASHBOARDS_PORT:-5601}:5601"
|
- "${OPENSEARCH_DASHBOARDS_PORT:-5601}:5601"
|
||||||
depends_on:
|
depends_on:
|
||||||
- opensearch
|
- opensearch
|
||||||
restart: unless-stopped
|
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:
|
springboot:
|
||||||
image: maven:3.9-eclipse-temurin-17
|
image: maven:3.9-eclipse-temurin-17
|
||||||
container_name: openisle-springboot
|
container_name: ${COMPOSE_PROJECT_NAME}-openisle-springboot
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
env_file:
|
env_file:
|
||||||
- ../backend/open-isle.env
|
- ${ENV_FILE:-../.env}
|
||||||
- ./.env
|
|
||||||
environment:
|
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:
|
ports:
|
||||||
- "${SERVER_PORT}:8080"
|
- "${SERVER_PORT:-8080}:${SERVER_PORT:-8080}"
|
||||||
volumes:
|
volumes:
|
||||||
- ../backend:/app
|
- ../backend:/app
|
||||||
- maven-repo:/root/.m2
|
- maven-repo:/root/.m2
|
||||||
depends_on:
|
depends_on:
|
||||||
- mysql
|
mysql:
|
||||||
command: mvn clean spring-boot:run -Dmaven.test.skip=true
|
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:
|
networks:
|
||||||
- openisle-network
|
- 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:
|
networks:
|
||||||
openisle-network:
|
openisle-network:
|
||||||
|
name: "${COMPOSE_PROJECT_NAME}_net"
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
|
name: "${COMPOSE_PROJECT_NAME}_mysql-data"
|
||||||
maven-repo:
|
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"
|
||||||
|
|||||||
39
docker/frontend-service.Dockerfile
Normal file
39
docker/frontend-service.Dockerfile
Normal file
@@ -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"]
|
||||||
10
docker/mysql/conf.d/charset.cnf
Normal file
10
docker/mysql/conf.d/charset.cnf
Normal file
@@ -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
|
||||||
1
docker/rabbitmq/conf/enabled_plugins
Normal file
1
docker/rabbitmq/conf/enabled_plugins
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[rabbitmq_management, rabbitmq_prometheus].
|
||||||
6
docker/rabbitmq/conf/rabbitmq.conf
Normal file
6
docker/rabbitmq/conf/rabbitmq.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 管理插件加载 definitions(仅空库时生效)
|
||||||
|
management.load_definitions = /etc/rabbitmq/definitions.json
|
||||||
|
|
||||||
|
# (可选)禁用管理老式统计采集,转 Prometheus,避免弃用告警
|
||||||
|
management_agent.disable_metrics_collector = true
|
||||||
|
management.disable_stats = true
|
||||||
31
docker/rabbitmq/definitions.json
Normal file
31
docker/rabbitmq/definitions.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
; 本地部署后端
|
# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
|
||||||
NUXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8080
|
# 如需在本地运行 Nuxt,请复制对应的示例文件到项目根目录:
|
||||||
NUXT_PUBLIC_WEBSOCKET_URL=https://127.0.0.1:8082
|
# cp ../.env.dev.example ../.env
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
; 本地部署后端
|
# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
|
||||||
; NUXT_PUBLIC_API_BASE_URL=https://127.0.0.1:8081
|
# 根据环境选择对应文件复制至项目根目录:
|
||||||
; 预发环境后端
|
# cp ../.env.dev.example ../.env
|
||||||
; NUXT_PUBLIC_API_BASE_URL=https://staging.open-isle.com
|
# cp ../.env.staging.example ../.env
|
||||||
; 生产环境后端
|
# cp ../.env.production.example ../.env
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
|
# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
|
||||||
; 生产环境后端
|
# 如需配置生产环境,请复制并修改对应示例文件:
|
||||||
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
# cp ../.env.production.example ../.env
|
||||||
; 正式环境/生产环境
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,17 +1,3 @@
|
|||||||
; 本地部署后端
|
# 环境变量已统一迁移至仓库根目录的 .env.*.example 文件。
|
||||||
; NUXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8080
|
# 如需配置预发环境,请复制并修改对应示例文件:
|
||||||
|
# cp ../.env.staging.example ../.env
|
||||||
; 预发环境后端
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default {
|
|||||||
|
|
||||||
.cropper-btn {
|
.cropper-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -128,7 +128,7 @@ export default {
|
|||||||
|
|
||||||
.cropper-btn.primary {
|
.cropper-btn.primary {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
color: var(--text-color);
|
color: #ffff;
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export default {
|
|||||||
border: 1px solid var(--normal-border-color);
|
border: 1px solid var(--normal-border-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -315,8 +316,9 @@ export default {
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
border: 1px solid var(--normal-border-color);
|
border: 1px solid var(--normal-border-color);
|
||||||
|
border-radius: 5px;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
max-height: 200px;
|
max-height: 300px;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export default defineNuxtConfig({
|
|||||||
modules: ['@nuxt/image'],
|
modules: ['@nuxt/image'],
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
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 || '',
|
websocketUrl: process.env.NUXT_PUBLIC_WEBSOCKET_URL || '',
|
||||||
websiteBaseUrl: process.env.NUXT_PUBLIC_WEBSITE_BASE_URL || '',
|
websiteBaseUrl: process.env.NUXT_PUBLIC_WEBSITE_BASE_URL || '',
|
||||||
googleClientId: process.env.NUXT_PUBLIC_GOOGLE_CLIENT_ID || '',
|
googleClientId: process.env.NUXT_PUBLIC_GOOGLE_CLIENT_ID || '',
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ export default {
|
|||||||
label: '隐私政策',
|
label: '隐私政策',
|
||||||
file: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/about/privacy.md',
|
file: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/about/privacy.md',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'points',
|
||||||
|
label: '积分说明',
|
||||||
|
content: `# 积分说明
|
||||||
|
|
||||||
|
- 积分可用于兑换商品、参与抽奖等社区玩法。
|
||||||
|
- 管理员可以通过后台新增的积分模块为用户发放奖励积分。
|
||||||
|
- 每次发放都会记录在积分历史中,方便你查看积分来源。
|
||||||
|
`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'api',
|
key: 'api',
|
||||||
label: 'API与调试',
|
label: 'API与调试',
|
||||||
@@ -88,11 +98,21 @@ export default {
|
|||||||
return `${token.value.slice(0, 20)}...${token.value.slice(-10)}`
|
return `${token.value.slice(0, 20)}...${token.value.slice(-10)}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadContent = async (file) => {
|
const loadContent = async (tab) => {
|
||||||
if (!file) return
|
if (!tab || tab.key === 'api') return
|
||||||
|
if (tab.content) {
|
||||||
|
isFetching.value = false
|
||||||
|
content.value = tab.content
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!tab.file) {
|
||||||
|
isFetching.value = false
|
||||||
|
content.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
isFetching.value = true
|
isFetching.value = true
|
||||||
const res = await fetch(file)
|
const res = await fetch(tab.file)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
content.value = await res.text()
|
content.value = await res.text()
|
||||||
} else {
|
} else {
|
||||||
@@ -110,15 +130,15 @@ export default {
|
|||||||
if (initTab && tabs.find((t) => t.key === initTab)) {
|
if (initTab && tabs.find((t) => t.key === initTab)) {
|
||||||
selectedTab.value = initTab
|
selectedTab.value = initTab
|
||||||
const tab = tabs.find((t) => t.key === initTab)
|
const tab = tabs.find((t) => t.key === initTab)
|
||||||
if (tab && tab.file) loadContent(tab.file)
|
if (tab) loadContent(tab)
|
||||||
} else {
|
} else {
|
||||||
loadContent(tabs[0].file)
|
loadContent(tabs[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(selectedTab, (name) => {
|
watch(selectedTab, (name) => {
|
||||||
const tab = tabs.find((t) => t.key === name)
|
const tab = tabs.find((t) => t.key === name)
|
||||||
if (tab && tab.file) loadContent(tab.file)
|
if (tab) loadContent(tab)
|
||||||
router.replace({ query: { ...route.query, tab: name } })
|
router.replace({ query: { ...route.query, tab: name } })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,6 +147,8 @@ export default {
|
|||||||
(name) => {
|
(name) => {
|
||||||
if (name && name !== selectedTab.value && tabs.find((t) => t.key === name)) {
|
if (name && name !== selectedTab.value && tabs.find((t) => t.key === name)) {
|
||||||
selectedTab.value = name
|
selectedTab.value = name
|
||||||
|
const tab = tabs.find((t) => t.key === name)
|
||||||
|
if (tab) loadContent(tab)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -184,6 +184,16 @@
|
|||||||
}}</NuxtLink>
|
}}</NuxtLink>
|
||||||
参与,获得 {{ item.amount }} 积分
|
参与,获得 {{ item.amount }} 积分
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="item.type === 'ADMIN_GRANT' && item.fromUserId">
|
||||||
|
管理员
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
赠送了 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'ADMIN_GRANT'">
|
||||||
|
管理员赠送了 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
||||||
<paper-money-two /> 你目前的积分是 {{ item.balance }}
|
<paper-money-two /> 你目前的积分是 {{ item.balance }}
|
||||||
</div>
|
</div>
|
||||||
@@ -229,6 +239,7 @@ const pointRules = [
|
|||||||
'评论被点赞:每次 10 积分',
|
'评论被点赞:每次 10 积分',
|
||||||
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
||||||
'文章被收录至精选:每次 500 积分',
|
'文章被收录至精选:每次 500 积分',
|
||||||
|
'管理员赠送:特殊活动可由管理员手动赠送积分',
|
||||||
]
|
]
|
||||||
|
|
||||||
const goods = ref([])
|
const goods = ref([])
|
||||||
@@ -250,6 +261,7 @@ const iconMap = {
|
|||||||
LOTTERY_REWARD: 'fireworks',
|
LOTTERY_REWARD: 'fireworks',
|
||||||
POST_LIKE_CANCELLED: 'clear-icon',
|
POST_LIKE_CANCELLED: 'clear-icon',
|
||||||
COMMENT_LIKE_CANCELLED: 'clear-icon',
|
COMMENT_LIKE_CANCELLED: 'clear-icon',
|
||||||
|
ADMIN_GRANT: 'paper-money-two',
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadTrend = async () => {
|
const loadTrend = async () => {
|
||||||
|
|||||||
@@ -65,6 +65,35 @@
|
|||||||
<div class="setting-title">注册模式</div>
|
<div class="setting-title">注册模式</div>
|
||||||
<Dropdown v-model="registerMode" :fetch-options="fetchRegisterModes" />
|
<Dropdown v-model="registerMode" :fetch-options="fetchRegisterModes" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row grant-row">
|
||||||
|
<div class="setting-title">发放积分</div>
|
||||||
|
<div class="grant-form">
|
||||||
|
<BaseInput
|
||||||
|
v-model="grantUsername"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
class="grant-input"
|
||||||
|
@input="grantError = ''"
|
||||||
|
/>
|
||||||
|
<BaseInput
|
||||||
|
v-model="grantAmount"
|
||||||
|
type="number"
|
||||||
|
placeholder="积分数量"
|
||||||
|
class="grant-input amount"
|
||||||
|
@input="grantError = ''"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="grant-button"
|
||||||
|
:class="{ disabled: isGrantingPoints }"
|
||||||
|
:disabled="isGrantingPoints"
|
||||||
|
@click="grantPoint"
|
||||||
|
>
|
||||||
|
{{ isGrantingPoints ? '发放中...' : '发放' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="grantError" class="grant-error-message">{{ grantError }}</div>
|
||||||
|
<div class="setting-description">积分会立即发放给目标用户,并记录在积分历史中</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div v-if="isSaving" class="save-button disabled">保存中...</div>
|
<div v-if="isSaving" class="save-button disabled">保存中...</div>
|
||||||
@@ -102,6 +131,10 @@ const registerMode = ref('DIRECT')
|
|||||||
const isLoadingPage = ref(false)
|
const isLoadingPage = ref(false)
|
||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
const frosted = ref(true)
|
const frosted = ref(true)
|
||||||
|
const grantUsername = ref('')
|
||||||
|
const grantAmount = ref('')
|
||||||
|
const grantError = ref('')
|
||||||
|
const isGrantingPoints = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
isLoadingPage.value = true
|
isLoadingPage.value = true
|
||||||
@@ -184,6 +217,55 @@ const loadAdminConfig = async () => {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const grantPoint = async () => {
|
||||||
|
if (isGrantingPoints.value) return
|
||||||
|
const username = grantUsername.value.trim()
|
||||||
|
if (!username) {
|
||||||
|
grantError.value = '用户名不能为空'
|
||||||
|
toast.error(grantError.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const amount = Number(grantAmount.value)
|
||||||
|
if (!Number.isInteger(amount) || amount <= 0) {
|
||||||
|
grantError.value = '积分数量必须为正整数'
|
||||||
|
toast.error(grantError.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isGrantingPoints.value = true
|
||||||
|
try {
|
||||||
|
const token = getToken()
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/admin/points/grant`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, amount }),
|
||||||
|
})
|
||||||
|
let data = null
|
||||||
|
try {
|
||||||
|
data = await res.json()
|
||||||
|
} catch (e) {
|
||||||
|
// ignore body parse errors
|
||||||
|
}
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(`已为 ${username} 发放 ${amount} 积分`)
|
||||||
|
grantUsername.value = ''
|
||||||
|
grantAmount.value = ''
|
||||||
|
grantError.value = ''
|
||||||
|
} else {
|
||||||
|
const message = data?.error || '发放失败'
|
||||||
|
grantError.value = message
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
grantError.value = '发放失败,请稍后再试'
|
||||||
|
toast.error(grantError.value)
|
||||||
|
} finally {
|
||||||
|
isGrantingPoints.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
|
|
||||||
@@ -323,6 +405,51 @@ const save = async () => {
|
|||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grant-row {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-input {
|
||||||
|
flex: 1 1 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-input.amount {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-button {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-button.disabled,
|
||||||
|
.grant-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--primary-color-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-button:not(.disabled):hover {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-error-message {
|
||||||
|
color: red;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.switch-row {
|
.switch-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ const SANITIZE_CFG = {
|
|||||||
'th',
|
'th',
|
||||||
'video',
|
'video',
|
||||||
'source',
|
'source',
|
||||||
|
'iframe',
|
||||||
],
|
],
|
||||||
// 允许的属性
|
// 允许的属性
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
@@ -180,6 +181,16 @@ const SANITIZE_CFG = {
|
|||||||
'crossorigin',
|
'crossorigin',
|
||||||
],
|
],
|
||||||
source: ['src', 'type'],
|
source: ['src', 'type'],
|
||||||
|
iframe: [
|
||||||
|
'src',
|
||||||
|
'title',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'allow',
|
||||||
|
'allowfullscreen',
|
||||||
|
'frameborder',
|
||||||
|
'referrerpolicy',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
// 允许的类名(保留你的样式钩子)
|
// 允许的类名(保留你的样式钩子)
|
||||||
allowedClasses: {
|
allowedClasses: {
|
||||||
|
|||||||
@@ -51,10 +51,10 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- <dependency>-->
|
<dependency>
|
||||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
<groupId>org.springframework.boot</groupId>
|
||||||
<!-- <artifactId>spring-boot-starter-actuator</artifactId>-->
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
<!-- </dependency>-->
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public class SecurityConfig {
|
|||||||
"http://30.211.97.238",
|
"http://30.211.97.238",
|
||||||
"http://192.168.7.98",
|
"http://192.168.7.98",
|
||||||
"http://192.168.7.98:3000",
|
"http://192.168.7.98:3000",
|
||||||
|
"http://frontend_dev:3000",
|
||||||
|
"http://frontend_service:3000",
|
||||||
websiteUrl,
|
websiteUrl,
|
||||||
websiteUrl.replace("://www.", "://")
|
websiteUrl.replace("://www.", "://")
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
server.port=${SERVER_PORT:8082}
|
server.port=${WEBSOCKET_PORT:8082}
|
||||||
|
|
||||||
# 服务器配置
|
# 服务器配置
|
||||||
spring.application.name=websocket-service
|
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}
|
logging.level.org.springframework.web.socket=${WEBSOCKET_LOG_LEVEL:DEBUG}
|
||||||
|
|
||||||
# 网站 URL 配置
|
# 网站 URL 配置
|
||||||
app.website-url=${WEBSITE_URL:https://www.open-isle.com}
|
app.website-url=${WEBSITE_URL:https://www.open-isle.com}
|
||||||
|
|
||||||
|
management.endpoints.web.exposure.include=health,info
|
||||||
|
management.endpoint.health.probes.enabled=true
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
|
||||||
|
# 如需在独立环境中运行,可参考以下字段:
|
||||||
SERVER_PORT=<your-server-port>
|
SERVER_PORT=<your-server-port>
|
||||||
|
|
||||||
# RabbitMQ 配置
|
# RabbitMQ 配置
|
||||||
|
|||||||
Reference in New Issue
Block a user