feat():learning后台管理项目初始化

This commit is contained in:
yuj
2025-12-04 16:23:46 +08:00
parent 39886d50d2
commit 88e048f4d1
154 changed files with 28966 additions and 6 deletions

View File

@@ -0,0 +1,103 @@
# 邀请码申请 API - Python 实现
这是 `/api/invite-code/apply` 接口的 Python 单文件实现,与 Go 项目共用同一个 MySQL 数据库。
## 快速开始
### 1. 直接启动(默认使用 AWS RDS
```bash
# 使用启动脚本(推荐)
./scripts/run_invite_api.sh
# 或者直接运行
uv run scripts/invite_code_api.py
```
> 💡 **提示**: 脚本已预配置 AWS RDS 数据库连接,无需额外配置即可运行。
### 2. 使用本地数据库(可选)
如需连接本地 MySQL创建 `.env` 文件:
```bash
cp scripts/.env.example scripts/.env
vim scripts/.env # 修改为本地数据库配置
```
服务启动后:
- API 地址: `http://localhost:8000`
- 交互式文档: `http://localhost:8000/docs`
- 健康检查: `http://localhost:8000/health`
## API 使用示例
### 提交申请
```bash
curl -X POST http://localhost:8000/api/invite-code/apply \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"reason": "想要体验产品功能"
}'
```
**成功响应:**
```json
{
"code": 0,
"message": "申请已提交我们将在1-2个工作日内处理您的申请",
"data": {
"id": 1,
"email": "user@example.com",
"reason": "想要体验产品功能",
"status": "pending",
"created_at": "2025-11-03T10:00:00Z",
"updated_at": "2025-11-03T10:00:00Z"
}
}
```
**错误响应(重复申请):**
```json
{
"detail": "您已经提交过申请,请等待审核"
}
```
## 业务逻辑
1. **参数验证**: Email 必填且格式正确
2. **重复检查**:
- 如果已有 `pending` 状态申请 → 提示等待审核
- 如果已有 `approved` 状态申请 → 提示检查邮箱
- 只有 `rejected` 或无申请时才能提交
3. **数据存储**: 保存到 `admin_invite_code_applications`
## 技术栈
- **FastAPI**: 现代化 Python Web 框架
- **SQLAlchemy**: ORM 框架
- **PyMySQL**: MySQL 数据库驱动
- **Pydantic**: 数据验证
## 与 Go 项目的兼容性
- ✅ 使用相同的数据库表 `admin_invite_code_applications`
- ✅ 完全相同的业务逻辑
- ✅ 相同的请求/响应格式
- ✅ 相同的错误处理
## 开发说明
文件位置: `scripts/invite_code_api.py`(单文件,约 120 行代码)
支持的环境变量:
- `DB_USER`: 数据库用户名(默认: root
- `DB_PASSWORD`: 数据库密码(默认: password
- `DB_HOST`: 数据库地址(默认: localhost
- `DB_PORT`: 数据库端口(默认: 3306
- `DB_NAME`: 数据库名称(默认: goalfymax_prod

124
scripts/invite_code_api.py Normal file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
邀请码申请 API - 简洁版本
使用方法:
1. 直接运行: ./scripts/run_invite_api.sh
2. 或者: uv run scripts/invite_code_api.py
3. 访问: POST http://localhost:8000/api/invite-code/apply
默认使用 AWS RDS 数据库(与 Go 项目共享)
如需使用本地数据库,请设置环境变量或创建 scripts/.env 文件
"""
import os
from datetime import datetime, timezone
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.exc import IntegrityError
# 数据库配置 - 从环境变量读取(默认使用 AWS RDS 配置)
DB_USER = os.getenv("DB_USER", "goalfymax_prod")
DB_PASSWORD = os.getenv("DB_PASSWORD", "X6cQDaOLOifFBOMq")
DB_HOST = os.getenv("DB_HOST", "goalfyagent-aurora-mysql-staging.cb2sq6y2mg93.us-west-2.rds.amazonaws.com")
DB_PORT = os.getenv("DB_PORT", "3306")
DB_NAME = os.getenv("DB_NAME", "goalfymax_prod")
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
Base = declarative_base()
engine = create_engine(DATABASE_URL, echo=False, pool_pre_ping=True)
# 数据模型 - 与 Go 项目的 admin_invite_code_applications 表结构保持一致
class InviteCodeApplication(Base):
__tablename__ = "admin_invite_code_applications"
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String(255), nullable=False, index=True)
reason = Column(Text, nullable=True)
language = Column(String(10), nullable=False, default="zh")
status = Column(String(20), nullable=False, default="pending", index=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# 不自动创建表(使用 Go 项目已有的表结构)
# Base.metadata.create_all(engine)
# FastAPI 应用
app = FastAPI(title="邀请码申请 API")
# 请求/响应模型
class ApplicationRequest(BaseModel):
email: EmailStr
reason: Optional[str] = None
language: Optional[str] = "zh" # zh 或 en默认 zh
class ApplicationResponse(BaseModel):
id: int
email: str
reason: Optional[str]
language: str
status: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class SuccessResponse(BaseModel):
code: int = 0
message: str
data: ApplicationResponse
# API 端点
@app.post("/api/invite-code/apply", response_model=SuccessResponse)
async def submit_application(req: ApplicationRequest):
"""提交邀请码申请"""
with Session(engine) as session:
# 检查是否已有待处理或已通过的申请
existing = session.query(InviteCodeApplication).filter(
InviteCodeApplication.email == req.email,
InviteCodeApplication.status.in_(["pending", "approved"])
).first()
if existing:
if existing.status == "pending":
raise HTTPException(status_code=400, detail="您已经提交过申请,请等待审核")
if existing.status == "approved":
raise HTTPException(status_code=400, detail="您的申请已通过,请检查邮箱")
# 创建新申请
language = req.language if req.language in ["zh", "en"] else "zh"
application = InviteCodeApplication(
email=req.email,
reason=req.reason,
language=language,
status="pending"
)
try:
session.add(application)
session.commit()
session.refresh(application)
return SuccessResponse(
message="申请已提交我们将在1-2个工作日内处理您的申请",
data=ApplicationResponse.model_validate(application)
)
except IntegrityError:
session.rollback()
raise HTTPException(status_code=500, detail="创建申请失败")
# 健康检查
@app.get("/health")
async def health_check():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

50
scripts/migrate.sh Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# 设置错误时退出
set -e
# 设置颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}开始执行数据库迁移...${NC}"
# 加载环境变量
if [ -f ".env" ]; then
export $(cat .env | grep -v '^#' | xargs)
fi
# 检查数据库连接
echo -e "${YELLOW}检查数据库连接...${NC}"
mysql -h${DB_HOST} -P${DB_PORT} -u${DB_USER} -p${DB_PASSWORD} -e "SELECT 1" >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}数据库连接成功${NC}"
else
echo -e "${RED}数据库连接失败,请检查配置${NC}"
exit 1
fi
# 执行迁移文件
MIGRATION_DIR="migrations"
if [ -d "$MIGRATION_DIR" ]; then
echo -e "${YELLOW}执行迁移文件...${NC}"
for file in $(ls $MIGRATION_DIR/*.sql 2>/dev/null | sort); do
filename=$(basename "$file")
echo -e "${GREEN}执行: $filename${NC}"
mysql -h${DB_HOST} -P${DB_PORT} -u${DB_USER} -p${DB_PASSWORD} ${DB_NAME} < "$file"
if [ $? -eq 0 ]; then
echo -e "${GREEN} ✓ 成功${NC}"
else
echo -e "${RED} ✗ 失败${NC}"
exit 1
fi
done
echo -e "${GREEN}数据库迁移完成!${NC}"
else
echo -e "${YELLOW}没有找到迁移目录${NC}"
exit 1
fi

28
scripts/run_invite_api.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# 邀请码申请 API 启动脚本
set -e
cd "$(dirname "$0")/.."
echo "=== 邀请码申请 API 启动 ==="
# 加载环境变量(如果存在 .env 文件)
if [ -f scripts/.env ]; then
echo "✓ 加载自定义配置: scripts/.env"
export $(grep -v '^#' scripts/.env | xargs)
else
echo "✓ 使用默认配置: AWS RDS (goalfyagent-aurora-mysql-staging)"
echo " 提示: 如需使用本地数据库,请创建 scripts/.env 文件"
fi
# 检查依赖
echo "检查 Python 依赖..."
uv pip install -q fastapi uvicorn sqlalchemy pymysql pydantic[email]
# 启动服务
echo "启动服务..."
echo "访问地址: http://localhost:8000"
echo "API 文档: http://localhost:8000/docs"
echo ""
uv run scripts/invite_code_api.py

54
scripts/start.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# 设置错误时退出
set -e
# 设置颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}启动 Goalfymax Admin 服务...${NC}"
# 切换到项目根目录
cd "$(dirname "$0")/.."
# 检查是否已经有服务在运行
if [ -f "admin-server.pid" ]; then
PID=$(cat admin-server.pid)
if ps -p $PID > /dev/null 2>&1; then
echo -e "${YELLOW}服务已经在运行 (PID: $PID)${NC}"
echo -e "${YELLOW}如需重启,请先执行 ./scripts/stop.sh${NC}"
exit 1
else
# 删除过期的 PID 文件
rm -f admin-server.pid
fi
fi
# 创建日志目录
mkdir -p logs
# 启动服务
echo -e "${YELLOW}正在启动服务...${NC}"
nohup ./admin-server --config etc/config.yaml > logs/admin-server.log 2>&1 &
PID=$!
# 保存 PID
echo $PID > admin-server.pid
# 等待服务启动
sleep 2
# 检查服务是否启动成功
if ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}✓ 服务启动成功 (PID: $PID)${NC}"
echo -e "${GREEN}✓ 日志文件: logs/admin-server.log${NC}"
echo -e "${GREEN}✓ 服务端口: 8087${NC}"
else
echo -e "${RED}✗ 服务启动失败${NC}"
echo -e "${RED}请检查日志文件: logs/admin-server.log${NC}"
rm -f admin-server.pid
exit 1
fi

65
scripts/status.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# 设置颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
echo -e "${CYAN}=== Goalfymax Admin 服务状态 ===${NC}"
echo
# 检查进程状态
if [ -f "admin-server.pid" ]; then
PID=$(cat admin-server.pid)
if ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}✓ 服务运行中${NC}"
echo -e " 进程ID: $PID"
echo -e " 端口: 8087"
else
echo -e "${RED}✗ 服务未运行PID文件存在但进程不存在${NC}"
fi
else
echo -e "${RED}✗ 服务未运行${NC}"
fi
echo
# 测试健康检查
echo -e "${YELLOW}测试健康检查接口...${NC}"
HEALTH=$(curl -s http://localhost:8087/health 2>/dev/null)
if [ "$HEALTH" = '{"status":"ok"}' ]; then
echo -e "${GREEN}✓ 健康检查通过${NC}"
else
echo -e "${RED}✗ 健康检查失败${NC}"
fi
echo
# 检查数据库连接
echo -e "${YELLOW}检查最近的日志...${NC}"
if [ -f "logs/admin-server.log" ]; then
LAST_ERROR=$(tail -n 100 logs/admin-server.log | grep -i "error\|fatal" | tail -n 1)
if [ -n "$LAST_ERROR" ]; then
echo -e "${RED}✗ 发现错误:${NC}"
echo " $LAST_ERROR"
else
echo -e "${GREEN}✓ 无错误日志${NC}"
fi
else
echo -e "${YELLOW}! 日志文件不存在${NC}"
fi
echo
# 显示API端点
echo -e "${CYAN}可用的API端点:${NC}"
echo -e " 公开接口:"
echo -e " POST http://localhost:8087/api/public/invite-code/apply"
echo -e ""
echo -e " 管理接口(需要认证):"
echo -e " GET http://localhost:8087/api/admin/invite-applications"
echo -e " GET http://localhost:8087/api/admin/invite-applications/statistics"
echo -e " POST http://localhost:8087/api/admin/invite-applications/approve"
echo -e " POST http://localhost:8087/api/admin/invite-applications/reject"
echo
echo -e "${CYAN}=== 状态检查完成 ===${NC}"

56
scripts/stop.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# 设置颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}停止 Goalfymax Admin 服务...${NC}"
# 切换到项目根目录
cd "$(dirname "$0")/.."
# 检查 PID 文件是否存在
if [ ! -f "admin-server.pid" ]; then
echo -e "${RED}✗ 服务未运行 (找不到 PID 文件)${NC}"
exit 1
fi
# 读取 PID
PID=$(cat admin-server.pid)
# 检查进程是否存在
if ! ps -p $PID > /dev/null 2>&1; then
echo -e "${RED}✗ 服务未运行 (进程不存在)${NC}"
rm -f admin-server.pid
exit 1
fi
# 停止服务
echo -e "${YELLOW}正在停止服务 (PID: $PID)...${NC}"
kill $PID
# 等待进程结束
for i in {1..10}; do
if ! ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}✓ 服务已停止${NC}"
rm -f admin-server.pid
exit 0
fi
sleep 1
done
# 如果进程还没结束,强制终止
if ps -p $PID > /dev/null 2>&1; then
echo -e "${YELLOW}强制终止服务...${NC}"
kill -9 $PID
sleep 1
if ! ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}✓ 服务已强制停止${NC}"
rm -f admin-server.pid
else
echo -e "${RED}✗ 无法停止服务${NC}"
exit 1
fi
fi

49
scripts/test_api.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# 测试邀请码申请API
# 设置颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# API基础URL
API_BASE_URL="${API_BASE_URL:-http://localhost:8080}"
echo -e "${CYAN}=== 测试邀请码申请API ===${NC}"
echo
# 1. 测试提交申请(公开接口)
echo -e "${YELLOW}1. 测试提交邀请码申请(公开接口)${NC}"
curl -X POST "${API_BASE_URL}/api/public/invite-code/apply" \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"reason": "我想体验一下Goalfy的AI编程助手功能"
}'
echo -e "\n${GREEN}✓ 申请提交完成${NC}\n"
# 2. 测试重复提交(应该返回错误)
echo -e "${YELLOW}2. 测试重复提交(应该返回错误)${NC}"
curl -X POST "${API_BASE_URL}/api/public/invite-code/apply" \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"reason": "第二次申请"
}'
echo -e "\n${GREEN}✓ 重复提交测试完成${NC}\n"
# 3. 测试不同邮箱提交
echo -e "${YELLOW}3. 测试不同邮箱提交${NC}"
curl -X POST "${API_BASE_URL}/api/public/invite-code/apply" \
-H "Content-Type: application/json" \
-d '{
"email": "another@example.com",
"reason": "希望获得邀请码"
}'
echo -e "\n${GREEN}✓ 不同邮箱申请完成${NC}\n"
echo -e "${CYAN}=== API测试完成 ===${NC}"
echo -e "${GREEN}请登录后台管理页面查看待处理申请${NC}"

174
scripts/test_email.py Executable file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
邮件发送功能测试脚本
测试步骤:
1. 添加 language 字段到数据库
2. 创建中文和英文测试申请
3. 通过 Go 服务审批并发送邮件
"""
import os
import sys
import time
import pymysql
from datetime import datetime
# 数据库配置
DB_USER = os.getenv("DB_USER", "goalfymax_prod")
DB_PASSWORD = os.getenv("DB_PASSWORD", "X6cQDaOLOifFBOMq")
DB_HOST = os.getenv("DB_HOST", "goalfyagent-aurora-mysql-staging.cb2sq6y2mg93.us-west-2.rds.amazonaws.com")
DB_PORT = int(os.getenv("DB_PORT", "3306"))
DB_NAME = os.getenv("DB_NAME", "goalfymax_prod")
def get_db_connection():
"""获取数据库连接"""
return pymysql.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def add_language_column():
"""添加 language 字段"""
print("\n=== 步骤 1: 添加 language 字段 ===")
conn = get_db_connection()
try:
with conn.cursor() as cursor:
# 检查字段是否已存在
cursor.execute("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = %s
AND TABLE_NAME = 'admin_invite_code_applications'
AND COLUMN_NAME = 'language'
""", (DB_NAME,))
if cursor.fetchone():
print("✓ language 字段已存在")
else:
# 添加字段
cursor.execute("""
ALTER TABLE admin_invite_code_applications
ADD COLUMN language VARCHAR(10) DEFAULT 'zh' COMMENT '语言:zh-中文,en-英文' AFTER reason
""")
conn.commit()
print("✓ language 字段添加成功")
except Exception as e:
print(f"✗ 添加字段失败: {e}")
return False
finally:
conn.close()
return True
def create_test_applications():
"""创建测试申请"""
print("\n=== 步骤 2: 创建测试申请 ===")
conn = get_db_connection()
test_emails = [
("test_zh@example.com", "测试中文邮件", "zh"),
("test_en@example.com", "Testing English email", "en")
]
created_ids = []
try:
with conn.cursor() as cursor:
for email, reason, language in test_emails:
# 删除旧的测试数据
cursor.execute("""
DELETE FROM admin_invite_code_applications
WHERE email = %s
""", (email,))
# 创建新申请
cursor.execute("""
INSERT INTO admin_invite_code_applications
(email, reason, language, status, created_at, updated_at)
VALUES (%s, %s, %s, 'pending', NOW(), NOW())
""", (email, reason, language))
created_ids.append(cursor.lastrowid)
print(f"✓ 创建申请成功: {email} (ID: {cursor.lastrowid}, Language: {language})")
conn.commit()
except Exception as e:
print(f"✗ 创建申请失败: {e}")
return []
finally:
conn.close()
return created_ids
def check_applications():
"""检查申请状态"""
print("\n=== 查看测试申请 ===")
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("""
SELECT id, email, reason, language, status, created_at
FROM admin_invite_code_applications
WHERE email LIKE 'test_%@example.com'
ORDER BY id DESC
LIMIT 10
""")
applications = cursor.fetchall()
if applications:
print("\n当前测试申请:")
for app in applications:
print(f" ID: {app['id']}, Email: {app['email']}, Language: {app['language']}, Status: {app['status']}")
else:
print("没有找到测试申请")
return applications
finally:
conn.close()
def main():
print("=" * 60)
print("邮件发送功能测试")
print("=" * 60)
# 步骤 1: 添加 language 字段
if not add_language_column():
print("\n❌ 测试失败:无法添加 language 字段")
sys.exit(1)
# 步骤 2: 创建测试申请
application_ids = create_test_applications()
if not application_ids:
print("\n❌ 测试失败:无法创建测试申请")
sys.exit(1)
# 步骤 3: 显示测试申请
check_applications()
print("\n" + "=" * 60)
print("✓ 测试准备完成!")
print("=" * 60)
print("\n下一步操作:")
print("1. 启动 Go 服务: ./scripts/start.sh")
print("2. 登录后台管理系统")
print("3. 在邀请码申请管理中审批这些测试申请")
print("4. 检查邮件发送情况(中文和英文)")
print("\n测试邮箱:")
print(" - test_zh@example.com (中文)")
print(" - test_en@example.com (英文)")
print("\n或者使用 API 测试:")
for app_id in application_ids:
print(f" curl -X POST http://localhost:8087/api/admin/invite-code/applications/approve \\")
print(f" -H 'Content-Type: application/json' \\")
print(f" -d '{{\"application_id\": {app_id}, \"valid_days\": 7}}'")
if __name__ == "__main__":
main()