Compare commits

...

8 Commits

Author SHA1 Message Date
shiran f1ce992d69 feat(server): 添加默认管理员自动创建功能
- 在 .env.example 中添加默认管理员相关配置项
- 在 docker-compose.yml 中添加默认管理员环境变量映射
- 在 server/app/core/config.py 中定义默认管理员配置
- 创建 server/app/db/bootstrap.py 文件实现默认管理员创建逻辑
- 在 server/app/main.py 的生命周期中集成默认管理员确保功能
- 更新 README.md 文档说明新的管理员配置方式

新配置项包括:DEFAULT_ADMIN_ENABLED、DEFAULT_ADMIN_PHONE、
DEFAULT_ADMIN_EMAIL、DEFAULT_ADMIN_PASSWORD、DEFAULT_ADMIN_NICKNAME
和 DEFAULT_ADMIN_SYNC_PASSWORD。
2026-05-09 19:00:02 +08:00
shiran 3f3acf834d feat(map): 添加地图定位功能的条件渲染控制
为了解决H5平台地图定位显示的问题,在index页面和pick-location页面中添加了
mapShowLocation响应式变量,并通过条件编译确保仅在非H5平台启用地图定位功能,
避免H5环境下出现定位相关的兼容性问题。
2026-05-09 18:49:42 +08:00
shiran a493d1bcf6 feat(server): 统一配置文件管理并增强环境变量配置
- 将服务器配置文件合并到根目录的 .env.example 中,移除 server/.env.example
- 为 PostgreSQL、Redis、MinIO/S3、Server、前端构建和Nginx 配置添加详细注释
- 新增 S3_REGION、S3_PUBLIC_URL 和 SENTRY_DSN 环境变量配置
- 修改配置加载逻辑以正确读取根目录下的 .env 文件
- 更新 docker-compose.yml 以包含新增的环境变量
2026-05-09 18:33:33 +08:00
shiran 0afd5bbb2c fix(api): 统一API端点路径格式
统一所有API端点路径末尾添加斜杠,确保与后端API规范一致,
包括收藏、通知、景点和用户相关接口。
2026-05-09 18:23:42 +08:00
shiran f2b4beed0a feat(config): 更新环境配置和Nginx设置以支持动态端口配置
- 修改 .env.example 文件中的客户端API基础路径配置
- 将Dockerfile中的nginx.conf复制到模板目录以支持环境变量
- 在nginx配置中使用环境变量替换硬编码端口
- 为API文档路径(/docs、/redoc、/openapi.json)添加代理配置
- 移除硬编码的服务器地址,改用相对路径配置
- 更新docker-compose.yml以传递内部端口环境变量
- 简化nginx反向代理配置,移除冗余的服务器块配置
2026-05-09 18:08:16 +08:00
shiran 9de0a56afa feat(config): 添加环境变量配置支持动态镜像和端口设置
- 在 .env.example 中添加 SERVER_BASE_IMAGE、SERVER_INTERNAL_PORT 等配置项
- 添加前端构建相关的 NODE_IMAGE 和 NGINX_IMAGE 配置
- 添加客户端 H5 和原生应用的 API 基础路径配置
- 支持动态端口配置以提高部署灵活性

feat(docker): 更新 Dockerfile 使用参数化镜像和端口配置

- 修改 server/Dockerfile 支持动态基础镜像和内部端口
- 更新 admin-web/Dockerfile 使用参数化镜像配置
- 修改 clients/Dockerfile 支持客户端多环境配置参数
- 所有 Dockerfile 现在使用 ARG 参数进行灵活配置

feat(nginx): 优化 Nginx 配置支持动态端口代理

- 更新 Nginx 配置文件使用环境变量定义监听端口
- 配置三个独立的服务端口分别处理客户端、管理后台和服务器API
- 添加完整的代理头信息设置以支持正确的请求转发
- 使用 Nginx 环境变量实现灵活的服务间通信

feat(deploy): 完善 docker-compose.yml 的环境变量集成

