feat: 增强IP地址记录功能以支持地理位置获取

- 在app.py中新增GeoIP2数据库支持,获取用户IP的地理位置信息
- 更新RawResponse模型,新增location、raw_response、sum_response和avg_response字段以存储更多用户响应数据
- 修改结果处理逻辑,保存用户的地理位置信息和响应数据
- 更新.gitignore以排除GeoLite2-City.mmdb文件
This commit is contained in:
Miu Li 2025-06-16 06:13:11 +08:00
parent e08909eacd
commit ee9e6a5cca
4 changed files with 50 additions and 18 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
scales/ scales/
psychoscales.db
__pycache__/ __pycache__/
psychoscales.db
GeoLite2-City.mmdb

52
app.py
View File

@ -10,11 +10,34 @@ from datetime import datetime
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from database import get_db, RawResponse from database import get_db, RawResponse
import geoip2.database
app = FastAPI() app = FastAPI()
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
# Initialize GeoIP2 reader
try:
geoip_reader = geoip2.database.Reader('GeoLite2-City.mmdb')
except FileNotFoundError:
print("Warning: GeoLite2-City.mmdb not found. IP location lookup will be disabled.")
geoip_reader = None
def get_location_from_ip(ip):
if not geoip_reader:
return None
try:
response = geoip_reader.city(ip)
return {
'country': response.country.name,
'city': response.city.name,
'latitude': response.location.latitude,
'longitude': response.location.longitude
}
except Exception as e:
print(f"Error getting location for IP {ip}: {e}")
return None
# 加载所有问卷数据 # 加载所有问卷数据
def load_all_scales(): def load_all_scales():
scales = {} scales = {}
@ -83,19 +106,6 @@ async def result(request: Request, scale_id: str, db: Session = Depends(get_db))
tags, scales = load_all_scales() tags, scales = load_all_scales()
scale = scales.get(scale_id) scale = scales.get(scale_id)
if scale: if scale:
# Save response to database
# Get real IP address considering proxy headers
ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or \
request.headers.get("X-Real-IP", "") or \
request.client.host
db_response = RawResponse(
scale_id=scale_id,
user_agent=request.headers.get("user-agent", "Unknown"),
ip_address=ip,
response=dict(form_data)
)
db.add(db_response)
db.commit()
responses = {} responses = {}
average = {} average = {}
options = {} options = {}
@ -110,6 +120,22 @@ async def result(request: Request, scale_id: str, db: Session = Depends(get_db))
else: else:
responses[subscale] += int(form_data[str(qid)]) responses[subscale] += int(form_data[str(qid)])
average[subscale] = round(responses[subscale]/len(qids),2) average[subscale] = round(responses[subscale]/len(qids),2)
# Save response to database
ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or \
request.headers.get("X-Real-IP", "") or \
request.client.host # Get real IP address considering proxy headers
location = get_location_from_ip(ip)# Get location information
db_response = RawResponse(
scale_id=scale_id,
user_agent=request.headers.get("user-agent", "Unknown"),
ip_address=ip,
location=json.dumps(location) if location else None,
raw_response=dict(form_data),
sum_response=responses,
avg_response=average
)
db.add(db_response)
db.commit()
return templates.TemplateResponse("result.html", { return templates.TemplateResponse("result.html", {
"request": request, "request": request,
"responses": responses, "responses": responses,

View File

@ -2,24 +2,29 @@ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime,
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime, UTC from datetime import datetime, UTC
import json
SQLALCHEMY_DATABASE_URL = "sqlite:///./psychoscales.db" SQLALCHEMY_DATABASE_URL = "sqlite:///./psychoscales.db"
engine = create_engine( engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False)
) )
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()
class RawResponse(Base): class RawResponse(Base):
__tablename__ = "responses_raw" __tablename__ = "responses"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
scale_id = Column(String, index=True) scale_id = Column(String, index=True)
user_agent = Column(String) user_agent = Column(String)
ip_address = Column(String) ip_address = Column(String)
response = Column(JSON) location = Column(String)
raw_response = Column(JSON)
sum_response = Column(JSON)
avg_response = Column(JSON)
created_at = Column(DateTime, default=datetime.now(UTC)) created_at = Column(DateTime, default=datetime.now(UTC))
# Create tables # Create tables

Binary file not shown.