mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-21 19:47:28 +08:00
Compare commits
5 Commits
codex/add-
...
bb955c98ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb955c98ba | ||
|
|
a12368602d | ||
|
|
208c875868 | ||
|
|
39ae8c02cb | ||
|
|
0d7dc93a67 |
97
.env.example
Normal file
97
.env.example
Normal file
@@ -0,0 +1,97 @@
|
||||
# === 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=
|
||||
|
||||
# === Database Configuration ===
|
||||
MYSQL_DATABASE=openisle
|
||||
MYSQL_ROOT_PASSWORD=
|
||||
MYSQL_USER=
|
||||
MYSQL_PASSWORD=
|
||||
MYSQL_HOST=localhost
|
||||
|
||||
# === Redis Configuration ===
|
||||
REDIS_HOST=redis
|
||||
REDIS_DATABASE=0
|
||||
|
||||
# === RabbitMQ Configuration ===
|
||||
RABBITMQ_HOST=local
|
||||
RABBITMQ_USERNAME=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
|
||||
# === 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://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=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:
|
||||
workflow_dispatch:
|
||||
# schedule:
|
||||
# - cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
||||
schedule:
|
||||
- cron: "0 19 * * *" # 每天 UTC 19:00,相当于北京时间凌晨3点
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
|
||||
# 此文件保留作参考用途,如需在 Docker 之外手动配置,可按需复制。
|
||||
|
||||
# === Spring Boot ===
|
||||
SERVER_PORT=8080
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class AdminGrantPointRequest {
|
||||
|
||||
private String username;
|
||||
private int amount;
|
||||
}
|
||||
@@ -13,5 +13,4 @@ public enum PointHistoryType {
|
||||
REDEEM,
|
||||
LOTTERY_JOIN,
|
||||
LOTTERY_REWARD,
|
||||
ADMIN_GRANT,
|
||||
}
|
||||
|
||||
@@ -43,22 +43,6 @@ public class PointService {
|
||||
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) {
|
||||
int cost = post.getPointCost();
|
||||
if (cost > 0) {
|
||||
|
||||
@@ -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}:3306/${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:}
|
||||
|
||||
@@ -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。
|
||||
|
||||
1
docker/.gitignore
vendored
Normal file
1
docker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
data
|
||||
@@ -5,21 +5,26 @@ services:
|
||||
container_name: openisle-mysql
|
||||
restart: always
|
||||
env_file:
|
||||
- ../backend/open-isle.env
|
||||
- ./.env
|
||||
- ../.env
|
||||
ports:
|
||||
- "${MYSQL_PORT}:3306"
|
||||
- "${MYSQL_PORT:-3306}:3306"
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
- ../backend/src/main/resources/db/init:/docker-entrypoint-initdb.d
|
||||
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:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: opensearch.Dockerfile
|
||||
container_name: opensearch
|
||||
environment:
|
||||
- cluster.name=os-single
|
||||
@@ -39,6 +44,14 @@ services:
|
||||
- "${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
|
||||
@@ -51,7 +64,35 @@ services:
|
||||
depends_on:
|
||||
- opensearch
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- openisle-network
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.13-management
|
||||
container_name: openisle-rabbitmq
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../.env
|
||||
ports:
|
||||
- "${RABBITMQ_PORT:-5672}:5672"
|
||||
- "${RABBITMQ_MANAGEMENT_PORT:-15672}:15672"
|
||||
volumes:
|
||||
- rabbitmq-data:/var/lib/rabbitmq
|
||||
networks:
|
||||
- openisle-network
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: openisle-redis
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ../.env
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
networks:
|
||||
- openisle-network
|
||||
|
||||
# Java spring boot service
|
||||
springboot:
|
||||
@@ -59,21 +100,90 @@ services:
|
||||
container_name: openisle-springboot
|
||||
working_dir: /app
|
||||
env_file:
|
||||
- ../backend/open-isle.env
|
||||
- ./.env
|
||||
- ../.env
|
||||
environment:
|
||||
- MYSQL_URL=jdbc:mysql://mysql:${MYSQL_PORT}/${MYSQL_DATABASE}?useUnicode=yes&characterEncoding=UTF-8&useInformationSchema=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
|
||||
- MYSQL_HOST=mysql
|
||||
- OPENSEARCH_HOST=opensearch
|
||||
- RABBITMQ_HOST=rabbitmq
|
||||
ports:
|
||||
- "${SERVER_PORT}:8080"
|
||||
- "${SERVER_PORT:-8080}:${SERVER_PORT:-8080}"
|
||||
volumes:
|
||||
- ../backend:/app
|
||||
- maven-repo:/root/.m2
|
||||
depends_on:
|
||||
- mysql
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
rabbitmq:
|
||||
condition: service_started
|
||||
websocket-service:
|
||||
condition: service_started
|
||||
opensearch:
|
||||
condition: service_healthy
|
||||
command: mvn clean spring-boot:run -Dmaven.test.skip=true
|
||||
networks:
|
||||
- openisle-network
|
||||
|
||||
websocket-service:
|
||||
image: maven:3.9-eclipse-temurin-17
|
||||
container_name: openisle-websocket
|
||||
working_dir: /app
|
||||
env_file:
|
||||
- ../.env
|
||||
ports:
|
||||
- "${WEBSOCKET_PORT:-8082}:${WEBSOCKET_PORT:-8082}"
|
||||
volumes:
|
||||
- ../websocket_service:/app
|
||||
- websocket-maven-repo:/root/.m2
|
||||
depends_on:
|
||||
- rabbitmq
|
||||
command: mvn clean spring-boot:run -Dmaven.test.skip=true
|
||||
networks:
|
||||
- openisle-network
|
||||
|
||||
frontend_dev:
|
||||
image: node:20
|
||||
container_name: openisle-frontend-dev
|
||||
working_dir: /app
|
||||
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
|
||||
- websocket-service
|
||||
networks:
|
||||
- openisle-network
|
||||
profiles:
|
||||
- dev
|
||||
|
||||
frontend_service:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: frontend-service.Dockerfile
|
||||
container_name: openisle-frontend-service
|
||||
working_dir: /app
|
||||
env_file:
|
||||
- ../.env
|
||||
volumes:
|
||||
- ../frontend_nuxt:/app
|
||||
- frontend-service-node-modules:/app/node_modules
|
||||
- frontend-static:/var/www/openisle
|
||||
ports:
|
||||
- "${FRONTEND_SERVICE_PORT:-3001}:3000"
|
||||
depends_on:
|
||||
- springboot
|
||||
- websocket-service
|
||||
networks:
|
||||
- openisle-network
|
||||
profiles:
|
||||
- service
|
||||
|
||||
networks:
|
||||
openisle-network:
|
||||
driver: bridge
|
||||
@@ -81,3 +191,9 @@ networks:
|
||||
volumes:
|
||||
mysql-data:
|
||||
maven-repo:
|
||||
redis-data:
|
||||
rabbitmq-data:
|
||||
websocket-maven-repo:
|
||||
frontend-node-modules:
|
||||
frontend-service-node-modules:
|
||||
frontend-static:
|
||||
|
||||
62
docker/frontend-service-entrypoint.sh
Executable file
62
docker/frontend-service-entrypoint.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd /app
|
||||
|
||||
echo "👉 Building frontend (Nuxt SSR)..."
|
||||
|
||||
if [ -f .env.production.example ] && [ ! -f .env ]; then
|
||||
echo "📄 Copying .env.production.example to .env"
|
||||
cp .env.production.example .env
|
||||
fi
|
||||
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
echo "🧪 Smoke-testing: nuxt generate (artifacts will NOT be used)..."
|
||||
|
||||
SSR_OUTPUT_DIR=".output"
|
||||
SSR_OUTPUT_BAK=".output-ssr-backup-$$"
|
||||
GEN_FAIL_MSG="❌ Generate smoke test failed"
|
||||
|
||||
if [ ! -d "${SSR_OUTPUT_DIR}" ]; then
|
||||
echo "❌ 未发现 ${SSR_OUTPUT_DIR},请先确保 npm run build 成功执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv "${SSR_OUTPUT_DIR}" "${SSR_OUTPUT_BAK}"
|
||||
|
||||
restore_on_fail() {
|
||||
if [ -d ".output" ]; then
|
||||
mv .output ".output-generate-failed-$(date +%s)" || true
|
||||
fi
|
||||
mv "${SSR_OUTPUT_BAK}" "${SSR_OUTPUT_DIR}"
|
||||
}
|
||||
|
||||
trap 'restore_on_fail; echo "${GEN_FAIL_MSG}: unexpected error"; exit 1' ERR
|
||||
|
||||
NUXT_TELEMETRY_DISABLED=1 \
|
||||
NITRO_PRERENDER_FAIL_ON_ERROR=1 \
|
||||
npx nuxi generate --preset static
|
||||
|
||||
if [ ! -d ".output/public" ]; then
|
||||
restore_on_fail
|
||||
echo "${GEN_FAIL_MSG}: .output/public not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf ".output"
|
||||
mv "${SSR_OUTPUT_BAK}" "${SSR_OUTPUT_DIR}"
|
||||
trap - ERR
|
||||
echo "✅ Generate smoke test passed."
|
||||
|
||||
if [ -d ".output/public" ]; then
|
||||
mkdir -p /var/www/openisle
|
||||
rsync -a --delete .output/public/ /var/www/openisle/
|
||||
else
|
||||
echo "❌ 未发现 .output/public;检查 nuxt.config.ts/nitro preset"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 Starting Nuxt SSR server..."
|
||||
exec node .output/server/index.mjs
|
||||
12
docker/frontend-service.Dockerfile
Normal file
12
docker/frontend-service.Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:20
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends rsync \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY docker/frontend-service-entrypoint.sh /usr/local/bin/frontend-service-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/frontend-service-entrypoint.sh
|
||||
|
||||
CMD ["frontend-service-entrypoint.sh"]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -119,7 +119,7 @@ export default {
|
||||
|
||||
.cropper-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 10px;
|
||||
border-radius: 4px;
|
||||
color: var(--primary-color);
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
|
||||
.cropper-btn.primary {
|
||||
background: var(--primary-color);
|
||||
color: #ffff;
|
||||
color: var(--text-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,16 +71,6 @@ export default {
|
||||
label: '隐私政策',
|
||||
file: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/about/privacy.md',
|
||||
},
|
||||
{
|
||||
key: 'points',
|
||||
label: '积分说明',
|
||||
content: `# 积分说明
|
||||
|
||||
- 积分可用于兑换商品、参与抽奖等社区玩法。
|
||||
- 管理员可以通过后台新增的积分模块为用户发放奖励积分。
|
||||
- 每次发放都会记录在积分历史中,方便你查看积分来源。
|
||||
`,
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
label: 'API与调试',
|
||||
@@ -98,21 +88,11 @@ export default {
|
||||
return `${token.value.slice(0, 20)}...${token.value.slice(-10)}`
|
||||
})
|
||||
|
||||
const loadContent = async (tab) => {
|
||||
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
|
||||
}
|
||||
const loadContent = async (file) => {
|
||||
if (!file) return
|
||||
try {
|
||||
isFetching.value = true
|
||||
const res = await fetch(tab.file)
|
||||
const res = await fetch(file)
|
||||
if (res.ok) {
|
||||
content.value = await res.text()
|
||||
} else {
|
||||
@@ -130,15 +110,15 @@ export default {
|
||||
if (initTab && tabs.find((t) => t.key === initTab)) {
|
||||
selectedTab.value = initTab
|
||||
const tab = tabs.find((t) => t.key === initTab)
|
||||
if (tab) loadContent(tab)
|
||||
if (tab && tab.file) loadContent(tab.file)
|
||||
} else {
|
||||
loadContent(tabs[0])
|
||||
loadContent(tabs[0].file)
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedTab, (name) => {
|
||||
const tab = tabs.find((t) => t.key === name)
|
||||
if (tab) loadContent(tab)
|
||||
if (tab && tab.file) loadContent(tab.file)
|
||||
router.replace({ query: { ...route.query, tab: name } })
|
||||
})
|
||||
|
||||
@@ -147,8 +127,6 @@ export default {
|
||||
(name) => {
|
||||
if (name && name !== selectedTab.value && tabs.find((t) => t.key === name)) {
|
||||
selectedTab.value = name
|
||||
const tab = tabs.find((t) => t.key === name)
|
||||
if (tab) loadContent(tab)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -184,16 +184,6 @@
|
||||
}}</NuxtLink>
|
||||
参与,获得 {{ item.amount }} 积分
|
||||
</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>
|
||||
<paper-money-two /> 你目前的积分是 {{ item.balance }}
|
||||
</div>
|
||||
@@ -239,7 +229,6 @@ const pointRules = [
|
||||
'评论被点赞:每次 10 积分',
|
||||
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
||||
'文章被收录至精选:每次 500 积分',
|
||||
'管理员赠送:特殊活动可由管理员手动赠送积分',
|
||||
]
|
||||
|
||||
const goods = ref([])
|
||||
@@ -261,7 +250,6 @@ const iconMap = {
|
||||
LOTTERY_REWARD: 'fireworks',
|
||||
POST_LIKE_CANCELLED: 'clear-icon',
|
||||
COMMENT_LIKE_CANCELLED: 'clear-icon',
|
||||
ADMIN_GRANT: 'paper-money-two',
|
||||
}
|
||||
|
||||
const loadTrend = async () => {
|
||||
|
||||
@@ -65,35 +65,6 @@
|
||||
<div class="setting-title">注册模式</div>
|
||||
<Dropdown v-model="registerMode" :fetch-options="fetchRegisterModes" />
|
||||
</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 class="buttons">
|
||||
<div v-if="isSaving" class="save-button disabled">保存中...</div>
|
||||
@@ -131,10 +102,6 @@ const registerMode = ref('DIRECT')
|
||||
const isLoadingPage = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const frosted = ref(true)
|
||||
const grantUsername = ref('')
|
||||
const grantAmount = ref('')
|
||||
const grantError = ref('')
|
||||
const isGrantingPoints = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
isLoadingPage.value = true
|
||||
@@ -217,55 +184,6 @@ const loadAdminConfig = async () => {
|
||||
// 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 () => {
|
||||
isSaving.value = true
|
||||
|
||||
@@ -405,51 +323,6 @@ const save = async () => {
|
||||
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 {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# 所有环境变量已集中在仓库根目录的 .env.*.example 文件。
|
||||
# 如需在独立环境中运行,可参考以下字段:
|
||||
SERVER_PORT=<your-server-port>
|
||||
|
||||
# RabbitMQ 配置
|
||||
|
||||
Reference in New Issue
Block a user