- 更新 docker-compose.yml 文件以使用新的环境变量配置
- 配置服务健康检查使用动态端口
- 设置 Nginx 容器环境变量以支持模板化配置
- 修复服务间通信端口使用环境变量替代硬编码值
2026-05-09 18:03:38 +08:00
shiran 19db342507 feat(server): 添加pip镜像源配置支持
添加清华PyPI镜像源配置选项到环境变量文件,包括
PIP_INDEX_URL、PIP_TRUSTED_HOST和PIP_DEFAULT_TIMEOUT参数,
用于加速Docker构建过程中的包安装速度。

在docker-compose.yml中将这些参数作为构建参数传递给
Docker容器,并在Dockerfile中接收并设置为环境变量,
同时移除pip install的--no-cache-dir参数以利用缓存。
2026-05-09 17:44:22 +08:00
shiran 3df8fcacbc feat(config): 添加镜像版本环境变量配置
为 Docker 镜像添加可配置的环境变量,包括 PostGIS、Redis、MinIO 和 Nginx,
使镜像版本可以通过 .env 文件进行自定义配置。

BREAKING CHANGE: 现在需要在 .env 文件中设置镜像版本变量。
2026-05-09 17:14:02 +08:00
21 changed files with 353 additions and 159 deletions
+92 -6
View File
@@ -1,35 +1,121 @@
# PostgreSQL
# PostgreSQL 数据库配置
# PostGIS 镜像地址,可改为私有镜像仓库地址。
POSTGRES_IMAGE=postgis/postgis:17-3.5
# PostgreSQL 用户名。
POSTGRES_USER=shiran
# PostgreSQL 密码,生产环境必须修改。
POSTGRES_PASSWORD=change-me-postgres-password
# PostgreSQL 数据库名。
POSTGRES_DB=ciyuan_viewfinder
# 容器内 PostgreSQL 数据目录。
POSTGRES_PGDATA=/var/lib/postgresql/data/pgdata
# Redis
# Redis 配置
# Redis 镜像地址,可改为私有镜像仓库地址。
REDIS_IMAGE=redis:7-alpine
# 后端连接 Redis 的地址,Docker Compose 内使用服务名 redis。
REDIS_URL=redis://redis:6379/0
# MinIO / S3
# MinIO / S3 对象存储配置
# MinIO 镜像地址,可改为私有镜像仓库地址。
MINIO_IMAGE=minio/minio:latest
# MinIO 管理员用户名。
MINIO_ROOT_USER=minioadmin
# MinIO 管理员密码,生产环境必须修改。
MINIO_ROOT_PASSWORD=change-me-minio-password
# S3 兼容服务地址,Docker Compose 内使用服务名 minio。
S3_ENDPOINT=http://minio:9000
# S3 访问密钥。
S3_ACCESS_KEY=minioadmin
# S3 访问密钥密码,通常与 MinIO 管理员密码保持一致。
S3_SECRET_KEY=change-me-minio-password
# S3 存储桶名称。
S3_BUCKET=ciyuan-viewfinder
# S3 区域,MinIO 可留空。
S3_REGION=
# S3 公网访问地址,留空时由后端按默认规则生成。
S3_PUBLIC_URL=
# Server
# 后端镜像与运行配置
# 后端 Dockerfile 基础镜像。
SERVER_BASE_IMAGE=python:3.12-slim
# 后端容器内部监听端口。
SERVER_INTERNAL_PORT=8000
# pip 包下载源,网络不稳定时可改为内网源。
PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
# pip 信任主机,通常与 PIP_INDEX_URL 的域名一致。
PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
# pip 下载超时时间,单位秒。
PIP_DEFAULT_TIMEOUT=120
# 后端异步数据库连接串,Docker Compose 内使用服务名 postgres。
DATABASE_URL=postgresql+asyncpg://shiran:change-me-postgres-password@postgres:5432/ciyuan_viewfinder
# 后端同步数据库连接串,用于迁移等同步场景。
DATABASE_URL_SYNC=postgresql://shiran:change-me-postgres-password@postgres:5432/ciyuan_viewfinder
# JWT 签名密钥,生产环境必须改成高强度随机字符串。
SECRET_KEY=change-me-before-production
# 访问令牌有效期,单位分钟。
ACCESS_TOKEN_EXPIRE_MINUTES=43200
# 刷新令牌有效期,单位天。
REFRESH_TOKEN_EXPIRE_DAYS=60
# 存储后端类型,可选 local 或 s3。
STORAGE_BACKEND=local
# 本地上传文件在后端容器内的保存路径。
LOCAL_STORAGE_PATH=/app/uploads
# 腾讯地图 API Key,没有地图相关能力时可留空。
TENCENT_MAP_KEY=
# Sentry DSN,留空表示不启用错误上报。
SENTRY_DSN=
# 后端日志级别。
LOG_LEVEL=INFO
# 是否输出 JSON 格式日志。
LOG_JSON=false
# 是否在后端启动时自动创建默认管理员。
DEFAULT_ADMIN_ENABLED=true
# 默认管理员手机号,可用于登录管理端。
DEFAULT_ADMIN_PHONE=13900000001
# 默认管理员邮箱,也可用于登录管理端。
DEFAULT_ADMIN_EMAIL=admin@ciyuan.local
# 默认管理员密码,生产环境必须修改。
DEFAULT_ADMIN_PASSWORD=admin123456
# 默认管理员昵称。
DEFAULT_ADMIN_NICKNAME=系统管理员
# 启动时是否把已有默认管理员密码同步为 DEFAULT_ADMIN_PASSWORD。
DEFAULT_ADMIN_SYNC_PASSWORD=true
# Frontend build
# 管理端前端构建配置
# 管理端构建阶段 Node 基础镜像。
ADMIN_WEB_NODE_IMAGE=node:20-alpine
# 管理端运行阶段 Nginx 基础镜像。
ADMIN_WEB_NGINX_IMAGE=nginx:1.27-alpine
# 管理端容器内部监听端口。
ADMIN_WEB_INTERNAL_PORT=80
# 管理端 API 基础路径,浏览器侧使用相对路径走当前域名反代。
VITE_API_BASE=/api/v1
# Nginx host ports
# 客户端前端构建配置
# 客户端构建阶段 Node 基础镜像。
CLIENTS_NODE_IMAGE=node:20-alpine
# 客户端运行阶段 Nginx 基础镜像。
CLIENTS_NGINX_IMAGE=nginx:1.27-alpine
# 客户端容器内部监听端口。
CLIENTS_INTERNAL_PORT=80
# H5 客户端 API 基础路径。
VITE_CLIENT_H5_API_BASE=/api/v1
# H5 客户端资源服务域名,留空表示使用相对路径。
VITE_CLIENT_H5_SERVER_ORIGIN=
# 非 H5 客户端 API 基础路径。
VITE_CLIENT_NATIVE_API_BASE=/api/v1
# 非 H5 客户端资源服务域名,留空表示使用相对路径。
VITE_CLIENT_NATIVE_SERVER_ORIGIN=
# 外层 Nginx 网关配置
# 外层 Nginx 镜像地址,可改为私有镜像仓库地址。
NGINX_IMAGE=nginx:1.27-alpine
# 外层 Nginx 客户端入口内部监听端口。
NGINX_CLIENT_INTERNAL_PORT=80
# 外层 Nginx 管理端入口内部监听端口。
NGINX_ADMIN_INTERNAL_PORT=81
# 客户端入口映射到宿主机的端口。
CLIENT_WEB_PORT=5173
# 管理端入口映射到宿主机的端口。
ADMIN_WEB_PORT=5174
+6 -3
View File
@@ -1,4 +1,7 @@
FROM node:20-alpine AS build
ARG ADMIN_WEB_NODE_IMAGE=node:20-alpine
ARG ADMIN_WEB_NGINX_IMAGE=nginx:1.27-alpine
FROM ${ADMIN_WEB_NODE_IMAGE} AS build
WORKDIR /app
@@ -11,9 +14,9 @@ RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
FROM ${ADMIN_WEB_NGINX_IMAGE}
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
+30 -3
View File
@@ -1,12 +1,12 @@
server {
listen 80;
listen ${ADMIN_WEB_INTERNAL_PORT};
server_name _;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://server:8000/api/;
proxy_pass http://server:${SERVER_INTERNAL_PORT}/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -15,11 +15,38 @@ server {
}
location /uploads/ {
proxy_pass http://server:8000/uploads/;
proxy_pass http://server:${SERVER_INTERNAL_PORT}/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /docs {
proxy_pass http://server:${SERVER_INTERNAL_PORT}/docs;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /redoc {
proxy_pass http://server:${SERVER_INTERNAL_PORT}/redoc;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /openapi.json {
proxy_pass http://server:${SERVER_INTERNAL_PORT}/openapi.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { clearAdminToken, getAdminToken, setAdminToken, setAdminUser } from './a
const API_BASE
= import.meta.env.VITE_API_BASE
|| (import.meta.env.DEV ? 'http://127.0.0.1:8000/api/v1' : '/api/v1')
|| '/api/v1'
type RequestError = Error & { status?: number }
+16 -3
View File
@@ -1,17 +1,30 @@
FROM node:20-alpine AS build
ARG CLIENTS_NODE_IMAGE=node:20-alpine
ARG CLIENTS_NGINX_IMAGE=nginx:1.27-alpine
FROM ${CLIENTS_NODE_IMAGE} AS build
WORKDIR /app
ENV UNI_INPUT_DIR=/app
ARG VITE_CLIENT_H5_API_BASE=/api/v1
ARG VITE_CLIENT_H5_SERVER_ORIGIN=
ARG VITE_CLIENT_NATIVE_API_BASE=http://10.0.10.11:8000/api/v1
ARG VITE_CLIENT_NATIVE_SERVER_ORIGIN=http://10.0.10.11:8000
ENV VITE_CLIENT_H5_API_BASE=${VITE_CLIENT_H5_API_BASE} \
VITE_CLIENT_H5_SERVER_ORIGIN=${VITE_CLIENT_H5_SERVER_ORIGIN} \
VITE_CLIENT_NATIVE_API_BASE=${VITE_CLIENT_NATIVE_API_BASE} \
VITE_CLIENT_NATIVE_SERVER_ORIGIN=${VITE_CLIENT_NATIVE_SERVER_ORIGIN}
COPY package.json package-lock.json* ./
RUN npm install --legacy-peer-deps
COPY . .
RUN npm run build:h5
FROM nginx:1.27-alpine
FROM ${CLIENTS_NGINX_IMAGE}
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=build /app/dist/build/h5 /usr/share/nginx/html
EXPOSE 80
+1 -1
View File
@@ -1,6 +1,6 @@
import { get, post, del } from "@/utils/request";
export const getFavorites = (params) => get("/favorites", params);
export const getFavorites = (params) => get("/favorites/", params);
export const addFavorite = (spotId) => post(`/favorites/${spotId}`);
+1 -1
View File
@@ -1,6 +1,6 @@
import { get, post } from "@/utils/request";
export const getNotifications = (params) => get("/notifications", params);
export const getNotifications = (params) => get("/notifications/", params);
export const getUnreadCount = () => get("/notifications/unread-count");
+2 -2
View File
@@ -1,13 +1,13 @@
import { get, post, put, del } from "@/utils/request";
import { API_BASE } from "@/utils/config";
export const getSpots = (params) => get("/spots", params);
export const getSpots = (params) => get("/spots/", params);
export const getNearbySpots = (params) => get("/spots/nearby", params);
export const getSpotDetail = (id) => get(`/spots/${id}`);
export const createSpot = (data) => post("/spots", data);
export const createSpot = (data) => post("/spots/", data);
export const updateSpot = (id, data) => put(`/spots/${id}`, data);
+1 -1
View File
@@ -11,4 +11,4 @@ export const changePassword = (data) => post("/users/me/change-password", data);
export const getUserInfo = (userId) => get(`/users/${userId}`);
export const getUserSpots = (userId, params) =>
get("/spots", { creator_id: userId, ...params });
get("/spots/", { creator_id: userId, ...params });
+30 -3
View File
@@ -1,12 +1,12 @@
server {
listen 80;
listen ${CLIENTS_INTERNAL_PORT};
server_name _;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://server:8000/api/;
proxy_pass http://server:${SERVER_INTERNAL_PORT}/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -15,11 +15,38 @@ server {
}
location /uploads/ {
proxy_pass http://server:8000/uploads/;
proxy_pass http://server:${SERVER_INTERNAL_PORT}/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /docs {
proxy_pass http://server:${SERVER_INTERNAL_PORT}/docs;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /redoc {
proxy_pass http://server:${SERVER_INTERNAL_PORT}/redoc;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /openapi.json {
proxy_pass http://server:${SERVER_INTERNAL_PORT}/openapi.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
+6 -1
View File
@@ -48,6 +48,11 @@ const selectedSpot = ref(null);
const locating = ref(false);
const mapCtx = ref(null);
const userLocation = ref(null);
const mapShowLocation = ref(false);
// #ifndef H5
mapShowLocation.value = true;
// #endif
const truncate = (str, len = 8) =>
str && str.length > len ? str.slice(0, len) + "…" : str;
@@ -390,7 +395,7 @@ onMounted(() => {
:longitude="mapCenter.longitude"
:markers="markers"
:scale="12"
:show-location="true"
:show-location="mapShowLocation"
:enable-zoom="true"
:enable-scroll="true"
@markertap="onMarkerTap"
+6 -1
View File
@@ -7,6 +7,11 @@ const longitude = ref(116.39747);
const address = ref("移动地图选择位置");
const currentCity = ref("");
const mapCtx = ref(null);
const mapShowLocation = ref(false);
// #ifndef H5
mapShowLocation.value = true;
// #endif
const keyword = ref("");
const searchResults = ref([]);
@@ -129,7 +134,7 @@ const confirmLocation = () => {
:latitude="latitude"
:longitude="longitude"
:scale="16"
show-location
:show-location="mapShowLocation"
@regionchange="onRegionChange"
/>
<view class="center-pin">
+5 -4
View File
@@ -1,11 +1,12 @@
// #ifdef H5
const API_BASE = "/api/v1";
const SERVER_ORIGIN = window.location.origin;
const API_BASE = import.meta.env.VITE_CLIENT_H5_API_BASE || "/api/v1";
const SERVER_ORIGIN = import.meta.env.VITE_CLIENT_H5_SERVER_ORIGIN || "";
// #endif
// #ifndef H5
const API_BASE = "http://10.0.10.11:8000/api/v1";
const SERVER_ORIGIN = "http://10.0.10.11:8000";
const API_BASE =
import.meta.env.VITE_CLIENT_NATIVE_API_BASE || "/api/v1";
const SERVER_ORIGIN = import.meta.env.VITE_CLIENT_NATIVE_SERVER_ORIGIN || "";
// #endif
export { API_BASE, SERVER_ORIGIN };
+47 -11
View File
@@ -2,7 +2,7 @@ version: "3.8"
services:
postgres:
image: postgis/postgis:17-3.5
image: "${POSTGRES_IMAGE}"
container_name: ciyuan-postgres
environment:
POSTGRES_USER: "${POSTGRES_USER}"
@@ -25,7 +25,7 @@ services:
- ciyuan-net
redis:
image: redis:7-alpine
image: "${REDIS_IMAGE}"
container_name: ciyuan-redis
expose:
- "6379"
@@ -41,7 +41,7 @@ services:
- ciyuan-net
minio:
image: minio/minio:latest
image: "${MINIO_IMAGE}"
container_name: ciyuan-minio
command: server /data --console-address ":9001"
environment:
@@ -59,8 +59,15 @@ services:
server:
build:
context: ./server
args:
SERVER_BASE_IMAGE: "${SERVER_BASE_IMAGE}"
PIP_INDEX_URL: "${PIP_INDEX_URL}"
PIP_TRUSTED_HOST: "${PIP_TRUSTED_HOST}"
PIP_DEFAULT_TIMEOUT: "${PIP_DEFAULT_TIMEOUT}"
SERVER_INTERNAL_PORT: "${SERVER_INTERNAL_PORT}"
container_name: ciyuan-server
environment:
SERVER_INTERNAL_PORT: "${SERVER_INTERNAL_PORT}"
DATABASE_URL: "${DATABASE_URL}"
DATABASE_URL_SYNC: "${DATABASE_URL_SYNC}"
REDIS_URL: "${REDIS_URL}"
@@ -73,11 +80,20 @@ services:
S3_ACCESS_KEY: "${S3_ACCESS_KEY}"
S3_SECRET_KEY: "${S3_SECRET_KEY}"
S3_BUCKET: "${S3_BUCKET}"
S3_REGION: "${S3_REGION}"
S3_PUBLIC_URL: "${S3_PUBLIC_URL}"
TENCENT_MAP_KEY: "${TENCENT_MAP_KEY}"
SENTRY_DSN: "${SENTRY_DSN}"
LOG_LEVEL: "${LOG_LEVEL}"
LOG_JSON: "${LOG_JSON}"
DEFAULT_ADMIN_ENABLED: "${DEFAULT_ADMIN_ENABLED}"
DEFAULT_ADMIN_PHONE: "${DEFAULT_ADMIN_PHONE}"
DEFAULT_ADMIN_EMAIL: "${DEFAULT_ADMIN_EMAIL}"
DEFAULT_ADMIN_PASSWORD: "${DEFAULT_ADMIN_PASSWORD}"
DEFAULT_ADMIN_NICKNAME: "${DEFAULT_ADMIN_NICKNAME}"
DEFAULT_ADMIN_SYNC_PASSWORD: "${DEFAULT_ADMIN_SYNC_PASSWORD}"
expose:
- "8000"
- "${SERVER_INTERNAL_PORT}"
volumes:
- server_uploads:/app/uploads
depends_on:
@@ -93,7 +109,7 @@ services:
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/', timeout=5)",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:${SERVER_INTERNAL_PORT}/', timeout=5)",
]
interval: 15s
timeout: 10s
@@ -107,10 +123,15 @@ services:
build:
context: ./admin-web
args:
ADMIN_WEB_NODE_IMAGE: "${ADMIN_WEB_NODE_IMAGE}"
ADMIN_WEB_NGINX_IMAGE: "${ADMIN_WEB_NGINX_IMAGE}"
VITE_API_BASE: "${VITE_API_BASE}"
container_name: ciyuan-admin-web
environment:
ADMIN_WEB_INTERNAL_PORT: "${ADMIN_WEB_INTERNAL_PORT}"
SERVER_INTERNAL_PORT: "${SERVER_INTERNAL_PORT}"
expose:
- "80"
- "${ADMIN_WEB_INTERNAL_PORT}"
depends_on:
server:
condition: service_healthy
@@ -121,9 +142,19 @@ services:
clients:
build:
context: ./clients
args:
CLIENTS_NODE_IMAGE: "${CLIENTS_NODE_IMAGE}"
CLIENTS_NGINX_IMAGE: "${CLIENTS_NGINX_IMAGE}"
VITE_CLIENT_H5_API_BASE: "${VITE_CLIENT_H5_API_BASE}"
VITE_CLIENT_H5_SERVER_ORIGIN: "${VITE_CLIENT_H5_SERVER_ORIGIN}"
VITE_CLIENT_NATIVE_API_BASE: "${VITE_CLIENT_NATIVE_API_BASE}"
VITE_CLIENT_NATIVE_SERVER_ORIGIN: "${VITE_CLIENT_NATIVE_SERVER_ORIGIN}"
container_name: ciyuan-clients
environment:
CLIENTS_INTERNAL_PORT: "${CLIENTS_INTERNAL_PORT}"
SERVER_INTERNAL_PORT: "${SERVER_INTERNAL_PORT}"
expose:
- "80"
- "${CLIENTS_INTERNAL_PORT}"
depends_on:
server:
condition: service_healthy
@@ -132,13 +163,18 @@ services:
- ciyuan-net
nginx:
image: nginx:1.27-alpine
image: "${NGINX_IMAGE}"
container_name: ciyuan-nginx
environment:
NGINX_CLIENT_INTERNAL_PORT: "${NGINX_CLIENT_INTERNAL_PORT}"
NGINX_ADMIN_INTERNAL_PORT: "${NGINX_ADMIN_INTERNAL_PORT}"
CLIENTS_INTERNAL_PORT: "${CLIENTS_INTERNAL_PORT}"
ADMIN_WEB_INTERNAL_PORT: "${ADMIN_WEB_INTERNAL_PORT}"
ports:
- "${CLIENT_WEB_PORT}:80"
- "${ADMIN_WEB_PORT}:81"
- "${CLIENT_WEB_PORT}:${NGINX_CLIENT_INTERNAL_PORT}"
- "${ADMIN_WEB_PORT}:${NGINX_ADMIN_INTERNAL_PORT}"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/default.conf:/etc/nginx/templates/default.conf.template:ro
depends_on:
server:
condition: service_healthy
+4 -88
View File
@@ -1,51 +1,9 @@
server {
listen 80;
listen ${NGINX_CLIENT_INTERNAL_PORT};
server_name _;
location /api/ {
proxy_pass http://server:8000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /uploads/ {
proxy_pass http://server:8000/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /docs {
proxy_pass http://server:8000/docs;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /redoc {
proxy_pass http://server:8000/redoc;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /openapi.json {
proxy_pass http://server:8000/openapi.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://clients:80;
proxy_pass http://clients:${CLIENTS_INTERNAL_PORT};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -55,53 +13,11 @@ server {
}
server {
listen 81;
listen ${NGINX_ADMIN_INTERNAL_PORT};
server_name _;
location /api/ {
proxy_pass http://server:8000/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /uploads/ {
proxy_pass http://server:8000/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /docs {
proxy_pass http://server:8000/docs;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /redoc {
proxy_pass http://server:8000/redoc;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /openapi.json {
proxy_pass http://server:8000/openapi.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://admin-web:80;
proxy_pass http://admin-web:${ADMIN_WEB_INTERNAL_PORT};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
-22
View File
@@ -1,22 +0,0 @@
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/ciyuan_viewfinder
DATABASE_URL_SYNC=postgresql://postgres:postgres@localhost:5432/ciyuan_viewfinder
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=change-me-to-a-random-secret-key
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7
# Storage: "local" or "s3" (s3 compatible with MinIO/OSS/COS)
STORAGE_BACKEND=local
LOCAL_STORAGE_PATH=./uploads
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=ciyuan-viewfinder
S3_REGION=
S3_PUBLIC_URL=
# Tencent Maps API Key (apply at https://lbs.qq.com/)
TENCENT_MAP_KEY=
SENTRY_DSN=
+15 -4
View File
@@ -1,16 +1,27 @@
FROM python:3.12-slim
ARG SERVER_BASE_IMAGE=python:3.12-slim
FROM ${SERVER_BASE_IMAGE}
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
ARG PIP_INDEX_URL
ARG PIP_TRUSTED_HOST
ARG PIP_DEFAULT_TIMEOUT=120
ARG SERVER_INTERNAL_PORT=8000
ENV PIP_INDEX_URL=${PIP_INDEX_URL} \
PIP_TRUSTED_HOST=${PIP_TRUSTED_HOST} \
PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT} \
SERVER_INTERNAL_PORT=${SERVER_INTERNAL_PORT}
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
EXPOSE ${SERVER_INTERNAL_PORT}
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${SERVER_INTERNAL_PORT}"]
+5 -3
View File
@@ -1,6 +1,8 @@
# 后端默认管理员账号
- 管理后台登录账号(手机号):`13900000001`
- 管理后台登录密码:`demo123456`
后端启动时会根据根目录 `.env` 自动确保默认管理员存在。
> 说明:以上为 `seed_demo_data.py` 初始化的演示管理员账号(role=`admin`)。
- 管理后台登录账号:`DEFAULT_ADMIN_PHONE``DEFAULT_ADMIN_EMAIL`
- 管理后台登录密码:`DEFAULT_ADMIN_PASSWORD`
默认模板值为 `13900000001 / admin123456`。生产环境请修改根目录 `.env`
+18 -1
View File
@@ -1,6 +1,16 @@
from pathlib import Path
from pydantic_settings import BaseSettings
_config_path = Path(__file__).resolve()
_env_file = (
_config_path.parents[3] / ".env"
if _config_path.parents[2].name == "server"
else _config_path.parents[2] / ".env"
)
class Settings(BaseSettings):
DATABASE_URL: str
DATABASE_URL_SYNC: str
@@ -24,8 +34,15 @@ class Settings(BaseSettings):
LOG_JSON: bool = False
LOG_LEVEL: str = "INFO"
DEFAULT_ADMIN_ENABLED: bool = True
DEFAULT_ADMIN_PHONE: str = "13900000001"
DEFAULT_ADMIN_EMAIL: str = "admin@ciyuan.local"
DEFAULT_ADMIN_PASSWORD: str = "admin123456"
DEFAULT_ADMIN_NICKNAME: str = "系统管理员"
DEFAULT_ADMIN_SYNC_PASSWORD: bool = True
class Config:
env_file = ".env"
env_file = _env_file
settings = Settings()
+65
View File
@@ -0,0 +1,65 @@
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import get_password_hash, verify_password
from app.db.session import sync_engine
from app.models.user import User
def ensure_default_admin() -> None:
if not settings.DEFAULT_ADMIN_ENABLED:
return
phone = settings.DEFAULT_ADMIN_PHONE.strip() or None
email = settings.DEFAULT_ADMIN_EMAIL.strip() or None
if not phone and not email:
raise RuntimeError("DEFAULT_ADMIN_PHONE or DEFAULT_ADMIN_EMAIL is required")
filters = []
if phone:
filters.append(User.phone == phone)
if email:
filters.append(User.email == email)
with Session(sync_engine) as session:
user = session.execute(select(User).where(or_(*filters))).scalar_one_or_none()
if user is None:
user = User(
phone=phone,
email=email,
password_hash=get_password_hash(settings.DEFAULT_ADMIN_PASSWORD),
nickname=settings.DEFAULT_ADMIN_NICKNAME,
identity="both",
role="admin",
is_active=True,
)
session.add(user)
session.commit()
return
changed = False
expected = {
"phone": phone,
"email": email,
"nickname": settings.DEFAULT_ADMIN_NICKNAME,
"identity": "both",
"role": "admin",
"is_active": True,
}
for field, value in expected.items():
if value is not None and getattr(user, field) != value:
setattr(user, field, value)
changed = True
if settings.DEFAULT_ADMIN_SYNC_PASSWORD and not verify_password(
settings.DEFAULT_ADMIN_PASSWORD,
user.password_hash,
):
user.password_hash = get_password_hash(settings.DEFAULT_ADMIN_PASSWORD)
changed = True
if changed:
session.add(user)
session.commit()
+2
View File
@@ -14,6 +14,7 @@ from app.api.v1.router import v1_router
from app.core.config import settings
from app.core.deps import get_current_active_user, get_db
from app.core.storage import init_storage
from app.db.bootstrap import ensure_default_admin
from app.db.migrations import run_startup_migrations
from app.models.spot import Spot
from app.models.tag import Tag
@@ -44,6 +45,7 @@ logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
await asyncio.to_thread(run_startup_migrations)
await asyncio.to_thread(ensure_default_admin)
app.state.redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
app.state.storage = init_storage()
yield