first commit
This commit is contained in:
commit
bc94c75b54
31 files changed
+3483
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.14-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY ./app /app/app
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
Whitespace-only changes.
@@ -0,0 +1,25 @@
|
|||||||
|
from jose import JWTError, jwt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
|
||||||
|
SECRET_KEY = "your-secret-key-change-in-production" # 生产环境用环境变量
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: timedelta):
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
role: str = payload.get("role")
|
||||||
|
if username is None:
|
||||||
|
raise HTTPException(401, "无效令牌")
|
||||||
|
return {"username": username, "role": role}
|
||||||
|
except JWTError:
|
||||||
|
raise HTTPException(401, "令牌验证失败")
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,35 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from .database import engine, Base
|
||||||
|
from .routers import violations, debts, rewards, stats
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
app = FastAPI(title="自律公约执行系统")
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # 生产环境应限制域名
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(violations.router, prefix="/api/violations", tags=["violations"])
|
||||||
|
app.include_router(debts.router, prefix="/api/debts", tags=["debts"])
|
||||||
|
app.include_router(rewards.router, prefix="/api/rewards", tags=["rewards"])
|
||||||
|
app.include_router(stats.router, prefix="/api/stats", tags=["stats"])
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
def init_users():
|
||||||
|
db = next(get_db())
|
||||||
|
if not db.query(User).first():
|
||||||
|
db.add_all([
|
||||||
|
User(username="parent", hashed_password=pwd_context.hash("family2026"), role="parent"),
|
||||||
|
User(username="child", hashed_password=pwd_context.hash("self2026"), role="child")
|
||||||
|
])
|
||||||
|
db.commit()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from passlib.context import CryptContext
|
||||||
|
from sqlalchemy import Column, Integer, String, Date, Boolean, Float, DateTime
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class Violation(Base):
|
||||||
|
__tablename__ = "violations"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
date = Column(Date, nullable=False)
|
||||||
|
type = Column(String, nullable=False)
|
||||||
|
clause = Column(String)
|
||||||
|
sanction_level = Column(Integer) # 1/2/3
|
||||||
|
executed = Column(Boolean, default=False)
|
||||||
|
confirmed = Column(Boolean, default=False)
|
||||||
|
remark = Column(String) # 含屏幕时间
|
||||||
|
star_mark = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
class Debt(Base):
|
||||||
|
__tablename__ = "debts"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
date = Column(Date)
|
||||||
|
task_name = Column(String)
|
||||||
|
owed_amount = Column(Float)
|
||||||
|
interest_rate = Column(Float, default=1.5)
|
||||||
|
due_amount = Column(Float)
|
||||||
|
completed = Column(Boolean, default=False)
|
||||||
|
completion_date = Column(Date)
|
||||||
|
|
||||||
|
class Reward(Base):
|
||||||
|
__tablename__ = "rewards"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
type = Column(String) # 全勤/时长之星等
|
||||||
|
achieve_date = Column(Date)
|
||||||
|
content = Column(String)
|
||||||
|
exchange_date = Column(Date)
|
||||||
|
confirmed = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
class Parameter(Base):
|
||||||
|
__tablename__ = "parameters"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String, unique=True)
|
||||||
|
value = Column(String)
|
||||||
|
effective_date = Column(Date)
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
username = Column(String, unique=True, nullable=False)
|
||||||
|
hashed_password = Column(String, nullable=False)
|
||||||
|
role = Column(String, default="child") # parent / child
|
||||||
|
|
||||||
|
def verify_password(self, password: str) -> bool:
|
||||||
|
return pwd_context.verify(password, self.hashed_password)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ..database import get_db
|
||||||
|
from .. import models, auth
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/token")
|
||||||
|
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||||
|
user = db.query(models.User).filter(models.User.username == form_data.username).first()
|
||||||
|
if not user or not user.verify_password(form_data.password):
|
||||||
|
raise HTTPException(401, "用户名或密码错误")
|
||||||
|
access_token = auth.create_access_token(
|
||||||
|
data={"sub": user.username, "role": user.role},
|
||||||
|
expires_delta=timedelta(hours=24)
|
||||||
|
)
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,14 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ..database import get_db
|
||||||
|
from .. import crud, schemas
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/", response_model=schemas.ViolationOut)
|
||||||
|
def create_violation(violation: schemas.ViolationCreate, db: Session = Depends(get_db)):
|
||||||
|
return crud.create_violation(db, violation)
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[schemas.ViolationOut])
|
||||||
|
def list_violations(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||||
|
return crud.get_violations(db, skip=skip, limit=limit)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
sqlalchemy
|
||||||
|
pydantic
|
||||||
|
python-jose[cryptography]
|
||||||
|
passlib[bcrypt]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
container_name: convention-backend
|
||||||
|
ports:
|
||||||
|
- "44310:8000"
|
||||||
|
volumes:
|
||||||
|
- ./backend/data:/app/data # SQLite 文件持久化
|
||||||
|
environment:
|
||||||
|
- SECRET_KEY=your-long-random-string
|
||||||
|
- DATABASE_URL=sqlite:///./data/convention.db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
container_name: convention-frontend
|
||||||
|
ports:
|
||||||
|
- "4431:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
+1668
File diff suppressed because it is too large.
Load diff
+1342
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:24-alpine as builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#0 building with "desktop-linux" instance using docker driver
|
||||||
|
|
||||||
|
#1 [internal] load build definition from Dockerfile
|
||||||
|
#1 transferring dockerfile: 329B done
|
||||||
|
#1 WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
|
||||||
|
#1 DONE 0.0s
|
||||||
|
|
||||||
|
#2 [internal] load metadata for docker.io/library/nginx:alpine
|
||||||
|
#2 DONE 0.4s
|
||||||
|
|
||||||
|
#3 [internal] load metadata for docker.io/library/node:24-alpine
|
||||||
|
#3 DONE 0.4s
|
||||||
|
|
||||||
|
#4 [internal] load .dockerignore
|
||||||
|
#4 transferring context: 2B done
|
||||||
|
#4 DONE 0.0s
|
||||||
|
|
||||||
|
#5 [internal] load build context
|
||||||
|
#5 transferring context: 1.03kB done
|
||||||
|
#5 DONE 0.0s
|
||||||
|
|
||||||
|
#6 [stage-1 1/3] FROM docker.io/library/nginx:alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752
|
||||||
|
#6 resolve docker.io/library/nginx:alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752 0.1s done
|
||||||
|
#6 DONE 0.1s
|
||||||
|
|
||||||
|
#7 [builder 1/6] FROM docker.io/library/node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||||
|
#7 resolve docker.io/library/node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43
|
||||||
|
#7 resolve docker.io/library/node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 0.1s done
|
||||||
|
#7 DONE 0.1s
|
||||||
|
|
||||||
|
#6 [stage-1 1/3] FROM docker.io/library/nginx:alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752
|
||||||
|
#6 CACHED
|
||||||
|
|
||||||
|
#8 [builder 2/6] WORKDIR /app
|
||||||
|
#8 CACHED
|
||||||
|
|
||||||
|
#9 [builder 3/6] COPY package*.json ./
|
||||||
|
#9 DONE 0.1s
|
||||||
|
|
||||||
|
#10 [builder 4/6] RUN npm install --registry=https://registry.npmmirror.com
|
||||||
|
#10 0.488 npm error code EJSONPARSE
|
||||||
|
#10 0.489 npm error JSON.parse Invalid package.json: JSONParseError: Unexpected end of JSON input while parsing empty string
|
||||||
|
#10 0.489 npm error JSON.parse Failed to parse JSON data.
|
||||||
|
#10 0.489 npm error JSON.parse Note: package.json must be actual JSON, not just JavaScript.
|
||||||
|
#10 0.490 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-08-05T11_12_45_107Z-debug-0.log
|
||||||
|
#10 ERROR: process "/bin/sh -c npm install --registry=https://registry.npmmirror.com" did not complete successfully: exit code: 1
|
||||||
|
------
|
||||||
|
> [builder 4/6] RUN npm install --registry=https://registry.npmmirror.com:
|
||||||
|
0.488 npm error code EJSONPARSE
|
||||||
|
0.489 npm error JSON.parse Invalid package.json: JSONParseError: Unexpected end of JSON input while parsing empty string
|
||||||
|
0.489 npm error JSON.parse Failed to parse JSON data.
|
||||||
|
0.489 npm error JSON.parse Note: package.json must be actual JSON, not just JavaScript.
|
||||||
|
0.490 npm error A complete log of this run can be found in: /root/.npm/_logs/2026-08-05T11_12_45_107Z-debug-0.log
|
||||||
|
------
|
||||||
|
|
||||||
|
1 warning found (use docker --debug to expand):
|
||||||
|
- FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
|
||||||
|
Dockerfile:4
|
||||||
|
--------------------
|
||||||
|
2 | WORKDIR /app
|
||||||
|
3 | COPY package*.json ./
|
||||||
|
4 | >>> RUN npm install --registry=https://registry.npmmirror.com
|
||||||
|
5 | COPY . .
|
||||||
|
6 | RUN npm run build
|
||||||
|
--------------------
|
||||||
|
ERROR: failed to build: failed to solve: process "/bin/sh -c npm install --registry=https://registry.npmmirror.com" did not complete successfully: exit code: 1
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE || '/api',
|
||||||
|
})
|
||||||
|
|
||||||
|
apiClient.interceptors.request.use(config => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
error => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default apiClient
|
||||||
Whitespace-only changes.
@@ -0,0 +1,31 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
import Records from '../views/Records.vue'
|
||||||
|
import Stats from '../views/Stats.vue'
|
||||||
|
import Mine from '../views/Mine.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/login', component: Login, meta: { public: true } },
|
||||||
|
{ path: '/', component: Home, meta: { title: '首页' } },
|
||||||
|
{ path: '/records', component: Records, meta: { title: '记录' } },
|
||||||
|
{ path: '/stats', component: Stats, meta: { title: '统计' } },
|
||||||
|
{ path: '/mine', component: Mine, meta: { title: '我的' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const isPublic = to.matched.some(r => r.meta.public)
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (!isPublic && !token) {
|
||||||
|
next('/login')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<el-card class="stats-card">
|
||||||
|
<div class="weekly-count">本周违规:{{ weeklyCount }}</div>
|
||||||
|
<el-tag :type="sanctionColor">{{ sanctionLabel }}</el-tag>
|
||||||
|
</el-card>
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="8" v-for="action in quickActions" :key="action.text">
|
||||||
|
<div class="quick-action" @click="action.handler">
|
||||||
|
<span>{{ action.icon }}</span>
|
||||||
|
<span>{{ action.text }}</span>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const weeklyCount = ref(2)
|
||||||
|
|
||||||
|
const sanctionLabel = computed(() => {
|
||||||
|
if (weeklyCount.value >= 3) return '三级制裁'
|
||||||
|
if (weeklyCount.value >= 2) return '二级制裁'
|
||||||
|
return '一级制裁'
|
||||||
|
})
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
{ icon: '📝', text: '记录违规', handler: () => router.push('/records?action=violation') },
|
||||||
|
{ icon: '📋', text: '登记债务', handler: () => router.push('/records?action=debt') },
|
||||||
|
{ icon: '🎁', text: '兑换奖励', handler: () => router.push('/records?action=reward') },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>自律公约系统</h2>
|
||||||
|
<el-form :model="form" @submit.prevent="login">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="form.username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="form.password" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" @click="login" :loading="loading">登录</el-button>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const form = ref({ username: 'parent', password: 'family2026' })
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('username', form.value.username)
|
||||||
|
params.append('password', form.value.password)
|
||||||
|
const res = await axios.post('/api/auth/token', params)
|
||||||
|
const token = res.data.access_token
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
// 存储用户角色
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||||
|
localStorage.setItem('role', payload.role)
|
||||||
|
router.push('/')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('登录失败,请检查用户名和密码')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+67
@@ -0,0 +1,67 @@
|
|||||||
|
# ------------------------------------------------------------
|
||||||
|
# 自律公约网站反代配置
|
||||||
|
# 域名:nurture.ppuc.lssa.fun
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
server_name nurture.ppuc.lssa.fun;
|
||||||
|
|
||||||
|
# SSL 证书
|
||||||
|
ssl_certificate /etc/ssl/acme/ppuc.lssa.fun.fullchain.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/acme/ppuc.lssa.fun.key;
|
||||||
|
|
||||||
|
# TLS 安全配置
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# 基础安全头
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 1. 前端页面(Vue SPA)
|
||||||
|
# --------------------------------------------------------
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:4431/;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# WebSocket 支持(Vite HMR 或未来实时功能预留)
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 2. 后端 API
|
||||||
|
# --------------------------------------------------------
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:44310/;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# 传递用户真实 IP 给后端(如需日志)
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# HTTP → HTTPS 重定向
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name nurture.ppuc.lssa.fun;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user