add
This commit is contained in:
commit
e2f60254c0
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
54
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
54
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,54 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<Languages>
|
||||
<language minSize="249" name="Python" />
|
||||
</Languages>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="13">
|
||||
<item index="0" class="java.lang.String" itemvalue="pygame" />
|
||||
<item index="1" class="java.lang.String" itemvalue="stable-baselines3" />
|
||||
<item index="2" class="java.lang.String" itemvalue="sb3-contrib" />
|
||||
<item index="3" class="java.lang.String" itemvalue="gym" />
|
||||
<item index="4" class="java.lang.String" itemvalue="tqdm" />
|
||||
<item index="5" class="java.lang.String" itemvalue="tiktoken" />
|
||||
<item index="6" class="java.lang.String" itemvalue="numba" />
|
||||
<item index="7" class="java.lang.String" itemvalue="more-itertools" />
|
||||
<item index="8" class="java.lang.String" itemvalue="mlx" />
|
||||
<item index="9" class="java.lang.String" itemvalue="torch" />
|
||||
<item index="10" class="java.lang.String" itemvalue="torchvision" />
|
||||
<item index="11" class="java.lang.String" itemvalue="torchaudio" />
|
||||
<item index="12" class="java.lang.String" itemvalue="llama-cpp-python" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="E501" />
|
||||
<option value="E302" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N803" />
|
||||
<option value="N806" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredIdentifiers">
|
||||
<list>
|
||||
<option value="bytes.*" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/table_game_project.iml" filepath="$PROJECT_DIR$/.idea/table_game_project.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
15
.idea/table_game_project.iml
generated
Normal file
15
.idea/table_game_project.iml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="table_game_project" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyNamespacePackagesService">
|
||||
<option name="namespacePackageFolders">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/frontend" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
54
backend/app/db.py
Normal file
54
backend/app/db.py
Normal file
@ -0,0 +1,54 @@
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
import configparser
|
||||
import hashlib
|
||||
|
||||
# 读取配置文件
|
||||
config = configparser.ConfigParser()
|
||||
config.read('backend/config.conf')
|
||||
|
||||
|
||||
def get_connection():
|
||||
try:
|
||||
connection = mysql.connector.connect(
|
||||
host=config['mysql']['host'],
|
||||
port=config['mysql']['port'],
|
||||
user=config['mysql']['user'],
|
||||
password=config['mysql']['password'],
|
||||
database=config['mysql']['database'],
|
||||
init_command="SET time_zone='+08:00'"
|
||||
)
|
||||
return connection
|
||||
except Error as e:
|
||||
print(f"Error connecting to MySQL: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def initialize_database():
|
||||
"""
|
||||
初始化数据库
|
||||
"""
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise Exception("Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT COUNT(*) AS user_count FROM users;")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result['user_count'] == 0:
|
||||
admin_password = hashlib.md5("admin".encode()).hexdigest()
|
||||
cursor.execute(
|
||||
"INSERT INTO users (username, password, user_type) VALUES (%s, %s, %s);",
|
||||
('admin', admin_password, 'admin')
|
||||
)
|
||||
connection.commit()
|
||||
print("Default admin user created: username=admin, password=admin")
|
||||
else:
|
||||
print("Users table already initialized.")
|
||||
except Error as e:
|
||||
print(f"Error during database initialization: {e}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
82
backend/app/main.py
Normal file
82
backend/app/main.py
Normal file
@ -0,0 +1,82 @@
|
||||
from fastapi import FastAPI
|
||||
from .db import initialize_database
|
||||
from .routers import admin_login
|
||||
from .routers import admin_user
|
||||
from .routers import admin_order
|
||||
from .routers import admin_game
|
||||
from .routers import admin_table
|
||||
from .routers import admin_group
|
||||
from .routers import user_auth
|
||||
from .routers import user_info
|
||||
from .routers import user_game
|
||||
from .routers import user_table
|
||||
from .routers import user_order
|
||||
from .routers import user_group
|
||||
from .routers import admin_announcement
|
||||
from .routers import user_announcement
|
||||
from .routers import admin_coupon
|
||||
from .routers import user_coupon
|
||||
from .routers import admin_message
|
||||
from .routers import user_messages
|
||||
from .routers import bell
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# 初始化数据库
|
||||
initialize_database()
|
||||
|
||||
# 注册路由
|
||||
app.include_router(admin_login.router, prefix="/admin", tags=["Admin"])
|
||||
# 管理员对用户的操作接口
|
||||
app.include_router(admin_user.router, prefix="/admin", tags=["Admin-User"])
|
||||
# 管理员订单管理路由
|
||||
app.include_router(admin_order.router, prefix="/admin", tags=["Admin-Order"])
|
||||
app.include_router(admin_game.router, prefix="/admin", tags=["Admin-Game"])
|
||||
app.include_router(admin_table.router, prefix="/admin", tags=["Admin-Table"]) # 添加在路由注册部分
|
||||
|
||||
app.include_router(admin_group.router, prefix="/admin", tags=["Admin-Group"])
|
||||
|
||||
app.include_router(admin_announcement.router, prefix="/admin/announcement", tags=["Admin-Announcement"])
|
||||
|
||||
app.include_router(admin_coupon.router, prefix="/admin", tags=["Admin-Coupon"])
|
||||
|
||||
app.include_router(admin_message.router, prefix="/admin", tags=["Admin-Message"])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.include_router(user_auth.router, prefix="/user", tags=["User Auth"])
|
||||
|
||||
app.include_router(user_info.router, prefix="/user", tags=["User Profile"])
|
||||
|
||||
app.include_router(user_game.router, prefix="/games", tags=["Games"])
|
||||
|
||||
app.include_router(user_table.router, prefix="/tables", tags=["Tables"])
|
||||
|
||||
app.include_router(user_order.router, prefix="/user/orders", tags=["User Orders"])
|
||||
|
||||
app.include_router(user_group.router, prefix="/user/groups", tags=["User Groups"])
|
||||
|
||||
app.include_router(user_announcement.router, prefix="/user/announcement", tags=["User Announcement"])
|
||||
|
||||
app.include_router(user_coupon.router, prefix="/user/coupons", tags=["User Coupon"])
|
||||
|
||||
app.include_router(user_messages.router, prefix="/user", tags=["User Messages"])
|
||||
|
||||
app.include_router(bell.router, prefix="/bell", tags=["Bell"])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# {
|
||||
# "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTczNjU3OTU3MX0.oJiCa7Mq56AfpfvYmL6v1WuvDGKhH8YfaIuNuqCFrGw",
|
||||
# "token_type": "bearer",
|
||||
# "expires_in": 86400
|
||||
# }
|
||||
6
backend/app/models/admin_user.py
Normal file
6
backend/app/models/admin_user.py
Normal file
@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AdminUser(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
user_type: str
|
||||
86
backend/app/routers/admin_announcement.py
Normal file
86
backend/app/routers/admin_announcement.py
Normal file
@ -0,0 +1,86 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class CreateAnnouncementRequest(BaseModel):
|
||||
token: str
|
||||
text: str
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
color: str = "#ffffff"
|
||||
|
||||
class DeleteAnnouncementRequest(BaseModel):
|
||||
token: str
|
||||
announcement_id: int
|
||||
|
||||
def _verify_admin_permission(token: str):
|
||||
"""公共权限验证方法"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user or admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
return username
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
@router.post("/create")
|
||||
def create_announcement(request: CreateAnnouncementRequest):
|
||||
_verify_admin_permission(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO announcements
|
||||
(text, start_time, end_time, color)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (request.text,
|
||||
request.start_time, request.end_time, request.color))
|
||||
conn.commit()
|
||||
return {"message": "公告创建成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
@router.post("/delete")
|
||||
def delete_announcement(request: DeleteAnnouncementRequest):
|
||||
_verify_admin_permission(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM announcements WHERE id = %s",
|
||||
(request.announcement_id,))
|
||||
conn.commit()
|
||||
return {"message": "公告删除成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
@router.get("/list")
|
||||
def get_all_announcements():
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT id, text, start_time, end_time, color, created_at
|
||||
FROM announcements
|
||||
ORDER BY created_at DESC
|
||||
""")
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
54
backend/app/routers/admin_coupon.py
Normal file
54
backend/app/routers/admin_coupon.py
Normal file
@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from ..services.admin_coupon_service import (
|
||||
create_coupon_service,
|
||||
get_coupons_service,
|
||||
issue_coupon_service,
|
||||
delete_coupon_service
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class CouponCreateRequest(BaseModel):
|
||||
token: str
|
||||
coupon_code: str
|
||||
discount_type: str
|
||||
discount_amount: float
|
||||
min_order_amount: float = None
|
||||
valid_from: str
|
||||
valid_to: str
|
||||
quantity: int
|
||||
|
||||
class CouponIssueRequest(BaseModel):
|
||||
token: str
|
||||
coupon_id: int
|
||||
user_id: int
|
||||
|
||||
@router.post("/coupons/create")
|
||||
def create_coupon(request: CouponCreateRequest):
|
||||
return create_coupon_service(
|
||||
token=request.token,
|
||||
coupon_code=request.coupon_code,
|
||||
discount_type=request.discount_type,
|
||||
discount_amount=request.discount_amount,
|
||||
min_order_amount=request.min_order_amount,
|
||||
valid_from=request.valid_from,
|
||||
valid_to=request.valid_to,
|
||||
quantity=request.quantity
|
||||
)
|
||||
|
||||
@router.get("/coupons")
|
||||
def get_coupons(token: str):
|
||||
return get_coupons_service(token)
|
||||
|
||||
@router.post("/coupons/issue")
|
||||
def issue_coupon(request: CouponIssueRequest):
|
||||
return issue_coupon_service(
|
||||
token=request.token,
|
||||
coupon_id=request.coupon_id,
|
||||
user_id=request.user_id
|
||||
)
|
||||
|
||||
@router.delete("/coupons/{coupon_id}")
|
||||
def delete_coupon(coupon_id: int, token: str):
|
||||
return delete_coupon_service(token, coupon_id)
|
||||
121
backend/app/routers/admin_game.py
Normal file
121
backend/app/routers/admin_game.py
Normal file
@ -0,0 +1,121 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Header
|
||||
from typing import List
|
||||
from ..schemas.game_info import GameListResponse, GameListRequest, GameCreate, GameCreateRequest, GamePhotoRequest, TagCreateRequest, GameTagLinkRequest, GameTagUnLinkRequest,GetGameTagsRequest, DeleteTagRequest
|
||||
from pydantic import BaseModel
|
||||
from ..services.admin_game_service import create_game, delete_game, update_game, list_games_service, set_the_game_photo, create_tag_service, link_tag_to_games_service, list_tags_service, get_game_tags_service, unlink_tag_from_game_service, get_games_by_tag_service, delete_tag_service
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from ..db import get_connection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/games/create")
|
||||
def create_game_api(request: GameCreateRequest):
|
||||
"""
|
||||
接口:创建游戏
|
||||
"""
|
||||
game = GameCreate(
|
||||
game_name=request.game_name,
|
||||
game_type=request.game_type,
|
||||
description=request.description,
|
||||
min_players=request.min_players,
|
||||
max_players=request.max_players,
|
||||
duration=request.duration,
|
||||
price=request.price,
|
||||
difficulty_level=request.difficulty_level,
|
||||
is_available=request.is_available,
|
||||
quantity=request.quantity,
|
||||
long_description=request.long_description
|
||||
)
|
||||
return create_game(token=request.token, game=game)
|
||||
|
||||
|
||||
@router.delete("/games/{game_id}")
|
||||
def delete_game_api(
|
||||
game_id: int,
|
||||
authorization: str = Header(..., alias="Authorization")
|
||||
):
|
||||
# 去掉 "Bearer " 前缀
|
||||
if authorization.startswith("Bearer "):
|
||||
token = authorization[7:]
|
||||
else:
|
||||
token = authorization
|
||||
return delete_game(token, game_id)
|
||||
|
||||
|
||||
@router.put("/games/{game_id}")
|
||||
def update_game_api(game_id: int, game: GameCreate, authorization: str = Header(..., alias="Authorization")):
|
||||
"""
|
||||
接口:更新游戏信息
|
||||
"""
|
||||
if authorization.startswith("Bearer "):
|
||||
token = authorization[7:]
|
||||
else:
|
||||
token = authorization
|
||||
return update_game(token, game_id, game)
|
||||
|
||||
|
||||
@router.post("/games", response_model=List[GameListResponse])
|
||||
def list_games(request: GameListRequest):
|
||||
"""
|
||||
路由层:通过 token 校验后,获取游戏列表
|
||||
"""
|
||||
return list_games_service(token=request.token)
|
||||
|
||||
|
||||
@router.get("/games/{game_id}")
|
||||
def get_game(game_id: int):
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM games WHERE game_id = %s", (game_id,))
|
||||
game = cursor.fetchone()
|
||||
if not game:
|
||||
raise HTTPException(404, detail="Game not found")
|
||||
return game
|
||||
|
||||
|
||||
@router.post("/games/set_photo")
|
||||
def set_game_photo(request: GamePhotoRequest):
|
||||
token = request.token
|
||||
game_id = request.game_id
|
||||
photo_url = request.photo_url
|
||||
return set_the_game_photo(token, photo_url, game_id)
|
||||
|
||||
|
||||
@router.post("/get_tags")
|
||||
def get_tags_api(request: GameListRequest): # 复用已有请求模型
|
||||
return list_tags_service(token=request.token)
|
||||
|
||||
@router.post("/tags/link_games")
|
||||
def link_tag_to_games(request: GameTagLinkRequest):
|
||||
"""批量关联标签与游戏"""
|
||||
return link_tag_to_games_service(
|
||||
token=request.token,
|
||||
tag_id=request.tag_id,
|
||||
game_ids=request.game_ids
|
||||
)
|
||||
|
||||
@router.post("/tags")
|
||||
def create_tag(request: TagCreateRequest):
|
||||
"""创建新标签"""
|
||||
return create_tag_service(token=request.token, tag_name=request.tag_name)
|
||||
|
||||
|
||||
@router.post("/get_game_tags")
|
||||
def get_game_tags_api(request: GetGameTagsRequest): # 需要新请求模型
|
||||
return get_game_tags_service(token=request.token, game_id=request.game_id)
|
||||
|
||||
@router.post("/tags/unlink")
|
||||
def unlink_tag_from_game(request: GameTagUnLinkRequest):
|
||||
return unlink_tag_from_game_service(
|
||||
token=request.token,
|
||||
tag_id=request.tag_id,
|
||||
game_id=request.game_id
|
||||
)
|
||||
|
||||
@router.post("/get_games_by_tag")
|
||||
def get_games_by_tag_api(request: GetGameTagsRequest): # 需要新请求模型
|
||||
return get_games_by_tag_service(token=request.token, tag_id=request.tag_id)
|
||||
@router.post("/tags/delete")
|
||||
def delete_tag(request: DeleteTagRequest):
|
||||
return delete_tag_service(token=request.token, tag_id=request.tag_id)
|
||||
23
backend/app/routers/admin_group.py
Normal file
23
backend/app/routers/admin_group.py
Normal file
@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from ..services.admin_group_service import list_groups_service, delete_group_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class GroupListRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
class GroupDeleteRequest(BaseModel):
|
||||
token: str
|
||||
group_id: int
|
||||
|
||||
@router.post("/groups/list")
|
||||
def list_groups(request: GroupListRequest):
|
||||
return list_groups_service(token=request.token)
|
||||
|
||||
@router.post("/groups/delete")
|
||||
def delete_group(request: GroupDeleteRequest):
|
||||
return delete_group_service(
|
||||
token=request.token,
|
||||
group_id=request.group_id
|
||||
)
|
||||
18
backend/app/routers/admin_login.py
Normal file
18
backend/app/routers/admin_login.py
Normal file
@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from ..schemas.admin_auth import LoginRequest, TokenResponse
|
||||
from ..services.auth_service import authenticate_admin, generate_login_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login/", response_model=TokenResponse)
|
||||
def login(request: LoginRequest, remember_me: bool = False):
|
||||
"""
|
||||
管理员登录接口
|
||||
"""
|
||||
user = authenticate_admin(request.username, request.password)
|
||||
token, expires_in = generate_login_token(user["username"], remember_me)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": expires_in
|
||||
}
|
||||
90
backend/app/routers/admin_message.py
Normal file
90
backend/app/routers/admin_message.py
Normal file
@ -0,0 +1,90 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from ..db import get_connection
|
||||
from pydantic import BaseModel
|
||||
from fastapi import Body
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def _verify_admin_permission(token: str):
|
||||
"""公共权限验证方法"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user or admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
return username
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
class MessageGet(BaseModel):
|
||||
token: str
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
class DeleteMessageRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
@router.post("/messages")
|
||||
def get_all_messages(request: MessageGet):
|
||||
"""获取留言列表(带分页)"""
|
||||
_verify_admin_permission(request.token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
offset = (request.page - 1) * request.page_size
|
||||
# 获取总数
|
||||
cursor.execute("SELECT COUNT(*) AS total FROM player_messages")
|
||||
total = cursor.fetchone()['total']
|
||||
|
||||
# 获取分页数据
|
||||
cursor.execute("""
|
||||
SELECT m.message_id, m.user_id, u.username, m.message_content, m.created_at
|
||||
FROM player_messages m
|
||||
JOIN users u ON m.user_id = u.user_id
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", (request.page_size, offset))
|
||||
|
||||
return {
|
||||
"data": cursor.fetchall(),
|
||||
"total": total,
|
||||
"page": request.page,
|
||||
"page_size": request.page_size
|
||||
}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
@router.delete("/messages/{message_id}")
|
||||
async def delete_message(
|
||||
message_id: int,
|
||||
request: DeleteMessageRequest
|
||||
):
|
||||
"""删除留言核心逻辑"""
|
||||
|
||||
# 数据库操作
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
affected_rows = cursor.execute(
|
||||
"DELETE FROM player_messages WHERE message_id = %s",
|
||||
(message_id,)
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
if affected_rows == 0:
|
||||
raise HTTPException(status_code=404, detail="留言不存在")
|
||||
return {"message": "删除成功"}
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
58
backend/app/routers/admin_order.py
Normal file
58
backend/app/routers/admin_order.py
Normal file
@ -0,0 +1,58 @@
|
||||
from fastapi import APIRouter, HTTPException, Body, Depends
|
||||
from typing import List
|
||||
from ..schemas.order_info import OrderRangeQueryRequest, OrderCompleteRequest, OrderSettleRequest, OrderDetailRequest, OrderDetailResponse
|
||||
from ..schemas.order_info import OrderInfo
|
||||
from ..schemas.order_info import OrderListResponse
|
||||
from ..services.admin_order_service import query_orders_by_status_and_range, complete_order, settle_order, get_order_details_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/orders/query", response_model=OrderListResponse)
|
||||
def query_orders(request: OrderRangeQueryRequest):
|
||||
"""
|
||||
管理员查询符合订单状态并限制范围的订单信息
|
||||
"""
|
||||
orders = query_orders_by_status_and_range(
|
||||
token=request.token,
|
||||
start=request.start,
|
||||
end=request.end,
|
||||
order_status=request.order_status
|
||||
)
|
||||
return orders
|
||||
|
||||
|
||||
# 修正路由定义
|
||||
@router.put("/orders/{order_id}/complete")
|
||||
async def complete_order_api(
|
||||
order_id: int,
|
||||
request: OrderCompleteRequest = Body(...) # 使用正确的请求模型
|
||||
):
|
||||
return complete_order(
|
||||
token=request.token,
|
||||
order_id=order_id,
|
||||
end_datetime=request.end_datetime
|
||||
)
|
||||
|
||||
|
||||
@router.put("/orders/{order_id}/settle")
|
||||
async def settle_order_endpoint(
|
||||
order_id: int,
|
||||
request: OrderSettleRequest = Body(...)
|
||||
):
|
||||
return settle_order(
|
||||
token=request.token,
|
||||
order_id=order_id,
|
||||
used_points=request.used_points,
|
||||
coupon_id=request.coupon_id
|
||||
)
|
||||
|
||||
|
||||
@router.post("/orders/details", response_model=OrderDetailResponse)
|
||||
def get_order_details(
|
||||
request: OrderDetailRequest
|
||||
):
|
||||
"""
|
||||
获取订单详细信息
|
||||
"""
|
||||
return get_order_details_service(token=request.token, order_id=request.order_id)
|
||||
|
||||
39
backend/app/routers/admin_table.py
Normal file
39
backend/app/routers/admin_table.py
Normal file
@ -0,0 +1,39 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from typing import List
|
||||
from ..schemas.table_info import TableCreateRequest, TableResponse
|
||||
from ..services.admin_table_service import (
|
||||
create_table,
|
||||
delete_table,
|
||||
update_table,
|
||||
list_tables_service
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_token(authorization: str = Header(...)):
|
||||
if authorization.startswith("Bearer "):
|
||||
return authorization[7:]
|
||||
return authorization
|
||||
|
||||
@router.post("/tables/create", response_model=dict)
|
||||
def create_table_api(request: TableCreateRequest):
|
||||
return create_table(
|
||||
token=request.token,
|
||||
table_data=request.dict(exclude={"token"})
|
||||
)
|
||||
|
||||
@router.delete("/tables/{table_id}", response_model=dict)
|
||||
def delete_table_api(table_id: int, token: str = Depends(get_token)):
|
||||
return delete_table(token, table_id)
|
||||
|
||||
@router.put("/tables/{table_id}", response_model=dict)
|
||||
def update_table_api(table_id: int, request: TableCreateRequest, token: str = Depends(get_token)):
|
||||
return update_table(
|
||||
token=token,
|
||||
table_id=table_id,
|
||||
table_data=request.dict(exclude={"token"})
|
||||
)
|
||||
|
||||
@router.get("/tables", response_model=List[TableResponse])
|
||||
def list_tables_api(token: str = Depends(get_token)):
|
||||
return list_tables_service(token)
|
||||
109
backend/app/routers/admin_user.py
Normal file
109
backend/app/routers/admin_user.py
Normal file
@ -0,0 +1,109 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import List
|
||||
from ..schemas.user_info import (
|
||||
UserRangeQueryRequest,
|
||||
UserInfo,
|
||||
UserDeleteRequest,
|
||||
UserUpdateRequest,
|
||||
TotalNumberOfUsers,
|
||||
UserQueryRequest,
|
||||
UserPasswordUpdateRequest,
|
||||
UserPointsUpdateRequest
|
||||
)
|
||||
from ..services.admin_user_service import (
|
||||
query_user,
|
||||
query_users_by_range,
|
||||
delete_user_by_token,
|
||||
update_user,
|
||||
get_total_of_users,
|
||||
update_user_password,
|
||||
update_user_points
|
||||
)
|
||||
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/users/query", response_model=List[UserInfo])
|
||||
def query_users(request: UserRangeQueryRequest):
|
||||
"""
|
||||
管理员查询范围内的用户信息
|
||||
"""
|
||||
users = query_users_by_range(
|
||||
token=request.token,
|
||||
start=request.start,
|
||||
end=request.end
|
||||
)
|
||||
return users
|
||||
|
||||
@router.post("/users/del")
|
||||
def delete_user(request: UserDeleteRequest):
|
||||
"""
|
||||
管理员删除用户
|
||||
"""
|
||||
result = delete_user_by_token(token=request.token, uid=request.uid)
|
||||
return result
|
||||
|
||||
@router.post("/users/update")
|
||||
def update_user_route(request: UserUpdateRequest):
|
||||
"""
|
||||
管理员修改用户信息
|
||||
"""
|
||||
result = update_user(
|
||||
token=request.token,
|
||||
uid=request.uid,
|
||||
username=request.username,
|
||||
email=request.email,
|
||||
phone_number=request.phone_number,
|
||||
gender=request.gender,
|
||||
user_type=request.user_type
|
||||
)
|
||||
return {"message": result}
|
||||
|
||||
@router.post("/users/sum")
|
||||
def get_total_users(request: TotalNumberOfUsers):
|
||||
"""
|
||||
获取用户总数
|
||||
"""
|
||||
result = get_total_of_users(
|
||||
token=request.token
|
||||
)
|
||||
return {"message": result}
|
||||
|
||||
@router.post("/users/search")
|
||||
def query_user_api(request: UserQueryRequest):
|
||||
"""
|
||||
管理员通过 `phone_number` / `email` / `username` / `uid` 查询用户信息
|
||||
"""
|
||||
result = query_user(
|
||||
token=request.token,
|
||||
query_mode=request.query_mode,
|
||||
query_value=request.query_value
|
||||
)
|
||||
return result
|
||||
|
||||
@router.post("/users/update_password")
|
||||
def update_user_password_route(request: UserPasswordUpdateRequest):
|
||||
"""
|
||||
管理员修改用户密码
|
||||
"""
|
||||
result = update_user_password(
|
||||
token=request.token,
|
||||
uid=request.uid,
|
||||
new_password=request.new_password
|
||||
)
|
||||
return {"message": result}
|
||||
|
||||
@router.post("/users/update_points")
|
||||
def update_points_route(request: UserPointsUpdateRequest):
|
||||
"""
|
||||
管理员调整用户积分
|
||||
"""
|
||||
result = update_user_points(
|
||||
token=request.token,
|
||||
uid=request.uid,
|
||||
points=request.points,
|
||||
reason=request.reason
|
||||
)
|
||||
return {"message": result}
|
||||
|
||||
36
backend/app/routers/bell.py
Normal file
36
backend/app/routers/bell.py
Normal file
@ -0,0 +1,36 @@
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, APIRouter
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 存储 Flask 前端的 WebSocket 连接
|
||||
flask_clients: List[WebSocket] = []
|
||||
|
||||
|
||||
@router.websocket("/ws/admin")
|
||||
async def admin_websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
flask_clients.append(websocket)
|
||||
try:
|
||||
while True:
|
||||
# 保持连接开放
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
flask_clients.remove(websocket)
|
||||
|
||||
class CallRequest(BaseModel):
|
||||
table_name: str
|
||||
|
||||
@router.post("/call")
|
||||
async def receive_call(request: CallRequest):
|
||||
table_name = request.table_name
|
||||
# 这里简单假设桌号就是桌子名称,实际中可按需转换
|
||||
table_number = table_name
|
||||
for client in flask_clients:
|
||||
try:
|
||||
await client.send_text(table_number)
|
||||
except Exception as e:
|
||||
print(f"Error sending message to client: {e}")
|
||||
return {"message": "Call received and forwarded"}
|
||||
|
||||
24
backend/app/routers/user_announcement.py
Normal file
24
backend/app/routers/user_announcement.py
Normal file
@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from ..db import get_connection
|
||||
|
||||
router = APIRouter()
|
||||
from datetime import datetime
|
||||
|
||||
# 在文件末尾添加新接口
|
||||
@router.get("/active_announcements")
|
||||
def get_active_announcements():
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT text, color
|
||||
FROM announcements
|
||||
WHERE start_time <= %s AND end_time >= %s
|
||||
ORDER BY created_at DESC
|
||||
""", (datetime.now(), datetime.now()))
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
120
backend/app/routers/user_auth.py
Normal file
120
backend/app/routers/user_auth.py
Normal file
@ -0,0 +1,120 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from ..schemas.user_auth import (
|
||||
UserLoginRequest,
|
||||
CodeLoginRequest,
|
||||
ResetPasswordRequest,
|
||||
SendCodeRequest,
|
||||
RegisterRequest,
|
||||
CheckUserExistenceRequest,
|
||||
WechatBindRequest
|
||||
)
|
||||
from ..schemas.admin_auth import TokenResponse
|
||||
from ..services.user_login_service import (
|
||||
authenticate_user,
|
||||
send_verification_code,
|
||||
verify_code_login,
|
||||
reset_password,
|
||||
register_user,
|
||||
check_user_existence,
|
||||
check_wx_bind_status,
|
||||
bind_wechat_openid
|
||||
)
|
||||
from ..services.auth_service import generate_login_token
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def user_login(request: UserLoginRequest):
|
||||
"""手机号密码登录"""
|
||||
try:
|
||||
user = await authenticate_user(request.phone_number, request.password)
|
||||
# 检查用户类型是否为普通用户
|
||||
if user["user_type"] not in ["player", "user"]:
|
||||
raise HTTPException(403, "非普通用户禁止登录")
|
||||
# 生成访问令牌(默认记住登录状态7天)
|
||||
token, expires_in = generate_login_token(user["phone_number"], remember_me=True)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": expires_in
|
||||
}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise HTTPException(500, "登录服务暂时不可用")
|
||||
|
||||
@router.post("/send_code")
|
||||
async def send_sms_code(request: SendCodeRequest):
|
||||
"""发送短信验证码"""
|
||||
try:
|
||||
return await send_verification_code(request.phone_number)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"短信发送失败: {str(e)}")
|
||||
|
||||
@router.post("/login_with_code", response_model=TokenResponse)
|
||||
async def code_login(request: CodeLoginRequest):
|
||||
"""验证码登录"""
|
||||
try:
|
||||
user = await verify_code_login(request.phone_number, request.code)
|
||||
# 生成访问令牌(默认记住登录状态7天)
|
||||
token, expires_in = generate_login_token(user["phone_number"], remember_me=True)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": expires_in
|
||||
}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise HTTPException(500, "登录服务暂时不可用")
|
||||
|
||||
@router.post("/reset_password")
|
||||
async def user_reset_password(request: ResetPasswordRequest):
|
||||
"""重置密码(需验证短信验证码)"""
|
||||
try:
|
||||
return await reset_password(request)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise HTTPException(500, "密码重置服务暂时不可用")
|
||||
|
||||
|
||||
|
||||
@router.post("/register")
|
||||
async def user_register(request: RegisterRequest):
|
||||
"""注册新用户"""
|
||||
try:
|
||||
return await register_user(request.phone_number, request.code, request.username, request.password)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise HTTPException(500, "注册服务暂时不可用")
|
||||
|
||||
@router.post("/check-existence")
|
||||
async def check_user_existence_route(request: CheckUserExistenceRequest):
|
||||
return await check_user_existence(request.phone_number)
|
||||
|
||||
# 在现有路由之后添加
|
||||
@router.post("/check_wx_bind")
|
||||
async def check_wx_bind(token: str):
|
||||
"""检查微信绑定状态"""
|
||||
try:
|
||||
return await check_wx_bind_status(token)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise HTTPException(500, "服务暂时不可用")
|
||||
|
||||
@router.post("/bind_wechat")
|
||||
async def bind_wechat_account(request: WechatBindRequest):
|
||||
"""绑定微信账号"""
|
||||
try:
|
||||
return await bind_wechat_openid(request.token, request.code)
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception:
|
||||
raise HTTPException(500, "微信绑定服务暂时不可用")
|
||||
21
backend/app/routers/user_coupon.py
Normal file
21
backend/app/routers/user_coupon.py
Normal file
@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from ..services.user_coupon_service import get_user_coupons_service, apply_coupon_service
|
||||
from pydantic import BaseModel
|
||||
|
||||
class GetUserCouponsRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
class CouponApplyRequest(BaseModel):
|
||||
token: str
|
||||
coupon_id: int
|
||||
order_id: int
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/user/coupons")
|
||||
def get_user_coupons(request: GetUserCouponsRequest):
|
||||
return get_user_coupons_service(request.token)
|
||||
|
||||
@router.post("/user/apply-coupon")
|
||||
def apply_coupon(request: CouponApplyRequest):
|
||||
return apply_coupon_service(request.token, request.coupon_id, request.order_id)
|
||||
426
backend/app/routers/user_game.py
Normal file
426
backend/app/routers/user_game.py
Normal file
@ -0,0 +1,426 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from ..services.admin_game_service import get_connection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class GameListRequest(BaseModel):
|
||||
game_type: int
|
||||
|
||||
|
||||
class GameListResponseForUser(BaseModel):
|
||||
game_id: int
|
||||
game_name: str
|
||||
photo_url: str
|
||||
game_type: int
|
||||
tags: List[str]
|
||||
|
||||
|
||||
class GameSearchRequest(BaseModel):
|
||||
search_name: str
|
||||
|
||||
|
||||
class GameDetailResponse(BaseModel):
|
||||
game_id: int
|
||||
game_name: str
|
||||
photo_url: str
|
||||
game_type: int
|
||||
description: str
|
||||
duration: str
|
||||
difficulty_level: int
|
||||
recommended_players: str
|
||||
remaining_quantity: int
|
||||
tags: List[str]
|
||||
long_description: str
|
||||
|
||||
|
||||
class GameDetailRequest(BaseModel):
|
||||
game_id: int
|
||||
|
||||
|
||||
class TagResponse(BaseModel):
|
||||
tag_id: int
|
||||
tag_name: str
|
||||
|
||||
|
||||
class TagGameRequest(BaseModel):
|
||||
tag_ids: List[int]
|
||||
|
||||
|
||||
class GameReviewCreate(BaseModel):
|
||||
token: str
|
||||
game_id: int
|
||||
rating: int = Field(..., gt=0, le=10)
|
||||
comment: Optional[str] = None
|
||||
|
||||
|
||||
class GameReviewResponse(BaseModel):
|
||||
user_name: str
|
||||
rating: int
|
||||
comment: Optional[str] = None
|
||||
created_at: str
|
||||
|
||||
|
||||
class GameReviewsResponse(BaseModel):
|
||||
support_count: int
|
||||
oppose_count: int
|
||||
reviews: List[GameReviewResponse]
|
||||
|
||||
|
||||
class GetUserRatingRequest(BaseModel):
|
||||
token: str
|
||||
game_id: int
|
||||
|
||||
|
||||
class GetUserRatingResponse(BaseModel):
|
||||
rating: int
|
||||
|
||||
|
||||
class UpdateUserRatingRequest(BaseModel):
|
||||
token: str
|
||||
game_id: int
|
||||
good: int
|
||||
|
||||
async def get_current_user(token: str):
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
return payload
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@router.post("/search", response_model=List[GameListResponseForUser])
|
||||
def search_games(request: GameSearchRequest):
|
||||
"""搜索游戏"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT g.*, GROUP_CONCAT(t.tag_name) as tags
|
||||
FROM games g
|
||||
LEFT JOIN game_tags gt ON g.game_id = gt.game_id
|
||||
LEFT JOIN tags t ON gt.tag_id = t.tag_id
|
||||
WHERE g.game_name LIKE %s AND is_available = 1 # 移除 game_type 条件
|
||||
GROUP BY g.game_id
|
||||
""", (f"%{request.search_name}%",)) # 参数减少为一个
|
||||
return [parse_game_with_tags(row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
@router.post("/detail", response_model=GameDetailResponse)
|
||||
def get_game_detail(request: GameDetailRequest):
|
||||
"""获取游戏详情"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT g.*,
|
||||
(g.quantity - COALESCE((
|
||||
SELECT SUM(quantity)
|
||||
FROM orders
|
||||
WHERE game_id = g.game_id
|
||||
AND order_status IN ('in_progress')
|
||||
), 0)) as remaining_quantity,
|
||||
GROUP_CONCAT(t.tag_name) as tags
|
||||
FROM games g
|
||||
LEFT JOIN game_tags gt ON g.game_id = gt.game_id
|
||||
LEFT JOIN tags t ON gt.tag_id = t.tag_id
|
||||
WHERE g.game_id = %s
|
||||
GROUP BY g.game_id
|
||||
""", (request.game_id,))
|
||||
row = cursor.fetchone()
|
||||
return parse_game_detail(row)
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
def parse_game_with_tags(row):
|
||||
return {
|
||||
"game_id": row["game_id"],
|
||||
"game_name": row["game_name"],
|
||||
"photo_url": row["photo_url"] or "",
|
||||
"game_type": row["game_type"],
|
||||
"tags": row["tags"].split(',') if row["tags"] else []
|
||||
}
|
||||
|
||||
|
||||
def parse_game_detail(row):
|
||||
return {
|
||||
"game_id": row["game_id"],
|
||||
"game_name": row["game_name"],
|
||||
"photo_url": row["photo_url"] or "",
|
||||
"game_type": row["game_type"],
|
||||
"description": row["description"],
|
||||
"duration": row["duration"],
|
||||
"long_description": row["long_description"] or "",
|
||||
"difficulty_level": row["difficulty_level"],
|
||||
"recommended_players": f"{row['min_players']}-{row['max_players']}人",
|
||||
"remaining_quantity": row["remaining_quantity"],
|
||||
"tags": row["tags"].split(',') if row["tags"] else []
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tags", response_model=List[TagResponse])
|
||||
def get_all_tags():
|
||||
"""获取所有标签"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("SELECT tag_id, tag_name FROM tags")
|
||||
return [{"tag_id": row["tag_id"], "tag_name": row["tag_name"]} for row in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
@router.post("/by_tag", response_model=List[GameListResponseForUser])
|
||||
def get_games_by_tag(request: TagGameRequest):
|
||||
"""根据多个标签获取游戏列表(空列表时返回所有游戏)"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
base_query = """
|
||||
SELECT g.*, GROUP_CONCAT(t.tag_name) as tags
|
||||
FROM games g
|
||||
LEFT JOIN game_tags gt ON g.game_id = gt.game_id
|
||||
LEFT JOIN tags t ON gt.tag_id = t.tag_id
|
||||
WHERE is_available = 1
|
||||
"""
|
||||
|
||||
# 动态构建查询条件
|
||||
params = []
|
||||
if request.tag_ids:
|
||||
in_params = ', '.join(['%s'] * len(request.tag_ids))
|
||||
base_query += f" AND gt.tag_id IN ({in_params})"
|
||||
params.extend(request.tag_ids)
|
||||
base_query += " GROUP BY g.game_id HAVING COUNT(DISTINCT gt.tag_id) = %s"
|
||||
params.append(len(request.tag_ids))
|
||||
else:
|
||||
base_query += " GROUP BY g.game_id"
|
||||
|
||||
cursor.execute(base_query, tuple(params))
|
||||
return [parse_game_with_tags(row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
# 在路由部分添加新接口
|
||||
@router.post("/review")
|
||||
def create_game_review(request: GameReviewCreate):
|
||||
"""提交游戏评价"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
payload = verify_token(request.token)
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
user_id = user['user_id']
|
||||
|
||||
# 修正参数为元组格式
|
||||
cursor.execute("""
|
||||
INSERT INTO player_reviews
|
||||
(user_id, game_id, rating, comment)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""",(user_id, request.game_id, request.rating, request.comment)) # 用单个元组包裹参数
|
||||
|
||||
connection.commit()
|
||||
return {"message": "评价提交成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
@router.post("/reviews", response_model=GameReviewsResponse)
|
||||
def get_game_reviews(request: GameDetailRequest):
|
||||
"""获取游戏评价"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
# 获取拉数和踩数
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as support_count,
|
||||
SUM(CASE WHEN rating = 0 THEN 1 ELSE 0 END) as oppose_count
|
||||
FROM user_game_rating
|
||||
WHERE game_id = %s
|
||||
""", (request.game_id,))
|
||||
count_result = cursor.fetchone()
|
||||
support_count = int(count_result['support_count']) if count_result and count_result['support_count'] else 0
|
||||
oppose_count = int(count_result['oppose_count']) if count_result and count_result['oppose_count'] else 0
|
||||
|
||||
# 获取有评论的评价列表
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
u.username,
|
||||
pr.rating,
|
||||
pr.comment,
|
||||
pr.created_at
|
||||
FROM player_reviews pr
|
||||
JOIN users u ON pr.user_id = u.user_id
|
||||
WHERE pr.game_id = %s AND pr.comment IS NOT NULL
|
||||
GROUP BY pr.review_id
|
||||
""", (request.game_id,))
|
||||
results = cursor.fetchall()
|
||||
|
||||
# 处理返回结果
|
||||
reviews = [{
|
||||
"user_name": row['username'],
|
||||
"rating": row['rating'],
|
||||
"comment": row['comment'],
|
||||
"created_at": str(row['created_at'])
|
||||
} for row in results]
|
||||
|
||||
return {
|
||||
"support_count": support_count,
|
||||
"oppose_count": oppose_count,
|
||||
"reviews": reviews
|
||||
}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
@router.post("/user-rating", response_model=GetUserRatingResponse)
|
||||
def get_user_rating(request: GetUserRatingRequest):
|
||||
"""获取用户对游戏的拉踩状态"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
payload = verify_token(request.token)
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
user_id = user['user_id']
|
||||
|
||||
# 查询用户对游戏的拉踩状态
|
||||
cursor.execute("""
|
||||
SELECT rating
|
||||
FROM user_game_rating
|
||||
WHERE user_id = %s AND game_id = %s
|
||||
""", (user_id, request.game_id))
|
||||
rating_result = cursor.fetchone()
|
||||
if rating_result:
|
||||
rating = rating_result['rating']
|
||||
else:
|
||||
rating = 3
|
||||
return {"rating": rating}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
@router.post("/update-user-rating")
|
||||
def update_user_rating(request: UpdateUserRatingRequest):
|
||||
"""更新用户对游戏的拉踩记录"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
payload = verify_token(request.token)
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
user_id = user['user_id']
|
||||
|
||||
# 检查是否已有记录
|
||||
cursor.execute("""
|
||||
SELECT id
|
||||
FROM user_game_rating
|
||||
WHERE user_id = %s AND game_id = %s
|
||||
""", (user_id, request.game_id))
|
||||
record = cursor.fetchone()
|
||||
|
||||
if record:
|
||||
if request.good == 3:
|
||||
# 删除记录
|
||||
cursor.execute("""
|
||||
DELETE FROM user_game_rating
|
||||
WHERE user_id = %s AND game_id = %s
|
||||
""", (user['user_id'], request.game_id))
|
||||
else:
|
||||
# 更新记录
|
||||
cursor.execute("""
|
||||
UPDATE user_game_rating
|
||||
SET rating = %s
|
||||
WHERE user_id = %s AND game_id = %s
|
||||
""", (request.good, user['user_id'], request.game_id))
|
||||
else:
|
||||
if request.good != 3:
|
||||
# 插入新记录
|
||||
cursor.execute("""
|
||||
INSERT INTO user_game_rating (user_id, game_id, rating)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (user['user_id'], request.game_id, request.good))
|
||||
|
||||
connection.commit()
|
||||
return {"message": "拉踩记录更新成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
token: str
|
||||
message: str
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message_id: int
|
||||
username: str
|
||||
message_content: str
|
||||
created_at: str
|
||||
|
||||
|
||||
|
||||
@router.post("/messages")
|
||||
def create_message(request: MessageCreate):
|
||||
"""创建用户留言"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
payload = verify_token(request.token)
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
user_id = user['user_id']
|
||||
|
||||
# 插入留言
|
||||
cursor.execute(
|
||||
"""INSERT INTO player_messages
|
||||
(user_id, message_content)
|
||||
VALUES (%s, %s)""",
|
||||
(user_id, request.message)
|
||||
)
|
||||
connection.commit()
|
||||
return {"message": "留言提交成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
@router.get("/messages", response_model=list[MessageResponse])
|
||||
def get_messages():
|
||||
"""获取最新10条留言"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT m.message_id, u.username, m.message_content, m.created_at
|
||||
FROM player_messages m
|
||||
JOIN users u ON m.user_id = u.user_id
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
534
backend/app/routers/user_group.py
Normal file
534
backend/app/routers/user_group.py
Normal file
@ -0,0 +1,534 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from datetime import timezone, timedelta
|
||||
from fastapi import FastAPI
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class CreateGroupRequest(BaseModel):
|
||||
token: str
|
||||
group_name: str
|
||||
description: str
|
||||
max_members: int
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
play_start_time: datetime
|
||||
play_end_time: datetime
|
||||
|
||||
|
||||
class JoinGroupRequest(BaseModel):
|
||||
group_id: int
|
||||
token: str
|
||||
|
||||
|
||||
class GroupOperationRequest(BaseModel):
|
||||
group_id: int
|
||||
|
||||
|
||||
class GroupDetailsRequest(GroupOperationRequest):
|
||||
token: str
|
||||
|
||||
|
||||
class BaseTokenRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
|
||||
class GroupListResponse(BaseModel):
|
||||
group_id: int
|
||||
group_name: str
|
||||
start_date: datetime
|
||||
start_time: datetime
|
||||
end_time: datetime
|
||||
play_start_time: datetime
|
||||
play_end_time: datetime
|
||||
group_status: str
|
||||
max_members: int
|
||||
current_members: int
|
||||
|
||||
|
||||
class LeaveGroupRequest(GroupOperationRequest):
|
||||
token: str
|
||||
|
||||
|
||||
class DeleteGroupRequest(GroupOperationRequest):
|
||||
token: str
|
||||
|
||||
|
||||
class UpdateDescriptionRequest(GroupOperationRequest):
|
||||
new_description: str
|
||||
token: str
|
||||
|
||||
|
||||
def _get_user_id(token: str):
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (phone_number,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(401, "用户不存在")
|
||||
return result[0]
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def _check_group_timing(group_id: int):
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT end_time, group_status
|
||||
FROM game_groups
|
||||
WHERE group_id = %s
|
||||
""", (group_id,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(404, "群组不存在")
|
||||
end_time, status = result
|
||||
return status
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
def create_group(request: CreateGroupRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
start_time = request.start_time.astimezone(timezone(timedelta(hours=8)))
|
||||
end_time = request.end_time.astimezone(timezone(timedelta(hours=8)))
|
||||
play_start_time = request.play_start_time.astimezone(timezone(timedelta(hours=8)))
|
||||
play_end_time = request.play_end_time.astimezone(timezone(timedelta(hours=8)))
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO game_groups
|
||||
(user_id, group_name, description, max_members, start_date,
|
||||
start_time, end_time, group_status, play_start_time, play_end_time)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, 'recruiting', %s, %s)
|
||||
""", (user_id, request.group_name, request.description,
|
||||
request.max_members, start_time.date(), start_time, end_time, play_start_time, play_end_time))
|
||||
|
||||
# 获取新创建的群组ID
|
||||
group_id = cursor.lastrowid
|
||||
# 加入群组
|
||||
cursor.execute("""
|
||||
INSERT INTO group_members (group_id, user_id)
|
||||
VALUES (%s, %s)
|
||||
""", (group_id, user_id))
|
||||
conn.commit()
|
||||
return {"message": "群组创建成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/join")
|
||||
def join_group(request: JoinGroupRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
# 检查是否已加入
|
||||
cursor.execute("SELECT 1 FROM group_members WHERE group_id = %s AND user_id = %s",
|
||||
(request.group_id, user_id))
|
||||
if cursor.fetchone():
|
||||
raise HTTPException(400, "已加入该群组")
|
||||
|
||||
# 检查群组状态和人数
|
||||
cursor.execute("""
|
||||
SELECT max_members, play_start_time
|
||||
FROM game_groups
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
group_info = cursor.fetchone()
|
||||
|
||||
# 获取当前实际人数
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM group_members
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
current_members = cursor.fetchone()[0]
|
||||
|
||||
if not group_info:
|
||||
raise HTTPException(404, "群组不存在")
|
||||
# 已移除的时间限制检查 ↓
|
||||
if current_members >= group_info[0]:
|
||||
raise HTTPException(400, "群组已满员")
|
||||
|
||||
# 加入群组
|
||||
cursor.execute("""
|
||||
INSERT INTO group_members (group_id, user_id)
|
||||
VALUES (%s, %s)
|
||||
""", (request.group_id, user_id))
|
||||
|
||||
# 自动更新群组状态
|
||||
if current_members + 1 == group_info[0]:
|
||||
cursor.execute("""
|
||||
UPDATE game_groups
|
||||
SET group_status = 'full'
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
|
||||
conn.commit()
|
||||
return {"message": "加入成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/members")
|
||||
def get_group_members(request: GroupOperationRequest):
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT u.user_id, u.username
|
||||
FROM group_members gm
|
||||
JOIN users u ON gm.user_id = u.user_id
|
||||
WHERE gm.group_id = %s
|
||||
""", (request.group_id,))
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/managed")
|
||||
def my_managed_groups(request: BaseTokenRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
g.group_id,
|
||||
g.group_name,
|
||||
g.start_date,
|
||||
g.start_time,
|
||||
g.end_time,
|
||||
g.group_status,
|
||||
g.max_members,
|
||||
g.play_start_time,
|
||||
g.play_end_time,
|
||||
(SELECT COUNT(*) FROM group_members WHERE group_id = g.group_id) AS current_members
|
||||
FROM game_groups g
|
||||
WHERE user_id = %s
|
||||
AND group_status IN ('recruiting', 'full', 'pause')
|
||||
""", (user_id,))
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# 退出群组接口
|
||||
@router.post("/leave")
|
||||
def leave_group(request: LeaveGroupRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取群组信息
|
||||
cursor.execute("""
|
||||
SELECT g.end_time, g.group_status, g.max_members
|
||||
FROM game_groups g
|
||||
WHERE g.group_id = %s
|
||||
""", (request.group_id,))
|
||||
group_info = cursor.fetchone()
|
||||
if not group_info:
|
||||
raise HTTPException(404, "群组不存在")
|
||||
|
||||
end_time, status, max_members = group_info
|
||||
|
||||
# 检查退出时间限制
|
||||
current_time = datetime.now(timezone(timedelta(hours=8))).replace(tzinfo=None)
|
||||
if current_time >= end_time - timedelta(hours=2):
|
||||
raise HTTPException(400, "距离招募结束不足2小时,无法退出")
|
||||
|
||||
# 删除成员记录
|
||||
cursor.execute("""
|
||||
DELETE FROM group_members
|
||||
WHERE group_id = %s AND user_id = %s
|
||||
""", (request.group_id, user_id))
|
||||
if cursor.rowcount == 0:
|
||||
raise HTTPException(404, "未加入该群组")
|
||||
|
||||
# 如果原状态是满员,检查并更新状态
|
||||
if status == 'full':
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM group_members
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
current_members = cursor.fetchone()[0]
|
||||
|
||||
if current_members < max_members:
|
||||
cursor.execute("""
|
||||
UPDATE game_groups
|
||||
SET group_status = 'recruiting'
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
|
||||
conn.commit()
|
||||
return {"message": "已成功退出群组"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# 删除群组接口
|
||||
@router.post("/delete")
|
||||
def delete_group(request: DeleteGroupRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 验证管理员权限
|
||||
cursor.execute("""
|
||||
SELECT 1 FROM game_groups
|
||||
WHERE group_id = %s AND user_id = %s
|
||||
""", (request.group_id, user_id))
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(403, "只有群主可以删除群组")
|
||||
|
||||
# 获取结束时间并检查时间限制
|
||||
cursor.execute("""
|
||||
SELECT end_time, group_status
|
||||
FROM game_groups
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(404, "群组不存在")
|
||||
end_time, group_status = result
|
||||
|
||||
current_time = datetime.now(timezone(timedelta(hours=8))).replace(tzinfo=None)
|
||||
if group_status != 'pause' and current_time >= end_time - timedelta(hours=1):
|
||||
raise HTTPException(400, "距离招募结束不足1小时,无法解散群组")
|
||||
|
||||
# 删除关联成员
|
||||
cursor.execute("DELETE FROM group_members WHERE group_id = %s", (request.group_id,))
|
||||
# 删除群组
|
||||
cursor.execute("DELETE FROM game_groups WHERE group_id = %s", (request.group_id,))
|
||||
|
||||
conn.commit()
|
||||
return {"message": "群组已删除"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# 获取我加入的群组
|
||||
@router.post("/joined")
|
||||
def my_joined_groups(request: BaseTokenRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT g.group_id, g.group_name, g.start_date,
|
||||
g.start_time, g.end_time, g.group_status,
|
||||
g.max_members, g.play_start_time, g.play_end_time,
|
||||
(SELECT COUNT(*) FROM group_members WHERE group_id = g.group_id) AS current_members
|
||||
FROM game_groups g
|
||||
JOIN group_members m ON g.group_id = m.group_id
|
||||
WHERE m.user_id = %s
|
||||
AND g.user_id != %s -- 新增排除创建者的条件
|
||||
AND g.group_status IN ('recruiting', 'full', 'pause')
|
||||
""", (user_id, user_id)) # 第二个user_id参数用于排除创建者
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# 获取所有活跃群组
|
||||
@router.get("/active")
|
||||
def get_all_active_groups():
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
group_id,
|
||||
group_name,
|
||||
start_date,
|
||||
start_time,
|
||||
end_time,
|
||||
group_status,
|
||||
max_members,
|
||||
play_start_time,
|
||||
play_end_time,
|
||||
(SELECT COUNT(*) FROM group_members WHERE group_id = g.group_id) AS current_members
|
||||
FROM game_groups g
|
||||
WHERE group_status IN ('recruiting')
|
||||
ORDER BY start_time ASC
|
||||
""")
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/description")
|
||||
def get_group_description(request: GroupOperationRequest):
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT description
|
||||
FROM game_groups
|
||||
WHERE group_id = %s
|
||||
""", (request.group_id,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(404, "群组不存在")
|
||||
return {"description": result['description']}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/update_description")
|
||||
def update_group_description(request: UpdateDescriptionRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
# 验证管理员权限
|
||||
cursor.execute("""
|
||||
SELECT 1 FROM game_groups
|
||||
WHERE group_id = %s AND user_id = %s
|
||||
""", (request.group_id, user_id))
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(403, "无权限修改该群组")
|
||||
|
||||
# 执行更新
|
||||
cursor.execute("""
|
||||
UPDATE game_groups
|
||||
SET description = %s
|
||||
WHERE group_id = %s
|
||||
""", (request.new_description, request.group_id))
|
||||
|
||||
conn.commit()
|
||||
return {"message": "群组简介已更新"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def _update_group_status():
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now()
|
||||
cursor.execute("""
|
||||
UPDATE game_groups
|
||||
SET group_status = 'pause'
|
||||
WHERE group_status IN ('recruiting', 'full')
|
||||
AND end_time < %s
|
||||
""", (now,))
|
||||
conn.commit()
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# 在已有路由后添加新接口
|
||||
@router.get("/check_expired_groups")
|
||||
def check_expired_groups():
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now()
|
||||
# 更新已满员的群组状态
|
||||
cursor.execute("""
|
||||
UPDATE game_groups
|
||||
SET group_status = 'full'
|
||||
WHERE group_status = 'recruiting'
|
||||
AND (SELECT COUNT(*) FROM group_members WHERE group_id = game_groups.group_id) >= max_members
|
||||
AND end_time > %s -- 仅处理未过期的群组
|
||||
""", (now,))
|
||||
full_count = cursor.rowcount
|
||||
|
||||
# 更新已过期的群组状态
|
||||
cursor.execute("""
|
||||
UPDATE game_groups
|
||||
SET group_status = 'pause'
|
||||
WHERE group_status IN ('recruiting', 'full')
|
||||
AND end_time < %s
|
||||
""", (now,))
|
||||
expired_count = cursor.rowcount
|
||||
|
||||
conn.commit()
|
||||
return {
|
||||
"updated_to_full": full_count,
|
||||
"updated_to_pause": expired_count
|
||||
}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# 在已有路由后添加新接口
|
||||
@router.post("/group_details")
|
||||
def get_group_details(request: GroupDetailsRequest):
|
||||
user_id = _get_user_id(request.token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
# 获取群组详情及创建者ID
|
||||
cursor.execute("""
|
||||
SELECT g.*, u.username as creator_name, (SELECT COUNT(*) FROM group_members WHERE group_id = g.group_id) AS current_members
|
||||
FROM game_groups g
|
||||
JOIN users u ON g.user_id = u.user_id
|
||||
WHERE g.group_id = %s
|
||||
""", (request.group_id,))
|
||||
group_info = cursor.fetchone()
|
||||
if not group_info:
|
||||
raise HTTPException(404, "群组不存在")
|
||||
|
||||
# 判断是否为创建者
|
||||
is_creator = group_info['user_id'] == user_id
|
||||
is_joined = False
|
||||
|
||||
if not is_creator:
|
||||
cursor.execute("""
|
||||
SELECT 1 FROM group_members
|
||||
WHERE group_id = %s AND user_id = %s
|
||||
""", (request.group_id, user_id))
|
||||
is_joined = cursor.fetchone() is not None
|
||||
# 构建返回数据
|
||||
return {
|
||||
"group_name": group_info['group_name'],
|
||||
"description": group_info['description'],
|
||||
"start_date": group_info['start_date'],
|
||||
"start_time": group_info['start_time'],
|
||||
"end_time": group_info['end_time'],
|
||||
"play_start_time": group_info['play_start_time'],
|
||||
"play_end_time": group_info['play_end_time'],
|
||||
"group_status": group_info['group_status'],
|
||||
"max_members": group_info['max_members'],
|
||||
"current_members": group_info['current_members'],
|
||||
"creator_name": group_info['creator_name'],
|
||||
"is_creator": is_creator,
|
||||
"is_joined": is_joined if not is_creator else None # 创建者不返回加入状态
|
||||
}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
27
backend/app/routers/user_info.py
Normal file
27
backend/app/routers/user_info.py
Normal file
@ -0,0 +1,27 @@
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from ..services.user_getInfo_service import (
|
||||
get_basic_info,
|
||||
get_order_history,
|
||||
get_points_history
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class TokenRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
# 基本信息接口
|
||||
@router.post("/basic")
|
||||
def get_basic_info_route(request: TokenRequest):
|
||||
return get_basic_info(request.token)
|
||||
|
||||
# 历史订单接口
|
||||
@router.post("/orders")
|
||||
def get_order_history_route(request: TokenRequest):
|
||||
return get_order_history(request.token)
|
||||
|
||||
# 积分变动接口
|
||||
@router.post("/points")
|
||||
def get_points_history_route(request: TokenRequest):
|
||||
return get_points_history(request.token)
|
||||
51
backend/app/routers/user_messages.py
Normal file
51
backend/app/routers/user_messages.py
Normal file
@ -0,0 +1,51 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from ..schemas.message_info import MessageCreate, MessageResponse
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/messages")
|
||||
def create_message(request: MessageCreate):
|
||||
"""创建用户留言"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
payload = verify_token(request.token)
|
||||
cursor.execute("SELECT user_id, wx_openid, points FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
user_id = user['user_id']
|
||||
|
||||
# 插入留言
|
||||
cursor.execute(
|
||||
"""INSERT INTO player_messages
|
||||
(user_id, message_content)
|
||||
VALUES (%s, %s)""",
|
||||
(user_id, request.message)
|
||||
)
|
||||
connection.commit()
|
||||
return {"message": "留言提交成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
@router.get("/messages", response_model=list[MessageResponse])
|
||||
def get_messages():
|
||||
"""获取最新10条留言"""
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT m.message_id, u.username, m.message_content, m.created_at
|
||||
FROM player_messages m
|
||||
JOIN users u ON m.user_id = u.user_id
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
349
backend/app/routers/user_order.py
Normal file
349
backend/app/routers/user_order.py
Normal file
@ -0,0 +1,349 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
from wechatpayv3 import WeChatPay, WeChatPayType
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from string import ascii_letters, digits
|
||||
from datetime import datetime
|
||||
import json
|
||||
from random import sample
|
||||
import configparser
|
||||
from ..services.user_order_service import (
|
||||
get_user_active_order,
|
||||
create_user_order,
|
||||
complete_user_order,
|
||||
get_earliest_pending_order,
|
||||
get_order_detail_with_points,
|
||||
preview_price_adjustment
|
||||
)
|
||||
from ..db import get_connection
|
||||
from decimal import Decimal
|
||||
import time
|
||||
import uuid
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read('backend/config.conf')
|
||||
|
||||
points_rate = Decimal(config.get('price', 'points_rate'))
|
||||
get_points_rate = Decimal(config.get('price', 'get_points_rate'))
|
||||
|
||||
|
||||
logging.basicConfig(filename='demo.log', level=logging.DEBUG, filemode='a',
|
||||
format='%(asctime)s - %(process)s - %(levelname)s: %(message)s')
|
||||
LOGGER = logging.getLogger("demo")
|
||||
|
||||
MCHID = config.get('wechat', 'mchid')
|
||||
with open('backend/cert/apiclient_key.pem') as f:
|
||||
PRIVATE_KEY = f.read()
|
||||
CERT_SERIAL_NO = config.get('wechat','cert_serial_no')
|
||||
APIV3_KEY = config.get('wechat','apiv3_key')
|
||||
APPID = config.get('wechat','appid')
|
||||
NOTIFY_URL = 'https://table-game-backend.miniprogram.ahaostudio.tech/user/order/paystatus/'
|
||||
CERT_DIR = './cert'
|
||||
PARTNER_MODE = False
|
||||
PROXY = None
|
||||
TIMEOUT = (10, 30)
|
||||
|
||||
|
||||
wxpay = WeChatPay(
|
||||
wechatpay_type=WeChatPayType.MINIPROG,
|
||||
mchid=MCHID,
|
||||
private_key=PRIVATE_KEY,
|
||||
cert_serial_no=CERT_SERIAL_NO,
|
||||
apiv3_key=APIV3_KEY,
|
||||
appid=APPID,
|
||||
notify_url=NOTIFY_URL,
|
||||
cert_dir=CERT_DIR,
|
||||
logger=LOGGER,
|
||||
partner_mode=PARTNER_MODE,
|
||||
proxy=PROXY,
|
||||
timeout=TIMEOUT
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class ActiveOrderRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
class CreateOrderRequest(BaseModel):
|
||||
token: str
|
||||
table_id: int
|
||||
num_players: int
|
||||
|
||||
class CompleteOrderRequest(BaseModel):
|
||||
order_id: int
|
||||
token: str
|
||||
|
||||
@router.post("/active")
|
||||
def get_active_order(request: ActiveOrderRequest):
|
||||
return get_user_active_order(request.token)
|
||||
|
||||
@router.post("/create")
|
||||
def create_order(request: CreateOrderRequest):
|
||||
return create_user_order(
|
||||
request.token,
|
||||
request.table_id,
|
||||
request.num_players
|
||||
)
|
||||
|
||||
@router.post("/complete")
|
||||
def complete_order(request: CompleteOrderRequest):
|
||||
return complete_user_order(request.token, request.order_id)
|
||||
|
||||
class PendingOrderRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
class OrderDetailRequest(BaseModel):
|
||||
order_id: int
|
||||
token: str
|
||||
|
||||
@router.post("/pending")
|
||||
def get_pending_order(request: PendingOrderRequest):
|
||||
"""获取用户最早的pending订单"""
|
||||
return get_earliest_pending_order(request.token)
|
||||
|
||||
@router.post("/details")
|
||||
def get_order_details(request: OrderDetailRequest):
|
||||
"""获取订单详情及用户积分"""
|
||||
return get_order_detail_with_points(request.token, request.order_id)
|
||||
|
||||
|
||||
class PricePreviewRequest(BaseModel):
|
||||
token: str
|
||||
order_id: int
|
||||
used_points: int = Field(..., ge=0)
|
||||
|
||||
@router.post("/preview_price")
|
||||
def preview_price(request: PricePreviewRequest):
|
||||
"""价格调整预览接口"""
|
||||
return preview_price_adjustment(
|
||||
request.token,
|
||||
request.order_id,
|
||||
request.used_points
|
||||
)
|
||||
|
||||
|
||||
class OrderPayRequest(BaseModel):
|
||||
token: str
|
||||
order_id: int
|
||||
used_points: int = None
|
||||
coupon_id: int = None
|
||||
|
||||
|
||||
@router.post("/pay/")
|
||||
async def order_pay(request: OrderPayRequest):
|
||||
token = request.token
|
||||
order_id = request.order_id
|
||||
|
||||
if not token or not order_id:
|
||||
raise HTTPException(status_code=400, detail="缺少必要参数")
|
||||
|
||||
try:
|
||||
connection = get_connection()
|
||||
payload = verify_token(token)
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_id, wx_openid, points FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
user_id = user['user_id']
|
||||
wx_openid = user['wx_openid']
|
||||
points = user['points']
|
||||
|
||||
if not wx_openid:
|
||||
raise HTTPException(status_code=400, detail="用户未绑定微信,无法进行支付")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE order_id = %s
|
||||
AND user_id = %s
|
||||
AND order_status = 'pending'
|
||||
FOR UPDATE""",
|
||||
(request.order_id, user_id))
|
||||
order = cursor.fetchone()
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在或不可支付")
|
||||
|
||||
final_price = order['payable_price']
|
||||
|
||||
# 计算最终价格
|
||||
used_points = request.used_points or 0
|
||||
if used_points > 0:
|
||||
if points < used_points:
|
||||
raise HTTPException(400, "积分不足")
|
||||
points_value = used_points * points_rate
|
||||
else:
|
||||
points_value = 0
|
||||
|
||||
coupon_value = 0
|
||||
if request.coupon_id != -1:
|
||||
cursor.execute("""
|
||||
SELECT coupon_type, discount, min_amount, is_used
|
||||
FROM coupons
|
||||
WHERE coupon_id = %s
|
||||
AND expiration_date > NOW()
|
||||
FOR UPDATE
|
||||
""", (request.coupon_id,))
|
||||
coupon = cursor.fetchone()
|
||||
if not coupon:
|
||||
raise HTTPException(400, "无效的优惠券")
|
||||
if coupon['is_used']:
|
||||
raise HTTPException(400, "优惠券已被使用")
|
||||
if coupon['min_amount'] and final_price < coupon['min_amount']:
|
||||
raise HTTPException(400, f"订单金额不足{coupon['min_amount']}元")
|
||||
|
||||
if coupon['coupon_type'] == 'discount':
|
||||
coupon_value = final_price * coupon['discount']
|
||||
elif coupon['coupon_type'] == 'cash':
|
||||
coupon_value = coupon['discount']
|
||||
else:
|
||||
raise HTTPException(400, "未知优惠券类型")
|
||||
|
||||
# 标记优惠券已使用
|
||||
cursor.execute("""
|
||||
UPDATE coupons
|
||||
SET is_used = TRUE
|
||||
WHERE coupon_id = %s
|
||||
""", (request.coupon_id,))
|
||||
|
||||
payable_price = max(final_price - points_value - coupon_value, Decimal('0'))
|
||||
payable_price = int(payable_price * 100) # 转换为分
|
||||
out_trade_no = ''.join(sample(ascii_letters + digits, 8))
|
||||
cursor.execute("SELECT game_table_number FROM game_tables WHERE table_id = %s", (order['game_table_id'],))
|
||||
game_table = cursor.fetchone()
|
||||
game_table_number = game_table['game_table_number'] if game_table else '未知'
|
||||
description = f"即墨区小鲨桌游店-第{game_table_number}桌-订单结算"
|
||||
|
||||
code, message = wxpay.pay(
|
||||
description=description,
|
||||
out_trade_no=out_trade_no,
|
||||
amount={'total': payable_price},
|
||||
payer={'openid': wx_openid},
|
||||
pay_type=WeChatPayType.MINIPROG
|
||||
)
|
||||
|
||||
if code == 200:
|
||||
try:
|
||||
message_dict = json.loads(message)
|
||||
prepay_id = message_dict.get('prepay_id')
|
||||
if prepay_id:
|
||||
timestamp = str(int(time.time()))
|
||||
noncestr = str(uuid.uuid4()).replace('-', '')
|
||||
package = f'prepay_id={prepay_id}'
|
||||
sign = wxpay.sign(data=[APPID, timestamp, noncestr, package])
|
||||
signtype = 'RSA'
|
||||
pay_params = {
|
||||
"appId": APPID,
|
||||
"timeStamp": timestamp,
|
||||
"nonceStr": noncestr,
|
||||
"package": package,
|
||||
"signType": signtype,
|
||||
"paySign": sign
|
||||
}
|
||||
cursor.execute(
|
||||
"UPDATE orders SET out_trade_no = %s WHERE order_id = %s",
|
||||
(out_trade_no, order_id)
|
||||
)
|
||||
if request.coupon_id != -1:
|
||||
cursor.execute("""
|
||||
UPDATE orders SET
|
||||
coupon_id = %s,
|
||||
coupon_type = %s,
|
||||
coupon_value = %s
|
||||
WHERE order_id = %s""",
|
||||
(request.coupon_id, coupon['coupon_type'], coupon_value, request.order_id))
|
||||
|
||||
connection.commit()
|
||||
return {"code": "success", "message": pay_params}
|
||||
else:
|
||||
return {"code": "error", "message": "未获取到 prepay_id"}
|
||||
except json.JSONDecodeError:
|
||||
return {"code": "error", "message": f"无法解析返回的消息: {message}"}
|
||||
else:
|
||||
return {"code": "error", "message": message}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
LOGGER.error(f"处理支付请求时发生错误:{e}")
|
||||
raise HTTPException(status_code=500, detail="处理支付请求时发生错误")
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
connection.close()
|
||||
|
||||
|
||||
@router.post("/paystatus/")
|
||||
async def wxpay_notify(request: Request):
|
||||
connection = None
|
||||
cursor = None
|
||||
try:
|
||||
# 获取回调数据
|
||||
headers = dict(request.headers)
|
||||
body_bytes = await request.body()
|
||||
|
||||
# 验证微信支付签名
|
||||
if not wxpay.verify(headers, body_bytes):
|
||||
logging.warning("签名验证失败")
|
||||
return JSONResponse(content={"code": "FAIL", "message": "签名验证失败"}, status_code=400)
|
||||
|
||||
# 解密回调数据
|
||||
result = wxpay.decrypt_callback(headers, body_bytes)
|
||||
out_trade_no = result.get('out_trade_no')
|
||||
transaction_id = result.get('transaction_id')
|
||||
|
||||
if not out_trade_no or not transaction_id:
|
||||
return JSONResponse(content={"code": "FAIL", "message": "缺少必要参数"}, status_code=400)
|
||||
|
||||
# 获取数据库连接
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 查询订单并锁定
|
||||
cursor.execute("""
|
||||
SELECT order_id, payable_price
|
||||
FROM orders
|
||||
WHERE out_trade_no = %s
|
||||
FOR UPDATE""", (out_trade_no,))
|
||||
order = cursor.fetchone()
|
||||
|
||||
if not order:
|
||||
logging.error(f"订单不存在: {out_trade_no}")
|
||||
return JSONResponse(content={"code": "FAIL", "message": "订单不存在"}, status_code=404)
|
||||
|
||||
# 验证金额(示例)
|
||||
callback_total = int(result.get('amount', {}).get('total', 0))
|
||||
payable_cents = int(order['payable_price'] * 100)
|
||||
|
||||
if callback_total != payable_cents:
|
||||
logging.error(f"金额不匹配: 订单应支付{payable_cents}分,回调收到{callback_total}分")
|
||||
return JSONResponse(content={"code": "FAIL", "message": "金额不匹配"}, status_code=400)
|
||||
|
||||
# 更新订单状态
|
||||
cursor.execute("""
|
||||
UPDATE orders SET
|
||||
order_status = 'completed',
|
||||
payment_method = 'wechat',
|
||||
wx_transaction_id = %s,
|
||||
settlement_time = NOW()
|
||||
WHERE order_id = %s""",
|
||||
(transaction_id, order['order_id']))
|
||||
|
||||
connection.commit()
|
||||
|
||||
# 返回微信要求的成功响应
|
||||
return JSONResponse(content={"code": "SUCCESS", "message": "OK"})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"回调处理异常: {str(e)}", exc_info=True)
|
||||
if connection:
|
||||
connection.rollback()
|
||||
return JSONResponse(content={"code": "FAIL", "message": "系统错误"}, status_code=500)
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
connection.close()
|
||||
41
backend/app/routers/user_table.py
Normal file
41
backend/app/routers/user_table.py
Normal file
@ -0,0 +1,41 @@
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from ..services.user_table_service import (
|
||||
get_table_availability_service,
|
||||
list_tables_service,
|
||||
check_table_occupancy_service,
|
||||
get_table_number_service
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class TableCheckRequest(BaseModel):
|
||||
table_id: int
|
||||
|
||||
class TableNumberRequest(BaseModel):
|
||||
game_table_id: int
|
||||
|
||||
# 桌子统计接口
|
||||
@router.post("/availability")
|
||||
def get_table_availability():
|
||||
return get_table_availability_service()
|
||||
|
||||
# 桌子列表接口(复用已有方法)
|
||||
@router.post("/list")
|
||||
def list_tables():
|
||||
tables = list_tables_service()
|
||||
# 仅返回需要的字段
|
||||
return [{
|
||||
"table_id": t["table_id"],
|
||||
"game_table_number": t["game_table_number"]
|
||||
} for t in tables]
|
||||
|
||||
# 检查桌子占用状态
|
||||
@router.post("/check_occupancy")
|
||||
def check_table_occupancy(request: TableCheckRequest):
|
||||
return check_table_occupancy_service(request.table_id)
|
||||
|
||||
@router.post("/number")
|
||||
def get_table_number(request: TableNumberRequest):
|
||||
"""根据桌台ID获取桌号"""
|
||||
return get_table_number_service(request.game_table_id)
|
||||
10
backend/app/schemas/admin_auth.py
Normal file
10
backend/app/schemas/admin_auth.py
Normal file
@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
expires_in: int
|
||||
79
backend/app/schemas/game_info.py
Normal file
79
backend/app/schemas/game_info.py
Normal file
@ -0,0 +1,79 @@
|
||||
from pydantic import BaseModel, condecimal, conint, Field
|
||||
from typing import Optional
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class GameListRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
|
||||
class GameCreate(BaseModel):
|
||||
game_name: str
|
||||
game_type: int
|
||||
description: str
|
||||
min_players: int
|
||||
max_players: int
|
||||
duration: str
|
||||
price: float
|
||||
difficulty_level: int
|
||||
is_available: int
|
||||
quantity: int
|
||||
long_description: str
|
||||
|
||||
|
||||
class GameCreateRequest(GameCreate):
|
||||
token: str
|
||||
|
||||
|
||||
class GameListResponse(BaseModel):
|
||||
game_id: int
|
||||
game_name: str
|
||||
game_type: int
|
||||
description: str
|
||||
min_players: int
|
||||
max_players: int
|
||||
duration: str
|
||||
price: float
|
||||
difficulty_level: int
|
||||
is_available: int
|
||||
quantity: int
|
||||
|
||||
|
||||
class GamePhotoRequest(BaseModel):
|
||||
game_id: int
|
||||
photo_url: str
|
||||
token: str
|
||||
|
||||
|
||||
class TagCreateRequest(BaseModel):
|
||||
tag_name: str = Field(..., min_length=1, max_length=50, example="亲子游戏")
|
||||
token: str # 管理员权限验证
|
||||
|
||||
class GameTagLinkRequest(BaseModel):
|
||||
tag_id: int = Field(..., gt=0, example=1)
|
||||
game_ids: list[int] = Field(..., min_items=1, example=[1,2,3])
|
||||
token: str # 管理员权限验证
|
||||
|
||||
class GameTagUnLinkRequest(BaseModel):
|
||||
tag_id: int = Field(..., gt=0, example=1)
|
||||
game_id: int
|
||||
token: str # 管理员权限验证
|
||||
|
||||
class GetGameTagsRequest(BaseModel):
|
||||
tag_id: int = Field(..., gt=0, example=1)
|
||||
token: str
|
||||
|
||||
class DeleteTagRequest(BaseModel):
|
||||
tag_id: int = Field(
|
||||
...,
|
||||
gt=0,
|
||||
example=1,
|
||||
description="要删除的标签ID"
|
||||
)
|
||||
token: str = Field(
|
||||
...,
|
||||
example="your_jwt_token_here",
|
||||
description="管理员身份验证令牌"
|
||||
)
|
||||
12
backend/app/schemas/message_info.py
Normal file
12
backend/app/schemas/message_info.py
Normal file
@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
token: str
|
||||
message: str
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
message_id: int
|
||||
username: str
|
||||
message_content: str
|
||||
created_at: datetime
|
||||
56
backend/app/schemas/order_info.py
Normal file
56
backend/app/schemas/order_info.py
Normal file
@ -0,0 +1,56 @@
|
||||
from pydantic import BaseModel, condecimal, conint, Field
|
||||
from typing import Optional
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
class OrderInfo(BaseModel):
|
||||
order_id: int
|
||||
user_id: Optional[int]
|
||||
order_date: date
|
||||
num_players: int
|
||||
order_status: str
|
||||
|
||||
# 额外字段
|
||||
payable_price: Optional[Decimal] = None
|
||||
paid_price: Optional[Decimal] = None
|
||||
payment_method: Optional[str] = None
|
||||
start_datetime: Optional[datetime] = None
|
||||
end_datetime: Optional[datetime] = None
|
||||
discount_amount: Optional[Decimal] = None
|
||||
used_points: Optional[int] = None
|
||||
coupon_id: Optional[int] = None
|
||||
game_process_time: Optional[int] = None
|
||||
game_table_number: Optional[str] = None
|
||||
user_name: Optional[str] = None
|
||||
settlement_time: Optional[datetime] = None
|
||||
|
||||
class OrderListResponse(BaseModel):
|
||||
orders: list[OrderInfo]
|
||||
total: int
|
||||
|
||||
class OrderRangeQueryRequest(BaseModel):
|
||||
token: str
|
||||
start: int
|
||||
end: int
|
||||
order_status: Literal['pending', 'paid', 'in_progress', 'completed', 'cancelled']
|
||||
# 订单状态包含:['待处理', '已支付', '进行中', '已完成', '已取消']
|
||||
|
||||
class OrderCompleteRequest(BaseModel):
|
||||
token: str
|
||||
end_datetime: datetime
|
||||
|
||||
class OrderSettleRequest(BaseModel):
|
||||
token: str
|
||||
used_points: int = Field(ge=0)
|
||||
coupon_id: Optional[int] = None
|
||||
|
||||
class OrderDetailResponse(OrderInfo):
|
||||
table_price: Decimal
|
||||
game_price: Optional[Decimal] = None
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
class OrderDetailRequest(BaseModel):
|
||||
token: str
|
||||
order_id: int
|
||||
18
backend/app/schemas/table_info.py
Normal file
18
backend/app/schemas/table_info.py
Normal file
@ -0,0 +1,18 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class TableCreate(BaseModel):
|
||||
game_table_number: str
|
||||
capacity: int
|
||||
price: float
|
||||
|
||||
class TableCreateRequest(BaseModel):
|
||||
token: str
|
||||
game_table_number: str
|
||||
capacity: int
|
||||
price: float
|
||||
|
||||
class TableResponse(BaseModel):
|
||||
table_id: int
|
||||
game_table_number: str
|
||||
capacity: int
|
||||
price: float
|
||||
30
backend/app/schemas/user_auth.py
Normal file
30
backend/app/schemas/user_auth.py
Normal file
@ -0,0 +1,30 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class UserLoginRequest(BaseModel):
|
||||
phone_number: str
|
||||
password: str
|
||||
|
||||
class CodeLoginRequest(BaseModel):
|
||||
phone_number: str
|
||||
code: str
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
phone_number: str
|
||||
code: str
|
||||
new_password: str
|
||||
|
||||
class SendCodeRequest(BaseModel):
|
||||
phone_number: str
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
phone_number: str
|
||||
code: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class CheckUserExistenceRequest(BaseModel):
|
||||
phone_number: str
|
||||
|
||||
class WechatBindRequest(BaseModel):
|
||||
token: str
|
||||
code: str
|
||||
59
backend/app/schemas/user_info.py
Normal file
59
backend/app/schemas/user_info.py
Normal file
@ -0,0 +1,59 @@
|
||||
from pydantic import BaseModel, EmailStr, constr, validator, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
class UserRangeQueryRequest(BaseModel):
|
||||
token: str
|
||||
start: int
|
||||
end: int
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
user_id: int
|
||||
username: str
|
||||
points: int
|
||||
gender: Optional[str]
|
||||
phone_number: Optional[str]
|
||||
email: Optional[str]
|
||||
user_type: str
|
||||
created_at: Optional[datetime]
|
||||
updated_at: Optional[datetime]
|
||||
|
||||
class UserDeleteRequest(BaseModel):
|
||||
token: str
|
||||
uid: int
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
token: str
|
||||
uid: int
|
||||
username: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
phone_number: Optional[str] = None # 使用 str 类型
|
||||
gender: Optional[str] = None # 'male', 'female', 'other'
|
||||
user_type: Optional[str] = None # 'admin' or 'player'
|
||||
|
||||
class TotalNumberOfUsers(BaseModel):
|
||||
token: str
|
||||
|
||||
class UserQueryRequest(BaseModel):
|
||||
token: str
|
||||
query_mode: Literal["phone_number", "email", "username", "uid"] # 限定模式类型
|
||||
query_value: str = Field(..., description="查询值,支持电话号码、邮箱、用户名或用户ID")
|
||||
|
||||
class UserPasswordUpdateRequest(BaseModel):
|
||||
token: str
|
||||
uid: int
|
||||
new_password: str
|
||||
|
||||
class UserPointsUpdateRequest(BaseModel):
|
||||
token: str
|
||||
uid: int
|
||||
points: int
|
||||
reason: str
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
phone_number: str
|
||||
code: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
91
backend/app/services/admin_coupon_service.py
Normal file
91
backend/app/services/admin_coupon_service.py
Normal file
@ -0,0 +1,91 @@
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
def validate_admin(token):
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s", (payload['sub'],))
|
||||
user = cursor.fetchone()
|
||||
if not user or user['user_type'] != 'admin':
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
return True
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
def create_coupon_service(token: str, **coupon_data):
|
||||
validate_admin(token)
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
valid_from = coupon_data['valid_from'].replace('T', ' ')
|
||||
valid_to = coupon_data['valid_to'].replace('T', ' ')
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO coupons (
|
||||
coupon_code, discount_type, discount_amount,
|
||||
min_order_amount, valid_from, valid_to, quantity
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
coupon_data['coupon_code'],
|
||||
coupon_data['discount_type'],
|
||||
coupon_data['discount_amount'],
|
||||
coupon_data.get('min_order_amount'),
|
||||
valid_from,
|
||||
valid_to,
|
||||
coupon_data['quantity']
|
||||
))
|
||||
connection.commit()
|
||||
return {"message": "优惠券创建成功"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
def get_coupons_service(token: str):
|
||||
validate_admin(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM coupons")
|
||||
return cursor.fetchall()
|
||||
|
||||
def issue_coupon_service(token: str, coupon_id: int, user_id: int):
|
||||
validate_admin(token)
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
|
||||
# 检查优惠券是否存在
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO user_coupons (user_id, coupon_id)
|
||||
VALUES (%s, %s)
|
||||
""", (user_id, coupon_id))
|
||||
|
||||
# 检查库存
|
||||
cursor.execute("SELECT quantity FROM coupons WHERE coupon_id = %s", (coupon_id,))
|
||||
coupon = cursor.fetchone()
|
||||
if not coupon or coupon['quantity'] <= 0:
|
||||
raise HTTPException(status_code=400, detail="优惠券库存不足")
|
||||
|
||||
# 减少库存
|
||||
cursor.execute("""
|
||||
UPDATE coupons SET quantity = quantity - 1
|
||||
WHERE coupon_id = %s
|
||||
""", (coupon_id,))
|
||||
connection.commit()
|
||||
return {"message": "优惠券发放成功"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
def delete_coupon_service(token: str, coupon_id: int):
|
||||
validate_admin(token)
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("DELETE FROM coupons WHERE coupon_id = %s", (coupon_id,))
|
||||
connection.commit()
|
||||
return {"message": "优惠券删除成功"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
326
backend/app/services/admin_game_service.py
Normal file
326
backend/app/services/admin_game_service.py
Normal file
@ -0,0 +1,326 @@
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from mysql.connector import IntegrityError
|
||||
|
||||
def _verify_admin_permission(token: str):
|
||||
"""公共权限验证方法"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user or admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
return username
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def create_game(token: str, game) -> dict:
|
||||
"""
|
||||
服务层:创建游戏
|
||||
"""
|
||||
# print(token)
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO games
|
||||
(game_name, game_type, description, min_players, max_players, duration, price, difficulty_level, is_available, quantity, long_description)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
game.game_name,
|
||||
game.game_type,
|
||||
game.description,
|
||||
game.min_players,
|
||||
game.max_players,
|
||||
game.duration,
|
||||
game.price,
|
||||
game.difficulty_level,
|
||||
game.is_available,
|
||||
game.quantity,
|
||||
game.long_description,
|
||||
),
|
||||
)
|
||||
|
||||
connection.commit()
|
||||
return {"message": "游戏创建成功"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def delete_game(token: str, game_id: int) -> dict:
|
||||
"""
|
||||
服务层:删除游戏
|
||||
"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute("DELETE FROM games WHERE game_id = %s", (game_id,))
|
||||
if cursor.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="游戏不存在")
|
||||
connection.commit()
|
||||
return {"message": "删除成功"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def update_game(token: str, game_id: int, game) -> dict:
|
||||
"""
|
||||
服务层:更新游戏信息
|
||||
"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE games SET
|
||||
game_name=%s, game_type=%s, description=%s,
|
||||
min_players=%s, max_players=%s, duration=%s,
|
||||
price=%s, difficulty_level=%s , quantity=%s ,
|
||||
is_available=%s , long_description=%s
|
||||
WHERE game_id=%s
|
||||
""",
|
||||
(
|
||||
game.game_name,
|
||||
game.game_type,
|
||||
game.description,
|
||||
game.min_players,
|
||||
game.max_players,
|
||||
game.duration,
|
||||
game.price,
|
||||
game.difficulty_level,
|
||||
game.quantity,
|
||||
game.is_available,
|
||||
game.long_description,
|
||||
game_id,
|
||||
),
|
||||
)
|
||||
status = cursor.statusmessage if hasattr(cursor, "statusmessage") else ""
|
||||
print("数据库返回的日志:", "rowcount =", cursor.rowcount, status)
|
||||
if cursor.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="游戏不存在")
|
||||
connection.commit()
|
||||
return {"message": "更新成功"}
|
||||
except Exception as e:
|
||||
print("数据库错误日志:", str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def list_games_service(token: str):
|
||||
"""
|
||||
服务层:获取所有游戏数据
|
||||
"""
|
||||
print(token)
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("SELECT * FROM games")
|
||||
return cursor.fetchall()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def set_the_game_photo(token: str, photo_url: str, game_id: int):
|
||||
"""
|
||||
服务层:设置游戏图片
|
||||
"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE games SET
|
||||
photo_url=%s
|
||||
WHERE game_id=%s
|
||||
""",
|
||||
(
|
||||
photo_url,
|
||||
game_id,
|
||||
),
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="游戏不存在")
|
||||
connection.commit()
|
||||
return {"message": "图片设置成功"}
|
||||
except Exception as e:
|
||||
print("数据库错误日志:", str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def create_tag_service(token: str, tag_name: str) -> dict:
|
||||
"""创建标签服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"INSERT INTO tags (tag_name) VALUES (%s)",
|
||||
(tag_name,)
|
||||
)
|
||||
connection.commit()
|
||||
return {"tag_id": cursor.lastrowid, "tag_name": tag_name}
|
||||
except IntegrityError:
|
||||
raise HTTPException(400, "标签已存在")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def link_tag_to_games_service(token: str, tag_id: int, game_ids: list[int]) -> dict:
|
||||
"""批量关联服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
# 检查标签存在
|
||||
cursor.execute("SELECT 1 FROM tags WHERE tag_id = %s", (tag_id,))
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(404, "标签不存在")
|
||||
|
||||
# 批量插入
|
||||
values = [(game_id, tag_id) for game_id in game_ids]
|
||||
cursor.executemany(
|
||||
"INSERT IGNORE INTO game_tags (game_id, tag_id) VALUES (%s, %s)",
|
||||
values
|
||||
)
|
||||
connection.commit()
|
||||
return {"linked_count": cursor.rowcount}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
def list_tags_service(token: str) -> list:
|
||||
"""获取所有标签服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(500, "数据库连接失败")
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT t.tag_id,
|
||||
t.tag_name,
|
||||
COUNT(gt.game_id) AS game_count
|
||||
FROM tags t
|
||||
LEFT JOIN game_tags gt ON t.tag_id = gt.tag_id
|
||||
GROUP BY t.tag_id
|
||||
ORDER BY t.tag_id
|
||||
""")
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def get_game_tags_service(token: str, game_id: int) -> list:
|
||||
"""获取游戏标签服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(500, "数据库连接失败")
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT t.tag_id, t.tag_name
|
||||
FROM game_tags gt
|
||||
JOIN tags t ON gt.tag_id = t.tag_id
|
||||
WHERE gt.game_id = %s
|
||||
""",
|
||||
(game_id,)
|
||||
)
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def unlink_tag_from_game_service(token: str, tag_id: int, game_id: int) -> dict:
|
||||
"""取消关联服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor()
|
||||
print("tag_id:", tag_id, "game_id:", game_id)
|
||||
try:
|
||||
cursor.execute(
|
||||
"DELETE FROM game_tags WHERE tag_id = %s AND game_id = %s",
|
||||
(tag_id, game_id)
|
||||
)
|
||||
connection.commit()
|
||||
return {"deleted": cursor.rowcount}
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
def get_games_by_tag_service(token: str, tag_id: int) -> list:
|
||||
"""获取标签关联游戏服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(500, "数据库连接失败")
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT g.game_id, g.game_name
|
||||
FROM game_tags gt
|
||||
JOIN games g ON gt.game_id = g.game_id
|
||||
WHERE gt.tag_id = %s
|
||||
""",
|
||||
(tag_id,)
|
||||
)
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
def delete_tag_service(token: str, tag_id: int) -> dict:
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
# 先删除关联关系
|
||||
cursor.execute("DELETE FROM game_tags WHERE tag_id = %s", (tag_id,))
|
||||
# 再删除标签
|
||||
cursor.execute("DELETE FROM tags WHERE tag_id = %s", (tag_id,))
|
||||
connection.commit()
|
||||
return {"deleted": cursor.rowcount}
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
59
backend/app/services/admin_group_service.py
Normal file
59
backend/app/services/admin_group_service.py
Normal file
@ -0,0 +1,59 @@
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
def _verify_admin_permission(token: str):
|
||||
"""公共权限验证方法"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user or admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
return username
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def list_groups_service(token: str) -> list:
|
||||
"""获取所有游戏群组服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT gg.*, u.username AS leader_name
|
||||
FROM game_groups gg
|
||||
LEFT JOIN users u ON gg.user_id = u.user_id
|
||||
""")
|
||||
return cursor.fetchall()
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def delete_group_service(token: str, group_id: int) -> dict:
|
||||
"""删除群组服务"""
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
# 先删除关联的成员
|
||||
cursor.execute("DELETE FROM group_members WHERE group_id = %s", (group_id,))
|
||||
# 再删除群组
|
||||
cursor.execute("DELETE FROM game_groups WHERE group_id = %s", (group_id,))
|
||||
connection.commit()
|
||||
return {"message": "删除成功"}
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
raise HTTPException(500, str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
221
backend/app/services/admin_order_service.py
Normal file
221
backend/app/services/admin_order_service.py
Normal file
@ -0,0 +1,221 @@
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
import configparser
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
# 读取配置文件
|
||||
config = configparser.ConfigParser()
|
||||
config.read('backend/config.conf')
|
||||
unit_price = Decimal(config.get('price', 'unit_price'))
|
||||
points_rate = Decimal(config.get('price', 'points_rate'))
|
||||
get_points_rate = Decimal(config.get('price', 'get_points_rate'))
|
||||
|
||||
def verify_admin_permission(token: str):
|
||||
"""公共权限验证方法"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user or admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
return username
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def query_orders_by_status_and_range(token: str, start: int, end: int, order_status: str):
|
||||
verify_admin_permission(token)
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
o.order_id,
|
||||
o.user_id,
|
||||
u.username AS user_name, -- 添加用户名
|
||||
g.game_table_number AS game_table_number, -- 添加游戏桌编号
|
||||
o.order_date,
|
||||
o.num_players,
|
||||
o.order_status,
|
||||
o.payable_price,
|
||||
o.paid_price,
|
||||
o.payment_method,
|
||||
o.start_datetime,
|
||||
o.end_datetime,
|
||||
o.used_points, -- 使用的积分
|
||||
o.coupon_id, -- 使用的优惠券
|
||||
o.game_process_time, -- 总游戏时间
|
||||
o.settlement_time
|
||||
FROM orders o
|
||||
LEFT JOIN users u ON o.user_id = u.user_id
|
||||
LEFT JOIN game_tables g ON o.game_table_id = g.table_id
|
||||
WHERE order_status = %s
|
||||
ORDER BY o.order_id ASC
|
||||
LIMIT %s OFFSET %s
|
||||
""", (order_status, end - start + 1, start - 1))
|
||||
result = cursor.fetchall()
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM orders WHERE order_status = %s", (order_status,))
|
||||
total = cursor.fetchone()["COUNT(*)"]
|
||||
|
||||
return {"orders": result, "total": total}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error during query: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def complete_order(token: str, order_id: int, end_datetime: datetime):
|
||||
"""结束订单并计算原始价格"""
|
||||
verify_admin_permission(token)
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("""
|
||||
SELECT o.start_datetime, t.price AS table_price,
|
||||
COALESCE(g.price, 1) AS game_price, o.num_players -- 处理空值情况
|
||||
FROM orders o
|
||||
JOIN game_tables t ON o.game_table_id = t.table_id
|
||||
LEFT JOIN games g ON o.game_id = g.game_id
|
||||
WHERE o.order_id = %s
|
||||
FOR UPDATE
|
||||
""", (order_id,))
|
||||
order = cursor.fetchone()
|
||||
|
||||
# 将数据库中的时间和输入的时间转换为 naive(移除时区信息)
|
||||
db_start = order['start_datetime'].replace(tzinfo=None)
|
||||
input_end = end_datetime.replace(tzinfo=None)
|
||||
|
||||
# 计算游戏时长(分钟),duration_minutes 是 float 类型
|
||||
duration_minutes = (input_end - db_start).total_seconds() / 60
|
||||
# 将时长转换为 Decimal 类型,避免与 Decimal 相乘时报错
|
||||
duration_minutes_dec = Decimal(str(duration_minutes))
|
||||
|
||||
# 计算原始价格(假设 table_price 单位为每分钟价格)
|
||||
base_price = (
|
||||
order['table_price'] *
|
||||
order['game_price'] *
|
||||
order['num_players'] *
|
||||
duration_minutes_dec *
|
||||
unit_price
|
||||
)
|
||||
print(f"Base Price: {base_price}")
|
||||
# 更新订单信息
|
||||
cursor.execute("""
|
||||
UPDATE orders
|
||||
SET end_datetime = %s,
|
||||
payable_price = %s,
|
||||
game_process_time = %s,
|
||||
order_status = 'pending'
|
||||
WHERE order_id = %s
|
||||
""", (end_datetime, base_price, duration_minutes_dec, order_id))
|
||||
|
||||
connection.commit()
|
||||
return {"message": "订单已结束待结算", "base_price": base_price}
|
||||
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def settle_order(token: str, order_id: int, used_points: int = 0, coupon_id: int = None):
|
||||
"""结算最终订单"""
|
||||
verify_admin_permission(token)
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("""
|
||||
SELECT payable_price, user_id
|
||||
FROM orders
|
||||
WHERE order_id = %s
|
||||
AND order_status = 'pending'
|
||||
FOR UPDATE
|
||||
""", (order_id,))
|
||||
order = cursor.fetchone()
|
||||
|
||||
|
||||
points_value = used_points * points_rate
|
||||
final_price = max(order['payable_price'] - points_value, Decimal('0'))
|
||||
settlement_time = datetime.now(pytz.timezone('Asia/Shanghai'))
|
||||
|
||||
# 更新订单状态
|
||||
cursor.execute("""
|
||||
UPDATE orders
|
||||
SET paid_price = %s,
|
||||
used_points = %s,
|
||||
coupon_id = %s,
|
||||
order_status = 'completed',
|
||||
payment_method = 'offline',
|
||||
settlement_time = %s
|
||||
WHERE order_id = %s
|
||||
""", (final_price, used_points, coupon_id, settlement_time.strftime("%Y-%m-%d %H:%M:%S"), order_id))
|
||||
|
||||
if used_points > 0:
|
||||
earned_points = 0
|
||||
else:
|
||||
earned_points = int(final_price * get_points_rate)
|
||||
|
||||
# 更新用户积分
|
||||
cursor.execute("""
|
||||
UPDATE users
|
||||
SET points = points - %s + %s
|
||||
WHERE user_id = %s
|
||||
""", (used_points, earned_points, order['user_id']))
|
||||
|
||||
# 插入积分变动记录
|
||||
change_amount = earned_points - used_points
|
||||
reason = f"订单结算,订单号:{order_id}"
|
||||
cursor.execute("""
|
||||
INSERT INTO points_history (user_id, change_amount, reason)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (order['user_id'], change_amount, reason))
|
||||
|
||||
connection.commit()
|
||||
return {"message": "订单结算成功", "final_price": final_price}
|
||||
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
# 在适当位置新增服务层方法
|
||||
def get_order_details_service(token: str, order_id: int):
|
||||
"""服务层:获取订单详情"""
|
||||
verify_admin_permission(token) # 复用已有的权限验证方法
|
||||
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
with connection.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("""
|
||||
SELECT o.*, t.price AS table_price, g.price AS game_price
|
||||
FROM orders o
|
||||
JOIN game_tables t ON o.game_table_id = t.table_id
|
||||
LEFT JOIN games g ON o.game_id = g.game_id
|
||||
WHERE o.order_id = %s
|
||||
""", (order_id,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Order not found")
|
||||
return result
|
||||
finally:
|
||||
connection.close()
|
||||
128
backend/app/services/admin_table_service.py
Normal file
128
backend/app/services/admin_table_service.py
Normal file
@ -0,0 +1,128 @@
|
||||
from fastapi import HTTPException
|
||||
import mysql.connector
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
def _verify_admin_permission(token: str):
|
||||
"""统一权限验证方法"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user or admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def create_table(token: str, table_data: dict) -> dict:
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 检查桌号是否已存在
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) AS count FROM game_tables WHERE game_table_number = %s",
|
||||
(table_data['game_table_number'],)
|
||||
)
|
||||
if cursor.fetchone()['count'] > 0:
|
||||
raise HTTPException(status_code=400, detail="桌号已存在")
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO game_tables (game_table_number, capacity, price) VALUES (%s, %s, %s)",
|
||||
(table_data['game_table_number'], table_data['capacity'], table_data['price'])
|
||||
)
|
||||
connection.commit()
|
||||
return {"message": "桌台创建成功"}
|
||||
except mysql.connector.IntegrityError as e:
|
||||
raise HTTPException(status_code=400, detail="桌号已存在")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
|
||||
def delete_table(token: str, table_id: int) -> dict:
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 检查关联订单
|
||||
cursor.execute("SELECT COUNT(*) AS order_count FROM orders WHERE game_table_id = %s", (table_id,))
|
||||
order_count = cursor.fetchone()["order_count"]
|
||||
if order_count > 0:
|
||||
raise HTTPException(status_code=400, detail="存在关联订单,无法删除")
|
||||
|
||||
cursor.execute("DELETE FROM game_tables WHERE table_id = %s", (table_id,))
|
||||
if cursor.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="桌台不存在")
|
||||
connection.commit()
|
||||
return {"message": "删除成功"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
|
||||
def update_table(token: str, table_id: int, table_data: dict) -> dict:
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
# 检查桌号是否已存在
|
||||
# 新增唯一性校验(排除当前记录)
|
||||
cursor.execute(
|
||||
"""SELECT COUNT(*) AS count
|
||||
FROM game_tables
|
||||
WHERE game_table_number = %s AND table_id != %s""",
|
||||
(table_data['game_table_number'], table_id)
|
||||
)
|
||||
if cursor.fetchone()['count'] > 0:
|
||||
raise HTTPException(status_code=400, detail="桌号已存在")
|
||||
|
||||
cursor.execute(
|
||||
"""UPDATE game_tables SET
|
||||
game_table_number=%s,
|
||||
capacity=%s,
|
||||
price=%s
|
||||
WHERE table_id=%s""",
|
||||
(table_data['game_table_number'], table_data['capacity'], table_data['price'], table_id)
|
||||
)
|
||||
if cursor.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="桌台不存在")
|
||||
connection.commit()
|
||||
return {"message": "更新成功"}
|
||||
except mysql.connector.IntegrityError as e:
|
||||
raise HTTPException(status_code=400, detail="桌号已存在")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
|
||||
def list_tables_service(token: str):
|
||||
_verify_admin_permission(token)
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM game_tables")
|
||||
return cursor.fetchall()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
394
backend/app/services/admin_user_service.py
Normal file
394
backend/app/services/admin_user_service.py
Normal file
@ -0,0 +1,394 @@
|
||||
from multiprocessing import connection
|
||||
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
import hashlib
|
||||
|
||||
|
||||
def query_users_by_range(token: str, start: int, end: int):
|
||||
"""
|
||||
1. 验证 token 是否有效,以及是否是 admin 用户
|
||||
2. 查询 user_id 在 [start, end] 范围内的用户
|
||||
3. 返回用户的基本信息
|
||||
"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token) # 如果验证失败会抛出异常
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# 2. 确认用户类型是否为 admin
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
|
||||
# 3. 查询范围内的用户
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
user_id,
|
||||
username,
|
||||
points,
|
||||
phone_number,
|
||||
gender,
|
||||
email,
|
||||
user_type,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM users
|
||||
WHERE user_id BETWEEN %s AND %s
|
||||
ORDER BY user_id ASC
|
||||
""", (start, end))
|
||||
result = cursor.fetchall()
|
||||
|
||||
# 4. 返回结果
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error during query: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def delete_user_by_token(token: str, uid: int):
|
||||
"""
|
||||
删除用户逻辑:
|
||||
1. 验证 token 确保为管理员。
|
||||
2. 检查指定用户是否存在。
|
||||
3. 检查用户是否有相关订单信息。
|
||||
4. 删除用户。
|
||||
"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token) # 如果验证失败会抛出异常
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# 2. 确认用户类型是否为 admin
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
|
||||
# 3. 检查用户是否存在
|
||||
cursor.execute("SELECT * FROM users WHERE user_id = %s;", (uid,))
|
||||
user_to_delete = cursor.fetchone()
|
||||
|
||||
if not user_to_delete:
|
||||
raise HTTPException(status_code=404, detail=f"User with ID {uid} not found.")
|
||||
|
||||
# 4. 检查用户是否有订单
|
||||
cursor.execute("SELECT COUNT(*) AS order_count FROM orders WHERE user_id = %s;", (uid,))
|
||||
order_info = cursor.fetchone()
|
||||
|
||||
if order_info["order_count"] > 0:
|
||||
raise HTTPException(status_code=400, detail=f"Cannot delete user with ID {uid}: user has existing orders.")
|
||||
|
||||
# 5. 删除用户
|
||||
cursor.execute("DELETE FROM users WHERE user_id = %s;", (uid,))
|
||||
connection.commit()
|
||||
|
||||
return f"User with ID {uid} deleted successfully."
|
||||
|
||||
# except Exception as e:
|
||||
# raise HTTPException(status_code=500, detail=f"Error during deletion: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
def update_user(token: str, uid: int, username: str = None, email: str = None,
|
||||
phone_number: str = None, gender: str = None, user_type: str = None):
|
||||
"""
|
||||
管理员更新用户信息:
|
||||
1. 验证 token 确保为管理员。
|
||||
2. 确认用户存在。
|
||||
3. 根据提供的参数更新用户信息。
|
||||
"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token) # 如果验证失败会抛出异常
|
||||
admin_username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# 2. 确认管理员权限
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (admin_username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
|
||||
# 3. 检查用户是否存在
|
||||
cursor.execute("SELECT * FROM users WHERE user_id = %s;", (uid,))
|
||||
user_to_update = cursor.fetchone()
|
||||
|
||||
if not user_to_update:
|
||||
raise HTTPException(status_code=404, detail=f"User with ID {uid} not found.")
|
||||
|
||||
# 4. 根据提供的字段更新用户信息
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
print(username, email, phone_number, gender, user_type)
|
||||
|
||||
if username:
|
||||
updates.append("username = %s")
|
||||
params.append(username)
|
||||
if email:
|
||||
updates.append("email = %s")
|
||||
params.append(email)
|
||||
if phone_number:
|
||||
updates.append("phone_number = %s")
|
||||
params.append(phone_number)
|
||||
if gender:
|
||||
updates.append("gender = %s")
|
||||
params.append(gender)
|
||||
if user_type:
|
||||
if user_type not in ['admin', 'player']:
|
||||
raise HTTPException(status_code=400, detail="Invalid user_type. Must be 'admin' or 'player'.")
|
||||
updates.append("user_type = %s")
|
||||
params.append(user_type)
|
||||
|
||||
if not updates:
|
||||
raise HTTPException(status_code=400, detail="No fields to update.")
|
||||
|
||||
params.append(uid)
|
||||
update_query = f"UPDATE users SET {', '.join(updates)} WHERE user_id = %s;"
|
||||
|
||||
# 5. 执行更新操作
|
||||
cursor.execute(update_query, tuple(params))
|
||||
connection.commit()
|
||||
|
||||
return f"User with ID {uid} updated successfully."
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error during update: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def get_total_of_users(token: str):
|
||||
"""
|
||||
获取用户总数
|
||||
1. 验证 token 确保为管理员。
|
||||
2. 查询总人数
|
||||
"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token) # 如果验证失败会抛出异常
|
||||
admin_username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (admin_username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS user_count FROM users")
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Database connection failed!")
|
||||
return result["user_count"]
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def query_user(token: str, query_mode: str, query_value: str):
|
||||
"""
|
||||
通过 `phone_number` / `email` / `username` / `uid` 进行模糊查询
|
||||
"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# 2. 确认管理员身份
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
# 3. 根据查询模式构建 SQL
|
||||
valid_modes = {
|
||||
"phone_number": "phone_number",
|
||||
"email": "email",
|
||||
"username": "username",
|
||||
"uid": "user_id"
|
||||
}
|
||||
|
||||
if query_mode not in valid_modes:
|
||||
raise HTTPException(status_code=400, detail="Invalid query mode.")
|
||||
|
||||
search_column = valid_modes[query_mode]
|
||||
|
||||
# `uid` 应该是精确匹配,而不是模糊查询
|
||||
if query_mode == "uid":
|
||||
query = f"""
|
||||
SELECT user_id, username, points, phone_number, gender, email, user_type, created_at, updated_at
|
||||
FROM users
|
||||
WHERE {search_column} = %s
|
||||
"""
|
||||
cursor.execute(query, (query_value,))
|
||||
else:
|
||||
# 其余模式使用 `LIKE` 进行模糊查询
|
||||
query = f"""
|
||||
SELECT user_id, username, points, phone_number, gender, email, user_type, created_at, updated_at
|
||||
FROM users
|
||||
WHERE {search_column} LIKE %s
|
||||
"""
|
||||
search_value = f"%{query_value}%" # 模糊匹配
|
||||
cursor.execute(query, (search_value,))
|
||||
|
||||
users = cursor.fetchall()
|
||||
|
||||
if not users:
|
||||
raise HTTPException(status_code=404, detail="User not found.")
|
||||
|
||||
return users
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error during query: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def update_user_password(token: str, uid: int, new_password: str):
|
||||
"""
|
||||
管理员修改用户密码:
|
||||
1. 验证 token 确保为管理员。
|
||||
2. 确认用户存在。
|
||||
3. 更新用户密码。
|
||||
"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token) # 如果验证失败会抛出异常
|
||||
admin_username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# 2. 确认管理员权限
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (admin_username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
|
||||
# 3. 检查用户是否存在
|
||||
cursor.execute("SELECT * FROM users WHERE user_id = %s;", (uid,))
|
||||
user_to_update = cursor.fetchone()
|
||||
|
||||
if not user_to_update:
|
||||
raise HTTPException(status_code=404, detail=f"User with ID {uid} not found.")
|
||||
|
||||
# 4. 更新用户密码
|
||||
hashed_password = hashlib.md5(new_password.encode()).hexdigest()
|
||||
cursor.execute("UPDATE users SET password = %s WHERE user_id = %s;", (hashed_password, uid))
|
||||
connection.commit()
|
||||
|
||||
return f"User with ID {uid} password updated successfully."
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error during password update: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
# 在适当位置添加积分更新方法
|
||||
def update_user_points(token: str, uid: int, points: int, reason: str):
|
||||
"""更新用户积分"""
|
||||
# 1. 验证 token
|
||||
try:
|
||||
payload = verify_token(token) # 如果验证失败会抛出异常
|
||||
admin_username = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
# 2. 确认管理员权限
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_type FROM users WHERE username = %s;", (admin_username,))
|
||||
admin_user = cursor.fetchone()
|
||||
|
||||
if not admin_user:
|
||||
raise HTTPException(status_code=401, detail="Admin user not found.")
|
||||
if admin_user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
# 更新积分
|
||||
cursor.execute("UPDATE users SET points = points + %s WHERE user_id = %s",
|
||||
(points, uid))
|
||||
# 记录积分变动历史
|
||||
cursor.execute(
|
||||
"INSERT INTO points_history (user_id, change_amount, reason) VALUES (%s, %s, %s)",
|
||||
(uid, points, reason)
|
||||
)
|
||||
connection.commit()
|
||||
return f"用户 {uid} 积分已更新"
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error during password update: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
42
backend/app/services/auth_service.py
Normal file
42
backend/app/services/auth_service.py
Normal file
@ -0,0 +1,42 @@
|
||||
from fastapi import HTTPException
|
||||
from ..utils.jwt_handler import create_token
|
||||
from ..db import get_connection
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
def authenticate_admin(username: str, password: str):
|
||||
"""
|
||||
验证管理员身份
|
||||
"""
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM users WHERE username = %s;", (username,))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password.")
|
||||
|
||||
# 验证密码
|
||||
if user["password"] != hashlib.md5(password.encode()).hexdigest():
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password.")
|
||||
|
||||
# 检查用户类型是否为 admin
|
||||
if user["user_type"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Permission denied: Not an admin user.")
|
||||
|
||||
return user
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def generate_login_token(username: str, remember_me: bool):
|
||||
"""
|
||||
生成登录 Token
|
||||
"""
|
||||
expires_delta = datetime.timedelta(days=7 if remember_me else 1)
|
||||
token = create_token({"sub": username}, expires_delta)
|
||||
return token, int(expires_delta.total_seconds())
|
||||
28
backend/app/services/coupons.py
Normal file
28
backend/app/services/coupons.py
Normal file
@ -0,0 +1,28 @@
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
import hashlib
|
||||
|
||||
def apply_coupons(base_price: float, coupon_id: int, duration: float) -> float:
|
||||
if not coupon_id:
|
||||
return base_price
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise HTTPException(status_code=500, detail="Database connection failed!")
|
||||
|
||||
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT discount_type, discount_amount
|
||||
FROM coupons
|
||||
WHERE coupon_id = %s
|
||||
""", (coupon_id,))
|
||||
coupon = cursor.fetchone()
|
||||
|
||||
if coupon['discount_type'] == 'percentage':
|
||||
return base_price * (1 - coupon['discount_amount']/100)
|
||||
elif coupon['discount_type'] == 'fixed':
|
||||
return max(base_price - coupon['discount_amount'], 0)
|
||||
elif coupon['discount_type'] == 'time':
|
||||
return base_price * (duration - coupon['discount_amount'])/duration
|
||||
return base_price
|
||||
114
backend/app/services/user_coupon_service.py
Normal file
114
backend/app/services/user_coupon_service.py
Normal file
@ -0,0 +1,114 @@
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
def get_user_coupons_service(token: str):
|
||||
"""获取用户所有优惠券"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
# 新增:通过phone_number查询用户ID
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
user_id = user['user_id']
|
||||
|
||||
# 获取有效期内且未被使用的优惠券
|
||||
cursor.execute("""
|
||||
SELECT c.*
|
||||
FROM user_coupons uc
|
||||
JOIN coupons c ON uc.coupon_id = c.coupon_id
|
||||
WHERE uc.user_id = %s
|
||||
AND c.valid_from <= NOW()
|
||||
AND c.valid_to >= NOW()
|
||||
""", (user_id,))
|
||||
|
||||
coupons = []
|
||||
for coupon in cursor.fetchall():
|
||||
# 生成描述文字
|
||||
if coupon['discount_type'] == 'fixed':
|
||||
desc = f"满{coupon['min_order_amount'] or 0}元,减{coupon['discount_amount']}元"
|
||||
else:
|
||||
# 转换百分比折扣为中文(如85 -> 八五折)
|
||||
discount = int(coupon['discount_amount'])
|
||||
chinese_num = ''.join(['〇一二三四五六七八九'[int(d)] for d in str(discount)])
|
||||
desc = f"满{coupon['min_order_amount'] or 0}元,打{chinese_num}折"
|
||||
|
||||
coupons.append({
|
||||
"coupon_id": coupon['coupon_id'],
|
||||
"description": desc
|
||||
})
|
||||
|
||||
return coupons
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
def apply_coupon_service(token: str, coupon_id: int, order_id: float):
|
||||
"""使用优惠券计算价格"""
|
||||
try:
|
||||
|
||||
payload = verify_token(token)
|
||||
# 新增:通过phone_number查询用户ID
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (payload["sub"],))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
user_id = user['user_id']
|
||||
|
||||
# 验证优惠券归属
|
||||
cursor.execute("""
|
||||
SELECT c.*
|
||||
FROM user_coupons uc
|
||||
JOIN coupons c ON uc.coupon_id = c.coupon_id
|
||||
WHERE uc.user_id = %s
|
||||
AND uc.coupon_id = %s
|
||||
AND c.valid_from <= NOW()
|
||||
AND c.valid_to >= NOW()
|
||||
LIMIT 1
|
||||
""", (user_id, coupon_id))
|
||||
|
||||
coupon = cursor.fetchone()
|
||||
if not coupon:
|
||||
raise HTTPException(status_code=400, detail="无效优惠券")
|
||||
# print(coupon)
|
||||
discount_amount = float(coupon['discount_amount'])
|
||||
min_order_amount = float(coupon['min_order_amount']) if coupon['min_order_amount'] else None
|
||||
|
||||
# discount_amount = 0.0
|
||||
# min_order_amount = 0.0
|
||||
|
||||
|
||||
cursor.execute("""
|
||||
SELECT payable_price
|
||||
FROM orders
|
||||
WHERE order_id = %s
|
||||
AND user_id = %s
|
||||
AND order_status = 'pending'
|
||||
""", (order_id, user['user_id']))
|
||||
order = cursor.fetchone()
|
||||
|
||||
price = float(order['payable_price'])
|
||||
|
||||
# 检查最低消费
|
||||
if coupon['min_order_amount'] and price < min_order_amount:
|
||||
raise HTTPException(status_code=400, detail="未达到最低消费金额")
|
||||
# 计算折扣
|
||||
if coupon['discount_type'] == 'fixed':
|
||||
final_price = max(price - discount_amount, 0)
|
||||
else:
|
||||
discount = price * (discount_amount / 100)
|
||||
final_price = max(discount, 0)
|
||||
|
||||
return {"final_price": round(final_price, 2)}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
124
backend/app/services/user_getInfo_service.py
Normal file
124
backend/app/services/user_getInfo_service.py
Normal file
@ -0,0 +1,124 @@
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
|
||||
# 基础信息模型
|
||||
class UserBasicInfo(BaseModel):
|
||||
username: str
|
||||
phone_number: str
|
||||
points: int
|
||||
|
||||
# 订单模型
|
||||
class OrderHistory(BaseModel):
|
||||
order_id: int
|
||||
order_date: datetime
|
||||
start_time: datetime
|
||||
end_time: datetime | None = None # 允许为空
|
||||
payable_amount: float | None = None # 允许空值
|
||||
paid_amount: float | None = None # 允许为空
|
||||
game_table_number: str
|
||||
order_status: str # 新增状态字段
|
||||
|
||||
# 积分变动模型
|
||||
class PointsHistory(BaseModel):
|
||||
change_time: datetime
|
||||
change_amount: int
|
||||
reason: str
|
||||
|
||||
def _get_user_info(token: str):
|
||||
"""公共方法:验证token并获取用户完整信息"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
print(f"Verified phone_number: {phone_number}")
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, str(e))
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT username, phone_number, points
|
||||
FROM users
|
||||
WHERE phone_number = %s
|
||||
""", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(404, "用户不存在")
|
||||
return user # 返回完整的用户记录
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
def get_basic_info(token: str):
|
||||
"""获取基础信息"""
|
||||
user = _get_user_info(token)
|
||||
|
||||
# 脱敏处理
|
||||
phone = user['phone_number']
|
||||
masked_phone = phone[:3] + "****" + phone[-4:] if phone else ""
|
||||
|
||||
return UserBasicInfo(
|
||||
username=user['username'], # 从数据库查询结果获取
|
||||
phone_number=masked_phone,
|
||||
points=user['points'] # 从数据库查询结果获取
|
||||
)
|
||||
|
||||
|
||||
def get_order_history(token: str):
|
||||
"""获取订单历史"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
print(f"Verified phone_number: {phone_number}")
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, str(e))
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
o.order_id,
|
||||
o.order_date,
|
||||
o.start_datetime AS start_time,
|
||||
o.end_datetime AS end_time,
|
||||
o.payable_price AS payable_amount,
|
||||
o.paid_price AS paid_amount,
|
||||
g.game_table_number,
|
||||
o.order_status # 新增状态字段
|
||||
FROM orders o
|
||||
LEFT JOIN game_tables g ON o.game_table_id = g.table_id
|
||||
WHERE o.user_id = (SELECT user_id FROM users WHERE phone_number = %s) AND o.order_status= 'completed'
|
||||
ORDER BY o.order_date DESC
|
||||
""", (phone_number,))
|
||||
return [OrderHistory(**row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
def get_points_history(token: str):
|
||||
"""获取积分变动"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
print(f"Verified phone_number: {phone_number}")
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, str(e))
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT created_at as change_time, change_amount, reason
|
||||
FROM points_history
|
||||
WHERE user_id = (SELECT user_id FROM users WHERE phone_number = %s) # 改为phone_number
|
||||
""", (phone_number,)) # 参数改为phone_number
|
||||
return [PointsHistory(**row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
221
backend/app/services/user_login_service.py
Normal file
221
backend/app/services/user_login_service.py
Normal file
@ -0,0 +1,221 @@
|
||||
from fastapi import HTTPException
|
||||
from ..db import get_connection
|
||||
import hashlib
|
||||
import random
|
||||
from ..utils.sms_sender import send_sms # 需要实现阿里云短信发送
|
||||
import redis
|
||||
from configparser import ConfigParser
|
||||
import os
|
||||
redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
||||
from ..schemas.user_auth import (
|
||||
UserLoginRequest,
|
||||
CodeLoginRequest,
|
||||
ResetPasswordRequest,
|
||||
SendCodeRequest,
|
||||
CheckUserExistenceRequest
|
||||
)
|
||||
|
||||
async def authenticate_user(phone_number: str, password: str):
|
||||
"""手机号密码登录验证"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM users WHERE phone_number = %s", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(404, "用户不存在")
|
||||
if user["password"] != hashlib.md5(password.encode()).hexdigest():
|
||||
raise HTTPException(401, "密码错误")
|
||||
return user
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
async def send_verification_code(phone_number: str):
|
||||
"""发送4位验证码"""
|
||||
code = f"{random.randint(0,9999):04}"
|
||||
print(code)
|
||||
# 存储验证码到Redis(需要配置Redis连接)
|
||||
redis_client.setex(f"sms_code:{phone_number}", 300, code)
|
||||
|
||||
# 调用阿里云短信接口
|
||||
await send_sms(
|
||||
phone_number=phone_number,
|
||||
template_code="SMS_480005109", # 实际模板ID
|
||||
template_param={"code": code}
|
||||
)
|
||||
return {"message": "验证码已发送"}
|
||||
|
||||
async def verify_code_login(phone_number: str, code: str):
|
||||
"""验证码登录"""
|
||||
stored_code = redis_client.get(f"sms_code:{phone_number}")
|
||||
if stored_code:
|
||||
stored_code = stored_code.decode('utf-8')
|
||||
if not stored_code or stored_code != code:
|
||||
raise HTTPException(400, "验证码错误")
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM users WHERE phone_number = %s", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(404, "用户不存在")
|
||||
return user
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
async def reset_password(request: ResetPasswordRequest):
|
||||
"""重置密码"""
|
||||
# 先验证验证码
|
||||
await verify_code_login(request.phone_number, request.code)
|
||||
|
||||
# 更新密码
|
||||
hashed_pwd = hashlib.md5(request.new_password.encode()).hexdigest()
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE users SET password = %s WHERE phone_number = %s",
|
||||
(hashed_pwd, request.phone_number))
|
||||
conn.commit()
|
||||
return {"message": "密码重置成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
async def register_user(phone_number: str, code: str, username: str, password: str):
|
||||
"""注册新用户"""
|
||||
# 先验证验证码
|
||||
stored_code = redis_client.get(f"sms_code:{phone_number}")
|
||||
if not stored_code or stored_code.decode() != code:
|
||||
raise HTTPException(400, "验证码错误")
|
||||
|
||||
# 检查手机号是否已注册
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM users WHERE phone_number = %s", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
if user:
|
||||
raise HTTPException(400, "该手机号已注册")
|
||||
|
||||
# 检查用户名是否已存在
|
||||
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
||||
user = cursor.fetchone()
|
||||
if user:
|
||||
raise HTTPException(400, "该用户名已被使用")
|
||||
|
||||
# 注册新用户
|
||||
hashed_pwd = hashlib.md5(password.encode()).hexdigest()
|
||||
cursor.execute("INSERT INTO users (phone_number, username, password) VALUES (%s, %s, %s)",
|
||||
(phone_number, username, hashed_pwd))
|
||||
conn.commit()
|
||||
return {"message": "注册成功"}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
async def check_user_existence(phone_number: str):
|
||||
"""检查用户是否存在"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT user_id FROM users WHERE phone_number = %s",
|
||||
(phone_number,)
|
||||
)
|
||||
exists = cursor.fetchone() is not None
|
||||
return {"exists": exists}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"查询失败: {str(e)}")
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
async def check_wx_bind_status(token: str):
|
||||
"""检查微信绑定状态"""
|
||||
from ..utils.jwt_handler import verify_token
|
||||
|
||||
try:
|
||||
# 验证JWT令牌
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
|
||||
# 获取数据库连接
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 查询微信OpenID
|
||||
cursor.execute(
|
||||
"SELECT wx_openid FROM users WHERE phone_number = %s",
|
||||
(phone_number,)
|
||||
)
|
||||
user = cursor.fetchone()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(404, "用户不存在")
|
||||
|
||||
return {"is_binded": user['wx_openid'] is not None}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
async def bind_wechat_openid(token: str, code: str):
|
||||
"""通过微信code绑定openid"""
|
||||
from ..utils.jwt_handler import verify_token
|
||||
import requests
|
||||
|
||||
try:
|
||||
# 验证JWT令牌
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
|
||||
config = ConfigParser()
|
||||
config.read(os.path.join(os.path.dirname(__file__), '../../config.conf'))
|
||||
|
||||
# 从微信获取openid(需要配置微信应用信息)
|
||||
wechat_url = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
params = {
|
||||
"appid": config.get("wechat", "appid"),
|
||||
"secret": config.get("wechat", "secret"),
|
||||
"js_code": code,
|
||||
"grant_type": "authorization_code"
|
||||
}
|
||||
|
||||
response = requests.get(wechat_url, params=params)
|
||||
wechat_data = response.json()
|
||||
|
||||
if 'openid' not in wechat_data:
|
||||
raise HTTPException(400, "微信授权失败")
|
||||
|
||||
openid = wechat_data['openid']
|
||||
|
||||
# 存储到数据库
|
||||
connection = get_connection()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE users SET wx_openid = %s WHERE phone_number = %s",
|
||||
(openid, phone_number)
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
return {"message": "微信绑定成功"}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, str(e))
|
||||
except requests.RequestException:
|
||||
raise HTTPException(503, "无法连接微信服务器")
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
raise HTTPException(500, str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
327
backend/app/services/user_order_service.py
Normal file
327
backend/app/services/user_order_service.py
Normal file
@ -0,0 +1,327 @@
|
||||
from fastapi import HTTPException
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from ..db import get_connection
|
||||
from ..utils.jwt_handler import verify_token
|
||||
import configparser
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read('backend/config.conf')
|
||||
unit_price = Decimal(config.get('price', 'unit_price'))
|
||||
points_rate = Decimal(config.get('price', 'points_rate'))
|
||||
get_points_rate = Decimal(config.get('price', 'get_points_rate'))
|
||||
|
||||
def get_user_active_order(token: str):
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
print(f"Verified phone_number: {phone_number}")
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, str(e))
|
||||
"""获取用户进行中订单"""
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
o.order_id,
|
||||
o.start_datetime,
|
||||
gt.game_table_number,
|
||||
o.num_players
|
||||
FROM users u
|
||||
INNER JOIN orders o ON u.user_id = o.user_id
|
||||
INNER JOIN game_tables gt ON o.game_table_id = gt.table_id
|
||||
WHERE
|
||||
u.phone_number = %s
|
||||
AND o.order_status = 'in_progress'
|
||||
ORDER BY o.start_datetime DESC
|
||||
LIMIT 1;
|
||||
""", (phone_number,))
|
||||
return cursor.fetchone() or {}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
def create_user_order(token: str, table_id: int, num_players: int):
|
||||
"""创建用户订单"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor = connection.cursor()
|
||||
# 新增用户订单状态检查
|
||||
cursor.execute("""
|
||||
SELECT order_status
|
||||
FROM orders
|
||||
WHERE user_id = (SELECT user_id FROM users WHERE phone_number = %s)
|
||||
AND order_status IN ('in_progress', 'pending')
|
||||
LIMIT 1
|
||||
""", (phone_number,))
|
||||
existing_order = cursor.fetchone()
|
||||
if existing_order:
|
||||
status = existing_order[0]
|
||||
if status == 'in_progress':
|
||||
raise HTTPException(status_code=400, detail="存在进行中的订单")
|
||||
raise HTTPException(status_code=400, detail="存在未付款的订单")
|
||||
|
||||
# 获取当前最大订单号
|
||||
cursor.execute("SELECT MAX(order_id) FROM orders")
|
||||
max_order_id = cursor.fetchone()[0]
|
||||
|
||||
# 生成订单号(从100开始)
|
||||
order_id = 100 if max_order_id is None else int(max_order_id) + 1
|
||||
print("order_id:", order_id)
|
||||
# 验证用户存在
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查桌子占用
|
||||
cursor.execute("""
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM orders
|
||||
WHERE game_table_id = %s
|
||||
AND order_status = 'in_progress'
|
||||
)""", (table_id,))
|
||||
if cursor.fetchone()[0]:
|
||||
raise HTTPException(status_code=400, detail="桌子已被占用")
|
||||
|
||||
# 创建订单
|
||||
order_date = datetime.now().date()
|
||||
start_datetime = datetime.now()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO orders (
|
||||
order_id, user_id, game_table_id,
|
||||
order_date, start_datetime, num_players,
|
||||
order_status
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""", (order_id, user[0], table_id, order_date, start_datetime, num_players, 'in_progress'))
|
||||
|
||||
connection.commit()
|
||||
return {"order_id": order_id, "message": "订单创建成功"}
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
def complete_user_order(token: str, order_id: int):
|
||||
"""完成用户订单"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
# 修改后的SQL查询
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
o.user_id,
|
||||
t.price AS table_price,
|
||||
COALESCE(g.price, 1.0) AS game_price,
|
||||
o.num_players,
|
||||
o.start_datetime
|
||||
FROM orders o
|
||||
JOIN game_tables t ON o.game_table_id = t.table_id
|
||||
LEFT JOIN games g ON o.game_id = g.game_id
|
||||
WHERE o.order_id = %s
|
||||
FOR UPDATE
|
||||
""", (order_id,))
|
||||
order = cursor.fetchone()
|
||||
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="订单不存在")
|
||||
|
||||
# 验证用户权限
|
||||
cursor.execute("SELECT user_id FROM users WHERE phone_number = %s", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
if order['user_id'] != user['user_id']:
|
||||
raise HTTPException(status_code=403, detail="无权操作此订单")
|
||||
|
||||
# 计算时长(优化精度处理)
|
||||
end_datetime = datetime.now()
|
||||
duration = end_datetime - order['start_datetime']
|
||||
duration_minutes = duration.total_seconds() / 60
|
||||
duration_minutes_dec = Decimal(duration_minutes).quantize(Decimal('0.0000'))
|
||||
|
||||
# 直接使用Decimal类型计算
|
||||
unit_price = Decimal('0.1667') # 示例单位价格(需按实际调整)
|
||||
base_price = (
|
||||
order['table_price'] *
|
||||
order['game_price'] *
|
||||
order['num_players'] *
|
||||
duration_minutes_dec *
|
||||
unit_price
|
||||
)
|
||||
total_price = round(base_price, 2)
|
||||
|
||||
# 使用Decimal类型更新数据库
|
||||
cursor.execute("""
|
||||
UPDATE orders SET
|
||||
end_datetime = %s,
|
||||
payable_price = %s,
|
||||
game_process_time = %s,
|
||||
order_status = 'pending'
|
||||
WHERE order_id = %s
|
||||
""", (end_datetime, total_price, duration_minutes_dec, order_id))
|
||||
|
||||
connection.commit()
|
||||
return {"message": "订单已结束待结算", "base_price": float(base_price)}
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
|
||||
def get_earliest_pending_order(token: str):
|
||||
"""获取用户最早的pending订单"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor(dictionary=True) as cursor:
|
||||
# 查询最早pending订单
|
||||
cursor.execute("""
|
||||
SELECT o.order_id
|
||||
FROM users u
|
||||
JOIN orders o ON u.user_id = o.user_id
|
||||
WHERE u.phone_number = %s
|
||||
AND o.order_status = 'pending'
|
||||
ORDER BY o.start_datetime ASC
|
||||
LIMIT 1
|
||||
""", (phone_number,))
|
||||
result = cursor.fetchone()
|
||||
return {"order_id": result["order_id"]} if result else {}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, detail=str(e))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def get_order_detail_with_points(token: str, order_id: int):
|
||||
"""获取订单详情及用户积分"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor(dictionary=True) as cursor:
|
||||
# 验证订单归属
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
o.order_id,
|
||||
gt.game_table_number,
|
||||
o.order_date,
|
||||
o.start_datetime AS start_time,
|
||||
o.end_datetime AS end_time,
|
||||
o.game_process_time,
|
||||
o.num_players,
|
||||
o.payable_price,
|
||||
u.points
|
||||
FROM orders o
|
||||
JOIN game_tables gt ON o.game_table_id = gt.table_id
|
||||
JOIN users u ON o.user_id = u.user_id
|
||||
WHERE o.order_id = %s
|
||||
AND u.phone_number = %s
|
||||
""", (order_id, phone_number))
|
||||
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(404, detail="订单不存在或无权访问")
|
||||
|
||||
# 格式化日期字段
|
||||
result['order_date'] = result['order_date'].isoformat()
|
||||
result['start_time'] = result['start_time'].isoformat()
|
||||
result['end_time'] = result['end_time'].isoformat() if result['end_time'] else None
|
||||
|
||||
return {
|
||||
"order_id": result["order_id"],
|
||||
"game_table_number": result["game_table_number"],
|
||||
"order_date": result["order_date"],
|
||||
"start_time": result["start_time"],
|
||||
"end_time": result["end_time"],
|
||||
"num_players": result["num_players"],
|
||||
"payable_price": float(result["payable_price"]),
|
||||
"game_process_time": result["game_process_time"],
|
||||
"points": result["points"]
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, detail=str(e))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def preview_price_adjustment(token: str, order_id: int, used_points: int):
|
||||
"""预览价格调整(不修改数据库)"""
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
phone_number = payload["sub"]
|
||||
except ValueError as e:
|
||||
raise HTTPException(401, detail=str(e))
|
||||
|
||||
connection = get_connection()
|
||||
try:
|
||||
with connection.cursor(dictionary=True) as cursor:
|
||||
# 获取用户当前积分
|
||||
cursor.execute("""
|
||||
SELECT u.user_id, u.points
|
||||
FROM users u
|
||||
WHERE u.phone_number = %s
|
||||
""", (phone_number,))
|
||||
user = cursor.fetchone()
|
||||
if not user:
|
||||
raise HTTPException(404, "用户不存在")
|
||||
|
||||
if user['points'] < used_points:
|
||||
raise HTTPException(400, "积分不足")
|
||||
|
||||
# 获取原始价格
|
||||
cursor.execute("""
|
||||
SELECT payable_price
|
||||
FROM orders
|
||||
WHERE order_id = %s
|
||||
AND user_id = %s
|
||||
AND order_status = 'pending'
|
||||
""", (order_id, user['user_id']))
|
||||
order = cursor.fetchone()
|
||||
if not order:
|
||||
raise HTTPException(404, "订单不存在或不可修改")
|
||||
|
||||
# 计算新价格(仅预览)
|
||||
points_value = used_points * points_rate
|
||||
new_price = max(order['payable_price'] - points_value, Decimal('0'))
|
||||
|
||||
return {
|
||||
"new_price": float(new_price),
|
||||
}
|
||||
|
||||
except HTTPException as e:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"计算失败: {str(e)}")
|
||||
finally:
|
||||
connection.close()
|
||||
82
backend/app/services/user_table_service.py
Normal file
82
backend/app/services/user_table_service.py
Normal file
@ -0,0 +1,82 @@
|
||||
from fastapi import HTTPException
|
||||
import mysql.connector
|
||||
from ..db import get_connection
|
||||
|
||||
def get_table_availability_service():
|
||||
"""获取桌子使用情况统计"""
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
# 统计总桌数和被占用的桌子数量
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) AS total_tables,
|
||||
SUM(CASE WHEN o.order_id IS NOT NULL THEN 1 ELSE 0 END) AS occupied_tables
|
||||
FROM game_tables gt
|
||||
LEFT JOIN orders o ON gt.table_id = o.game_table_id
|
||||
AND o.order_status = 'in_progress'
|
||||
""")
|
||||
return cursor.fetchone()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
|
||||
def check_table_occupancy_service(table_id: int):
|
||||
"""检查桌子是否被占用"""
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM orders
|
||||
WHERE game_table_id = %s
|
||||
AND order_status = 'in_progress'
|
||||
) AS is_occupied
|
||||
""", (table_id,))
|
||||
result = cursor.fetchone()
|
||||
return {"is_occupied": bool(result['is_occupied'])}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
|
||||
def list_tables_service():
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM game_tables")
|
||||
return cursor.fetchall()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
|
||||
def get_table_number_service(table_id: int):
|
||||
"""根据桌台ID获取桌号服务"""
|
||||
connection = get_connection()
|
||||
cursor = None
|
||||
try:
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor.execute("""
|
||||
SELECT game_table_number
|
||||
FROM game_tables
|
||||
WHERE table_id = %s
|
||||
""", (table_id,))
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="桌台不存在")
|
||||
return {"game_table_number": result['game_table_number']}
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
finally:
|
||||
if cursor: cursor.close()
|
||||
connection.close()
|
||||
264
backend/app/utils/create_database.py
Normal file
264
backend/app/utils/create_database.py
Normal file
@ -0,0 +1,264 @@
|
||||
|
||||
|
||||
import mysql.connector
|
||||
from mysql.connector import errorcode
|
||||
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': '123456', # 替换为你的 MySQL 密码
|
||||
'port': 3306
|
||||
}
|
||||
|
||||
|
||||
DATABASE_NAME = 'tgst01'
|
||||
|
||||
|
||||
SQL_SCRIPT = """
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS `coupons`;
|
||||
CREATE TABLE `coupons` (
|
||||
`coupon_id` int NOT NULL AUTO_INCREMENT,
|
||||
`coupon_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`discount_type` enum('fixed','percentage') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`discount_amount` decimal(10,2) DEFAULT NULL,
|
||||
`min_order_amount` decimal(10,2) DEFAULT NULL,
|
||||
`valid_from` date DEFAULT NULL,
|
||||
`valid_to` date DEFAULT NULL,
|
||||
`is_active` tinyint(1) DEFAULT '1',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`coupon_id`),
|
||||
UNIQUE KEY `coupon_code` (`coupon_code`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `game_game_tags`;
|
||||
CREATE TABLE `game_game_tags` (
|
||||
`game_tag_relation_id` int NOT NULL AUTO_INCREMENT,
|
||||
`game_id` int DEFAULT NULL,
|
||||
`tag_id` int DEFAULT NULL,
|
||||
PRIMARY KEY (`game_tag_relation_id`),
|
||||
KEY `game_id` (`game_id`),
|
||||
KEY `tag_id` (`tag_id`),
|
||||
CONSTRAINT `game_game_tags_ibfk_1` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`),
|
||||
CONSTRAINT `game_game_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `game_tags` (`tag_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
CREATE TABLE `game_groups` (
|
||||
`group_id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int DEFAULT NULL,
|
||||
`group_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
||||
`start_date` date DEFAULT NULL,
|
||||
`start_time` datetime DEFAULT NULL,
|
||||
`end_time` datetime DEFAULT NULL,
|
||||
`max_members` int DEFAULT NULL,
|
||||
`group_status` enum('recruiting','full','completed','cancelled','pause') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT 'recruiting',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
CREATE TABLE `game_tables` (
|
||||
`table_id` int NOT NULL AUTO_INCREMENT COMMENT '服务器桌号',
|
||||
`game_table_number` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '物理桌号',
|
||||
`capacity` int NOT NULL COMMENT '承载人数',
|
||||
`price` decimal(10,2) NOT NULL COMMENT '价格倍率',
|
||||
PRIMARY KEY (`table_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
CREATE TABLE `announcement` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`text` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`start_time` datetime NOT NULL,
|
||||
`end_time` datetime NOT NULL,
|
||||
`color` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
CREATE TABLE `user_coupons` (
|
||||
`user_coupon_id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`coupon_id` int NOT NULL,
|
||||
`obtained_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`used_at` timestamp NULL DEFAULT NULL,
|
||||
`is_used` tinyint(1) DEFAULT '0',
|
||||
`valid_from` date DEFAULT NULL,
|
||||
`valid_to` date DEFAULT NULL,
|
||||
PRIMARY KEY (`user_coupon_id`),
|
||||
UNIQUE KEY `unique_user_coupon` (`user_id`,`coupon_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`coupon_id`) REFERENCES `coupons` (`coupon_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `game_tags`;
|
||||
CREATE TABLE `game_tags` (
|
||||
`tag_id` int NOT NULL AUTO_INCREMENT,
|
||||
`tag_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
PRIMARY KEY (`tag_id`),
|
||||
UNIQUE KEY `tag_name` (`tag_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
CREATE TABLE `games` (
|
||||
`game_id` int NOT NULL AUTO_INCREMENT,
|
||||
`game_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`game_type` int NOT NULL,
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
||||
`min_players` int DEFAULT NULL,
|
||||
`max_players` int DEFAULT NULL,
|
||||
`duration` int DEFAULT NULL,
|
||||
`price` decimal(10,2) DEFAULT NULL,
|
||||
`difficulty_level` int DEFAULT NULL,
|
||||
`is_available` tinyint(1) DEFAULT '1',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`quantity` int DEFAULT NULL,
|
||||
`photo_url` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
PRIMARY KEY (`game_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `group_members`;
|
||||
CREATE TABLE `group_members` (
|
||||
`group_member_id` int NOT NULL AUTO_INCREMENT,
|
||||
`group_id` int DEFAULT NULL,
|
||||
`user_id` int DEFAULT NULL,
|
||||
`join_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`entry_status` int DEFAULT '1',
|
||||
PRIMARY KEY (`group_member_id`),
|
||||
KEY `group_id` (`group_id`),
|
||||
CONSTRAINT `group_members_ibfk_1` FOREIGN KEY (`group_id`) REFERENCES `game_groups` (`group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `order_coupons`;
|
||||
CREATE TABLE `order_coupons` (
|
||||
`order_coupon_id` int NOT NULL AUTO_INCREMENT,
|
||||
`order_id` int DEFAULT NULL,
|
||||
`coupon_id` int DEFAULT NULL,
|
||||
PRIMARY KEY (`order_coupon_id`),
|
||||
KEY `order_id` (`order_id`),
|
||||
KEY `coupon_id` (`coupon_id`),
|
||||
CONSTRAINT `order_coupons_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`order_id`),
|
||||
CONSTRAINT `order_coupons_ibfk_2` FOREIGN KEY (`coupon_id`) REFERENCES `coupons` (`coupon_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `orders`;
|
||||
CREATE TABLE `orders` (
|
||||
`order_id` int NOT NULL COMMENT '订单 id',
|
||||
`user_id` int NOT NULL COMMENT '下单用户 id',
|
||||
`game_table_id` int NOT NULL COMMENT '占用游戏桌 id',
|
||||
`game_id` int DEFAULT NULL COMMENT '使用游戏 id',
|
||||
`order_date` date NOT NULL COMMENT '下单时间',
|
||||
`start_datetime` datetime NOT NULL COMMENT '订单开始时间',
|
||||
`end_datetime` datetime DEFAULT NULL COMMENT '订单结束时间',
|
||||
`num_players` int NOT NULL COMMENT '游玩人数',
|
||||
`payable_price` decimal(10,2) DEFAULT NULL COMMENT '未优惠价格',
|
||||
`order_status` enum('pending','paid','in_progress','completed','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT 'pending' COMMENT '订单状态',
|
||||
`payment_method` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '优惠后价格',
|
||||
`coupon_id` int DEFAULT NULL COMMENT '优惠卷 id',
|
||||
`used_points` int DEFAULT NULL COMMENT '使用积分',
|
||||
`paid_price` decimal(10,2) DEFAULT '0.00' COMMENT '优惠后价格',
|
||||
`game_process_time` int DEFAULT NULL COMMENT '游戏时长',
|
||||
`settlement_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`order_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `game_table_id` (`game_table_id`),
|
||||
KEY `game_id` (`game_id`),
|
||||
KEY `coupon_id` (`coupon_id`),
|
||||
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`),
|
||||
CONSTRAINT `orders_ibfk_2` FOREIGN KEY (`game_table_id`) REFERENCES `game_tables` (`table_id`),
|
||||
CONSTRAINT `orders_ibfk_3` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `points_history`;
|
||||
CREATE TABLE `points_history` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`change_amount` int NOT NULL,
|
||||
`reason` varchar(255) COLLATE utf8mb4_bin NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `reviews`;
|
||||
CREATE TABLE `reviews` (
|
||||
`review_id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int DEFAULT NULL,
|
||||
`game_id` int DEFAULT NULL,
|
||||
`rating` int DEFAULT NULL,
|
||||
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`review_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `game_id` (`game_id`),
|
||||
CONSTRAINT `reviews_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`),
|
||||
CONSTRAINT `reviews_ibfk_2` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users` (
|
||||
`user_id` int NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`phone_number` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`user_type` enum('player','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT 'player',
|
||||
`gender` enum('male','female','other') CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`points` int DEFAULT '0',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`user_id`),
|
||||
UNIQUE KEY `phone_number` (`phone_number`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
"""
|
||||
|
||||
|
||||
def create_database_and_tables():
|
||||
try:
|
||||
# 第一步:先连到 MySQL Server(不指定数据库)
|
||||
cnx = mysql.connector.connect(**DB_CONFIG)
|
||||
cursor = cnx.cursor()
|
||||
# 创建数据库(如果不存在则创建)
|
||||
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DATABASE_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;")
|
||||
print(f"Database {DATABASE_NAME} created or already exists.")
|
||||
cursor.close()
|
||||
cnx.close()
|
||||
|
||||
# 第二步:重新连接到刚创建/已存在的数据库
|
||||
cnx = mysql.connector.connect(database=DATABASE_NAME, **DB_CONFIG)
|
||||
cursor = cnx.cursor()
|
||||
|
||||
# 由于脚本中有多条语句,需要手动分割执行
|
||||
# 简单做法:按分号分割,然后逐条执行
|
||||
# 注意如果脚本中有存储过程/触发器之类带有分号的场景,需要更复杂的处理。
|
||||
statements = SQL_SCRIPT.split(';')
|
||||
for stmt in statements:
|
||||
# 去除前后空格
|
||||
stmt = stmt.strip()
|
||||
if stmt: # 如果不为空,就执行
|
||||
cursor.execute(stmt)
|
||||
|
||||
cursor.close()
|
||||
cnx.close()
|
||||
print("All tables created successfully!")
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
|
||||
print("连接数据库失败:用户名或密码错误。")
|
||||
elif err.errno == errorcode.ER_BAD_DB_ERROR:
|
||||
print("数据库不存在,且创建失败。")
|
||||
else:
|
||||
print(err)
|
||||
except Exception as e:
|
||||
print("执行过程中出现错误:", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_database_and_tables()
|
||||
30
backend/app/utils/jwt_handler.py
Normal file
30
backend/app/utils/jwt_handler.py
Normal file
@ -0,0 +1,30 @@
|
||||
import jwt
|
||||
import datetime
|
||||
import configparser
|
||||
|
||||
# 读取配置文件
|
||||
config = configparser.ConfigParser()
|
||||
config.read('backend/config.conf')
|
||||
SECRET_KEY = config['jwt']['key']
|
||||
|
||||
def create_token(data: dict, expires_delta: datetime.timedelta):
|
||||
"""
|
||||
创建 JWT Token
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.datetime.utcnow() + expires_delta
|
||||
to_encode.update({"exp": expire})
|
||||
token = jwt.encode(to_encode, SECRET_KEY, algorithm="HS256")
|
||||
return token
|
||||
|
||||
def verify_token(token: str):
|
||||
"""
|
||||
验证 JWT Token
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise ValueError("Token has expired")
|
||||
except jwt.InvalidTokenError:
|
||||
raise ValueError("Invalid token")
|
||||
25
backend/app/utils/sms_sender.py
Normal file
25
backend/app/utils/sms_sender.py
Normal file
@ -0,0 +1,25 @@
|
||||
import asyncio
|
||||
from aliyunsdkcore.client import AcsClient
|
||||
from aliyunsdkcore.acs_exception.exceptions import ClientException
|
||||
from aliyunsdkcore.acs_exception.exceptions import ServerException
|
||||
from aliyunsdkdysmsapi.request.v20170525.SendSmsRequest import SendSmsRequest
|
||||
|
||||
client = AcsClient("LTAI5tATXL7A3zS7dmo3mfy9", "KcFlKcHUXQvyMZLltzGsmhGCNuUPQF", "cn-hangzhou")
|
||||
|
||||
async def send_sms(phone_number, template_code, template_param):
|
||||
try:
|
||||
# 创建短信发送请求
|
||||
request = SendSmsRequest()
|
||||
request.set_PhoneNumbers(phone_number)
|
||||
request.set_SignName("小鲨桌游吧")
|
||||
request.set_TemplateCode(template_code)
|
||||
request.set_TemplateParam(template_param)
|
||||
|
||||
# 执行请求
|
||||
result = await asyncio.to_thread(client.do_action_with_exception, request)
|
||||
return result
|
||||
except ClientException as e:
|
||||
print(f"ClientException: {e}")
|
||||
except ServerException as e:
|
||||
print(f"ServerException: {e}")
|
||||
return None
|
||||
8
frontend/app.py
Normal file
8
frontend/app.py
Normal file
@ -0,0 +1,8 @@
|
||||
from frontend import create_app
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
5
frontend/config.py
Normal file
5
frontend/config.py
Normal file
@ -0,0 +1,5 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')
|
||||
BASE_API_URL = os.getenv('BASE_API_URL', 'http://192.168.5.16:8000')
|
||||
29
frontend/routes/auth.py
Normal file
29
frontend/routes/auth.py
Normal file
@ -0,0 +1,29 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
import requests
|
||||
from frontend.config import Config
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
remember_me = request.form.get('remember_me') == 'on'
|
||||
|
||||
response = requests.post(f'{Config.BASE_API_URL}/admin/login/', json={"username": username, "password": password}, params={"remember_me": remember_me})
|
||||
|
||||
if response.status_code == 200:
|
||||
session['token'] = response.json().get('access_token')
|
||||
flash("登录成功", "success")
|
||||
return render_template("refresh.html")
|
||||
else:
|
||||
flash("登录失败,请检查用户名或密码", "danger")
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
flash("已退出登录", "info")
|
||||
return redirect(url_for('auth.login'))
|
||||
73
frontend/routes/coupons.py
Normal file
73
frontend/routes/coupons.py
Normal file
@ -0,0 +1,73 @@
|
||||
from flask import Blueprint, render_template, session, redirect, url_for, flash, request
|
||||
import requests
|
||||
from frontend.config import Config
|
||||
|
||||
coupons_bp = Blueprint('coupons', __name__, url_prefix='/admin/coupons')
|
||||
|
||||
@coupons_bp.route('/')
|
||||
def list_coupons():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{Config.BASE_API_URL}/admin/coupons",
|
||||
params={"token": session['token']}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return render_template('coupons/list.html', coupons=response.json())
|
||||
flash("获取优惠券列表失败", "danger")
|
||||
return render_template('coupons/list.html', coupons=[])
|
||||
except requests.exceptions.ConnectionError:
|
||||
flash("后端服务不可用", "danger")
|
||||
return render_template('coupons/list.html', coupons=[])
|
||||
|
||||
@coupons_bp.route('/create', methods=['POST'])
|
||||
def create_coupon():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
form_data = {
|
||||
"token": session['token'],
|
||||
"coupon_code": request.form['coupon_code'],
|
||||
"discount_type": request.form['discount_type'],
|
||||
"discount_amount": request.form['discount_amount'],
|
||||
"min_order_amount": request.form.get('min_order_amount'),
|
||||
"valid_from": request.form['valid_from'],
|
||||
"valid_to": request.form['valid_to'],
|
||||
"quantity": request.form['quantity']
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/coupons/create",
|
||||
json=form_data
|
||||
)
|
||||
if response.status_code == 200:
|
||||
flash("优惠券创建成功", "success")
|
||||
else:
|
||||
flash(response.json().get('detail', '创建失败'), "danger")
|
||||
except requests.exceptions.ConnectionError:
|
||||
flash("后端服务不可用", "danger")
|
||||
|
||||
return redirect(url_for('coupons.list_coupons'))
|
||||
|
||||
@coupons_bp.route('/delete/<int:coupon_id>', methods=['POST'])
|
||||
def delete_coupon(coupon_id):
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
response = requests.delete(
|
||||
f"{Config.BASE_API_URL}/admin/coupons/{coupon_id}",
|
||||
params={"token": session['token']}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
flash("优惠券删除成功", "success")
|
||||
else:
|
||||
flash(response.json().get('detail', '删除失败'), "danger")
|
||||
except requests.exceptions.ConnectionError:
|
||||
flash("后端服务不可用", "danger")
|
||||
|
||||
return redirect(url_for('coupons.list_coupons'))
|
||||
|
||||
10
frontend/routes/dashboard.py
Normal file
10
frontend/routes/dashboard.py
Normal file
@ -0,0 +1,10 @@
|
||||
from flask import Blueprint, render_template, session, redirect, url_for, flash
|
||||
|
||||
dashboard_bp = Blueprint('dashboard', __name__)
|
||||
|
||||
@dashboard_bp.route('/')
|
||||
def index():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login')) # 🚀 未登录用户跳转到登录页
|
||||
return render_template('dashboard.html') # ✅ 主页为 dashboard
|
||||
274
frontend/routes/games.py
Normal file
274
frontend/routes/games.py
Normal file
@ -0,0 +1,274 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
import requests
|
||||
from frontend.config import Config
|
||||
|
||||
games_bp = Blueprint('games', __name__)
|
||||
|
||||
|
||||
@games_bp.route('/games/')
|
||||
def list_games():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# 仅发送必要参数
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/games",
|
||||
json={"token": session['token']} # 匹配后端的请求模型
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
flash("获取游戏列表失败", "danger")
|
||||
return redirect(url_for('dashboard.index'))
|
||||
|
||||
return render_template('games/list.html', games=resp.json())
|
||||
|
||||
|
||||
|
||||
@games_bp.route('/games/add', methods=['GET', 'POST'])
|
||||
def add_game():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
payload = {
|
||||
"game_name": request.form['name'],
|
||||
"game_type": 0 if request.form['type'] == 'script' else 1,
|
||||
"description": request.form['desc'],
|
||||
"min_players": int(request.form['min_players']),
|
||||
"max_players": int(request.form['max_players']),
|
||||
"duration": request.form['duration'],
|
||||
"price": float(request.form['price']),
|
||||
"difficulty_level": int(request.form['difficulty']),
|
||||
"is_available": int(request.form['is_available']),
|
||||
"quantity": int(request.form['quantity']),
|
||||
"token": session['token'],
|
||||
"long_description": request.form['long_desc'],
|
||||
}
|
||||
# 添加游戏使用 admin 接口
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/games/create",
|
||||
json=payload,
|
||||
)
|
||||
print(payload)
|
||||
if resp.status_code == 200:
|
||||
flash("添加成功", "success")
|
||||
return redirect(url_for('games.list_games'))
|
||||
else:
|
||||
flash("添加失败", "danger")
|
||||
return render_template('games/add.html')
|
||||
|
||||
|
||||
@games_bp.route('/games/delete/<int:game_id>', methods=['POST'])
|
||||
def delete_game(game_id):
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
resp = requests.delete(
|
||||
f"{Config.BASE_API_URL}/admin/games/{game_id}",
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
flash("删除成功", "success")
|
||||
else:
|
||||
flash("删除失败,因为该游戏产生过订单,可以进行下架操作", "danger")
|
||||
return redirect(url_for('games.list_games'))
|
||||
|
||||
@games_bp.route('/games/edit/<int:game_id>', methods=['GET', 'POST'])
|
||||
def edit_game(game_id):
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
# 更新游戏逻辑
|
||||
payload = {
|
||||
"game_name": request.form['name'],
|
||||
"game_type": 0 if request.form['type'] == 'script' else 1,
|
||||
"description": request.form['desc'],
|
||||
"min_players": int(request.form['min_players']),
|
||||
"max_players": int(request.form['max_players']),
|
||||
"duration": request.form['duration'],
|
||||
"price": float(request.form['price']),
|
||||
"difficulty_level": int(request.form['difficulty']),
|
||||
"is_available": 1 if 'available' in request.form else 0,
|
||||
"quantity": int(request.form['quantity']),
|
||||
"long_description": request.form['long_desc'],
|
||||
}
|
||||
|
||||
resp = requests.put(
|
||||
f"{Config.BASE_API_URL}/admin/games/{game_id}",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
# print(resp.json())
|
||||
if resp.status_code == 200:
|
||||
flash("游戏更新成功", "success")
|
||||
return redirect(url_for('games.list_games'))
|
||||
else:
|
||||
flash("更新失败,请检查数据", "danger")
|
||||
return redirect(url_for('games.edit_game', game_id=game_id))
|
||||
|
||||
# GET请求处理
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{Config.BASE_API_URL}/admin/games/{game_id}",
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return render_template('games/edit.html', game=resp.json())
|
||||
except requests.exceptions.HTTPError as e:
|
||||
flash("游戏不存在或无权访问", "danger")
|
||||
except Exception as e:
|
||||
flash("获取游戏信息失败", "danger")
|
||||
return redirect(url_for('games.list_games'))
|
||||
|
||||
@games_bp.route('/games/photos/<int:game_id>', methods=['POST'])
|
||||
def manage_photos(game_id):
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
if request.method == 'POST':
|
||||
# 处理图片上传逻辑
|
||||
photo_url: str = request.form['photo_url']
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/games/set_photo",
|
||||
json={"game_id": game_id, "photo_url": photo_url, "token": session['token']}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
flash("图片url设置成功", "success")
|
||||
else:
|
||||
flash("图片设置失败", "danger")
|
||||
return redirect(url_for('games.list_games', game_id=game_id))
|
||||
|
||||
|
||||
@games_bp.route('/games/tags', methods=['GET', 'POST'])
|
||||
def manage_tags():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# 获取所有游戏数据 (保持原GET请求)
|
||||
|
||||
|
||||
games_resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/games",
|
||||
json={"token": session['token']} # 匹配后端的请求模型
|
||||
)
|
||||
all_games = games_resp.json() if games_resp.status_code == 200 else []
|
||||
# 处理标签创建请求
|
||||
if request.method == 'POST':
|
||||
# 创建新标签
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/tags",
|
||||
json={
|
||||
"tag_name": request.form['tag_name'],
|
||||
"token": session['token']
|
||||
}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
flash("标签创建成功", "success")
|
||||
else:
|
||||
flash(resp.json().get('detail', '创建失败'), "danger")
|
||||
return redirect(url_for('games.manage_tags'))
|
||||
|
||||
# 获取所有标签 (改为新的POST请求)
|
||||
tags_resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/get_tags",
|
||||
json={"token": session['token']}
|
||||
)
|
||||
|
||||
# 错误处理
|
||||
if tags_resp.status_code != 200:
|
||||
flash("获取标签列表失败", "danger")
|
||||
return redirect(url_for('games.manage_tags'))
|
||||
|
||||
# 获取游戏标签关联数据 (示例调用)
|
||||
game_tags_data = []
|
||||
if tags_resp.status_code == 200:
|
||||
for tag in tags_resp.json():
|
||||
# 调用新的接口获取每个标签关联的游戏
|
||||
games_resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/get_games_by_tag",
|
||||
json={"tag_id": tag['tag_id'], "token": session['token']}
|
||||
)
|
||||
if games_resp.status_code == 200:
|
||||
game_tags_data.append({
|
||||
"tag_id": tag['tag_id'],
|
||||
"games": games_resp.json()
|
||||
})
|
||||
|
||||
return render_template('games/tags.html',
|
||||
tags=tags_resp.json(),
|
||||
all_games=all_games,
|
||||
game_tags=game_tags_data)
|
||||
|
||||
@games_bp.route('/games/link_tags', methods=['POST'])
|
||||
def link_tags():
|
||||
"""批量关联标签"""
|
||||
data = {
|
||||
"tag_id": request.form['tag_id'],
|
||||
"game_ids": list(map(int, request.form.getlist('game_ids'))),
|
||||
"token": session['token']
|
||||
}
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/tags/link_games",
|
||||
json=data
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
flash(f"成功关联 {resp.json()['linked_count']} 个游戏", "success")
|
||||
else:
|
||||
flash("关联失败", "danger")
|
||||
return redirect(url_for('games.manage_tags'))
|
||||
|
||||
|
||||
@games_bp.route('/games/get_game_tags', methods=['POST'])
|
||||
def proxy_get_game_tags():
|
||||
"""代理获取标签关联游戏的请求"""
|
||||
if not session.get('token'):
|
||||
return {'error': 'Unauthorized'}, 401
|
||||
|
||||
data = {
|
||||
"tag_id": request.json.get('tag_id'),
|
||||
"token": session['token']
|
||||
}
|
||||
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/get_games_by_tag",
|
||||
json=data
|
||||
)
|
||||
return resp.json(), resp.status_code
|
||||
|
||||
|
||||
@games_bp.route('/games/unlink_game', methods=['POST'])
|
||||
def proxy_unlink_game():
|
||||
"""代理取消关联请求"""
|
||||
if not session.get('token'):
|
||||
return {'error': 'Unauthorized'}, 401
|
||||
|
||||
data = {
|
||||
"tag_id": request.json.get('tag_id'),
|
||||
"game_id": request.json.get('game_id'),
|
||||
"token": session['token']
|
||||
}
|
||||
|
||||
print(data)
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/tags/unlink",
|
||||
json=data
|
||||
)
|
||||
return {'status': 'success'}, 200
|
||||
|
||||
|
||||
@games_bp.route('/games/delete_tag', methods=['POST'])
|
||||
def delete_tag():
|
||||
"""处理标签删除请求"""
|
||||
if not session.get('token'):
|
||||
return {'error': 'Unauthorized'}, 401
|
||||
|
||||
data = {
|
||||
"tag_id": request.json.get('tag_id'),
|
||||
"token": session['token']
|
||||
}
|
||||
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/tags/delete",
|
||||
json=data
|
||||
)
|
||||
return resp.json(), resp.status_code
|
||||
38
frontend/routes/groups.py
Normal file
38
frontend/routes/groups.py
Normal file
@ -0,0 +1,38 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
import requests
|
||||
from frontend.config import Config
|
||||
|
||||
groups_bp = Blueprint('groups', __name__)
|
||||
|
||||
@groups_bp.route('/groups/')
|
||||
def manage_groups():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# 获取群组列表
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/groups/list",
|
||||
json={"token": session['token']}
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
flash("获取群组列表失败", "danger")
|
||||
return redirect(url_for('dashboard.index'))
|
||||
|
||||
return render_template('groups/list.html', groups=resp.json())
|
||||
|
||||
@groups_bp.route('/groups/delete', methods=['POST'])
|
||||
def delete_group():
|
||||
group_id = request.form.get('group_id')
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/groups/delete",
|
||||
json={
|
||||
"token": session['token'],
|
||||
"group_id": group_id
|
||||
}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
flash("群组删除成功", "success")
|
||||
else:
|
||||
flash(resp.json().get('detail', '删除失败'), "danger")
|
||||
return redirect(url_for('groups.manage_groups'))
|
||||
47
frontend/routes/messages.py
Normal file
47
frontend/routes/messages.py
Normal file
@ -0,0 +1,47 @@
|
||||
from flask import Blueprint, render_template, session, redirect, url_for, jsonify
|
||||
import json
|
||||
from frontend.config import Config
|
||||
import requests
|
||||
|
||||
messages_bp = Blueprint('messages', __name__)
|
||||
|
||||
@messages_bp.route('/admin/messages')
|
||||
def list_messages():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/messages",
|
||||
headers={"Authorization": f"Bearer {session['token']}"},
|
||||
json={ # 添加分页参数
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"token": session['token']
|
||||
}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return render_template('messages/list.html',
|
||||
messages=response.json().get('data', []),
|
||||
total=response.json().get('total', 0))
|
||||
return render_template('messages/list.html', messages=[], total=0)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return render_template('messages/list.html', messages=[], total=0)
|
||||
|
||||
@messages_bp.route('/admin/messages/<int:message_id>', methods=['DELETE'])
|
||||
def delete_message(message_id):
|
||||
if not session.get('token'):
|
||||
return jsonify({"detail": "未认证"}), 401
|
||||
|
||||
try:
|
||||
# 中转到FastAPI
|
||||
response = requests.delete(
|
||||
f"{Config.BASE_API_URL}/admin/messages/{message_id}",
|
||||
headers={"Authorization": f"Bearer {session['token']}"},
|
||||
json={"token": session['token']}
|
||||
)
|
||||
return jsonify(response.json()), response.status_code
|
||||
except requests.exceptions.ConnectionError:
|
||||
return jsonify({"detail": "后端服务不可用"}), 500
|
||||
|
||||
|
||||
120
frontend/routes/orders.py
Normal file
120
frontend/routes/orders.py
Normal file
@ -0,0 +1,120 @@
|
||||
from flask import Blueprint, render_template, session, redirect, url_for, flash, jsonify
|
||||
import requests
|
||||
from frontend.config import Config
|
||||
from flask import request
|
||||
|
||||
orders_bp = Blueprint('orders', __name__, url_prefix='/orders')
|
||||
|
||||
@orders_bp.route('/')
|
||||
def index():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('orders.html', page=1)
|
||||
|
||||
@orders_bp.route('/list')
|
||||
def list_orders():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
status = request.args.get('status', 'in_progress')
|
||||
per_page = 20
|
||||
page = int(request.args.get('page', 1))
|
||||
|
||||
# 调用后端API
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"order_status": status,
|
||||
"start": (page - 1) * per_page + 1,
|
||||
"end": page * per_page
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/orders/query",
|
||||
json=payload
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
# 修正数据解析方式
|
||||
response_data = response.json()
|
||||
orders = response_data.get("orders", []) # 获取订单列表
|
||||
total_orders = response_data.get("total", 0) # 获取总数
|
||||
|
||||
# 修正分页计算逻辑
|
||||
import math
|
||||
total_pages = math.ceil(total_orders / per_page) if total_orders > 0 else 1
|
||||
|
||||
return render_template('_order_table.html',
|
||||
orders=orders,
|
||||
page=page,
|
||||
total_pages=total_pages,
|
||||
status=status)
|
||||
|
||||
flash("获取订单失败", "danger")
|
||||
return redirect(url_for('orders.list_orders'))
|
||||
|
||||
@orders_bp.route('/complete/<int:order_id>', methods=['PUT'])
|
||||
def complete_order(order_id):
|
||||
"""处理订单完成请求"""
|
||||
if not session.get('token'):
|
||||
return jsonify({"error": "未授权"}), 401
|
||||
|
||||
# 获取前端参数
|
||||
data = request.get_json()
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"order_id": order_id,
|
||||
"end_datetime": data.get('end_datetime').replace(" ", "T")
|
||||
}
|
||||
|
||||
# 调用后端API
|
||||
backend_response = requests.put(
|
||||
f"{Config.BASE_API_URL}/admin/orders/{order_id}/complete",
|
||||
json=payload
|
||||
)
|
||||
|
||||
return jsonify(backend_response.json()), backend_response.status_code
|
||||
|
||||
@orders_bp.route('/settle/<int:order_id>', methods=['PUT'])
|
||||
def settle_order(order_id):
|
||||
"""处理订单结算请求"""
|
||||
if not session.get('token'):
|
||||
return jsonify({"error": "未授权"}), 401
|
||||
|
||||
data = request.get_json()
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"order_id": order_id,
|
||||
"used_points": data.get('used_points', 0),
|
||||
"coupon_id": data.get('coupon_id')
|
||||
}
|
||||
|
||||
backend_response = requests.put(
|
||||
f"{Config.BASE_API_URL}/admin/orders/{order_id}/settle",
|
||||
json=payload
|
||||
)
|
||||
|
||||
return jsonify(backend_response.json()), backend_response.status_code
|
||||
|
||||
|
||||
@orders_bp.route('/details/<int:order_id>', methods=['GET'])
|
||||
def get_order_details(order_id):
|
||||
"""获取订单详情"""
|
||||
if not session.get('token'):
|
||||
return jsonify({"error": "未授权"}), 401
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"order_id": order_id
|
||||
}
|
||||
# 调用后端API
|
||||
backend_response = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/orders/details",
|
||||
json=payload
|
||||
|
||||
)
|
||||
return jsonify(backend_response.json()), backend_response.status_code
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
124
frontend/routes/tables.py
Normal file
124
frontend/routes/tables.py
Normal file
@ -0,0 +1,124 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||
import requests
|
||||
from frontend.config import Config
|
||||
|
||||
tables_bp = Blueprint('tables', __name__)
|
||||
|
||||
@tables_bp.route('/tables/')
|
||||
def list_tables():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{Config.BASE_API_URL}/admin/tables",
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
flash("获取桌台列表失败", "danger")
|
||||
return redirect(url_for('dashboard.index'))
|
||||
|
||||
return render_template('tables/list.html', tables=resp.json())
|
||||
except Exception as e:
|
||||
flash(f"网络错误: {str(e)}", "danger")
|
||||
return redirect(url_for('dashboard.index'))
|
||||
|
||||
@tables_bp.route('/tables/add', methods=['GET', 'POST'])
|
||||
def add_table():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"game_table_number": str(request.form['game_table_number']),
|
||||
"capacity": int(request.form['capacity']),
|
||||
"price": float(request.form['price'])
|
||||
}
|
||||
resp = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/tables/create",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
flash("添加成功", "success")
|
||||
return redirect(url_for('tables.list_tables'))
|
||||
else:
|
||||
error_msg = resp.json().get('detail', '未知错误')
|
||||
flash(f"添加失败: {error_msg}", "danger")
|
||||
except ValueError:
|
||||
flash("输入格式不正确", "danger")
|
||||
except Exception as e:
|
||||
flash(f"网络错误: {str(e)}", "danger")
|
||||
|
||||
return render_template('tables/add.html')
|
||||
|
||||
@tables_bp.route('/tables/edit/<int:table_id>', methods=['GET', 'POST'])
|
||||
def edit_table(table_id):
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"game_table_number": str(request.form['game_table_number']),
|
||||
"capacity": int(request.form['capacity']),
|
||||
"price": float(request.form['price'])
|
||||
}
|
||||
resp = requests.put(
|
||||
f"{Config.BASE_API_URL}/admin/tables/{table_id}",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
flash("更新成功", "success")
|
||||
return redirect(url_for('tables.list_tables'))
|
||||
else:
|
||||
error_msg = resp.json().get('detail', '未知错误')
|
||||
flash(f"更新失败: {error_msg}", "danger")
|
||||
except ValueError:
|
||||
flash("输入格式不正确", "danger")
|
||||
except Exception as e:
|
||||
flash(f"网络错误: {str(e)}", "danger")
|
||||
|
||||
# 获取当前桌台信息
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{Config.BASE_API_URL}/admin/tables",
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
tables = resp.json()
|
||||
table = next((t for t in tables if t['table_id'] == table_id), None)
|
||||
if table:
|
||||
return render_template('tables/edit.html', table=table)
|
||||
flash("桌台不存在", "danger")
|
||||
except Exception as e:
|
||||
flash(f"获取数据失败: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('tables.list_tables'))
|
||||
|
||||
@tables_bp.route('/tables/delete/<int:table_id>', methods=['POST'])
|
||||
def delete_table(table_id):
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
resp = requests.delete(
|
||||
f"{Config.BASE_API_URL}/admin/tables/{table_id}",
|
||||
headers={"Authorization": f"Bearer {session['token']}"}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
flash("删除成功", "success")
|
||||
else:
|
||||
error_msg = resp.json().get('detail', '未知错误')
|
||||
flash(f"删除失败: {error_msg}", "danger")
|
||||
except Exception as e:
|
||||
flash(f"网络错误: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('tables.list_tables'))
|
||||
206
frontend/routes/users.py
Normal file
206
frontend/routes/users.py
Normal file
@ -0,0 +1,206 @@
|
||||
# users.py
|
||||
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
|
||||
import requests
|
||||
import math
|
||||
from frontend.config import Config
|
||||
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
# 显示用户列表,带分页
|
||||
@users_bp.route('/')
|
||||
def list_users():
|
||||
"""显示用户列表,带分页"""
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# 1. 获取用户总数
|
||||
response = requests.post(f'{Config.BASE_API_URL}/admin/users/sum',
|
||||
json={"token": session.get('token')})
|
||||
total_users = 0
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
total_users = int(response.json().get('message', 0))
|
||||
except (ValueError, TypeError):
|
||||
total_users = 0
|
||||
|
||||
# 2. 分页查询用户
|
||||
page = request.args.get('page', 1, type=int)
|
||||
start = (page - 1) * 20 + 1
|
||||
end = page * 20
|
||||
|
||||
coupon_resp = requests.get(
|
||||
f"{Config.BASE_API_URL}/admin/coupons",
|
||||
params={"token": session['token']}
|
||||
)
|
||||
coupons = coupon_resp.json() if coupon_resp.status_code == 200 else []
|
||||
|
||||
response2 = requests.post(f'{Config.BASE_API_URL}/admin/users/query',
|
||||
json={"token": session.get('token'), "start": start, "end": end})
|
||||
users = response2.json() if response2.status_code == 200 else []
|
||||
|
||||
total_pages = math.ceil(total_users / 20)
|
||||
return render_template("users.html",
|
||||
users=users,
|
||||
coupons=coupons,
|
||||
page=page,
|
||||
total_users=total_users,
|
||||
total_pages=total_pages)
|
||||
|
||||
# 搜索接口
|
||||
@users_bp.route('/search', methods=['POST'])
|
||||
def search_users():
|
||||
"""
|
||||
前端在 /users 页面点击搜索时 POST 到 /users/search
|
||||
不需要从前端传 token,直接使用 session['token']
|
||||
"""
|
||||
if not session.get('token'):
|
||||
return jsonify([]) # 或返回 401
|
||||
|
||||
data = request.get_json() or {}
|
||||
query_mode = data.get('query_mode')
|
||||
query_value = data.get('query_value')
|
||||
|
||||
# 若搜索值为空,直接返回空
|
||||
if not query_value:
|
||||
return jsonify([])
|
||||
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"query_mode": query_mode,
|
||||
"query_value": query_value
|
||||
}
|
||||
resp = requests.post(f"{Config.BASE_API_URL}/admin/users/search", json=payload)
|
||||
if resp.status_code == 200:
|
||||
return jsonify(resp.json())
|
||||
return jsonify([])
|
||||
|
||||
# 删除用户
|
||||
@users_bp.route('/delete_user', methods=['POST'])
|
||||
def delete_user():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
uid = request.form.get("uid")
|
||||
page = request.args.get('page', 1, type=int)
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"uid": int(uid)
|
||||
}
|
||||
resp = requests.post(f"{Config.BASE_API_URL}/admin/users/del", json=payload)
|
||||
if resp.status_code == 200:
|
||||
flash("删除成功", "success")
|
||||
else:
|
||||
flash("删除失败", "danger")
|
||||
return redirect(url_for('users.list_users', page=page))
|
||||
|
||||
# 更新用户
|
||||
@users_bp.route('/update_user', methods=['POST'])
|
||||
def update_user():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
uid = request.form.get('uid')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"uid": int(uid),
|
||||
"username": request.form.get('username'),
|
||||
"email": request.form.get('email'),
|
||||
"phone_number": request.form.get('phone_number'),
|
||||
"gender": request.form.get('gender'),
|
||||
"user_type": request.form.get('user_type')
|
||||
}
|
||||
resp = requests.post(f"{Config.BASE_API_URL}/admin/users/update", json=payload)
|
||||
if resp.status_code == 200:
|
||||
flash("更新成功", "success")
|
||||
else:
|
||||
flash("更新失败", "danger")
|
||||
|
||||
return redirect(url_for('users.list_users', page=page))
|
||||
|
||||
# 重置密码(实现具体逻辑)
|
||||
@users_bp.route('/reset_password', methods=['POST'])
|
||||
def reset_password():
|
||||
"""
|
||||
这个路由接收新密码并调用后端 API 进行密码重置。
|
||||
"""
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
uid = request.form.get('uid')
|
||||
new_password = request.form.get('password')
|
||||
# 确保 uid 和 new_password 不为空
|
||||
if not uid or not new_password:
|
||||
flash("缺少必要参数", "danger")
|
||||
return redirect(url_for('users.list_users', page=request.args.get('page', 1, type=int)))
|
||||
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"uid": int(uid),
|
||||
"new_password": new_password
|
||||
}
|
||||
resp = requests.post(f"{Config.BASE_API_URL}/admin/users/update_password", json=payload)
|
||||
if resp.status_code == 200:
|
||||
flash("重置密码成功", "success")
|
||||
else:
|
||||
flash("重置密码失败", "danger")
|
||||
return redirect(url_for('users.list_users', page=request.args.get('page', 1, type=int)))
|
||||
|
||||
# 在users_bp蓝图下添加新路由
|
||||
@users_bp.route('/adjust_points', methods=['POST'])
|
||||
def adjust_points():
|
||||
if not session.get('token'):
|
||||
flash("请先登录", "warning")
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
uid = request.form.get('uid')
|
||||
points = int(request.form.get('points'))
|
||||
reason = request.form.get('reason')
|
||||
|
||||
payload = {
|
||||
"token": session['token'],
|
||||
"uid": uid,
|
||||
"points": points,
|
||||
"reason": reason
|
||||
}
|
||||
|
||||
resp = requests.post(f"{Config.BASE_API_URL}/admin/users/update_points", json=payload)
|
||||
if resp.status_code == 200:
|
||||
flash("积分更新成功", "success")
|
||||
else:
|
||||
flash("积分更新失败", "danger")
|
||||
|
||||
return redirect(url_for('users.list_users', page=request.args.get('page', 1)))
|
||||
|
||||
@users_bp.route('/issue-coupon', methods=['POST'])
|
||||
def issue_coupon():
|
||||
if not session.get('token'):
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
user_id = int(request.form['user_id'])
|
||||
coupon_id = int(request.form['coupon_id'])
|
||||
|
||||
# 通过Flask中转请求到FastAPI
|
||||
response = requests.post(
|
||||
f"{Config.BASE_API_URL}/admin/coupons/issue",
|
||||
json={
|
||||
"token": session['token'],
|
||||
"user_id": user_id,
|
||||
"coupon_id": coupon_id
|
||||
}
|
||||
)
|
||||
print(request.form)
|
||||
if response.status_code == 200:
|
||||
flash("优惠券发放成功", "success")
|
||||
else:
|
||||
flash(response.json().get('detail', '发放失败'), "danger")
|
||||
except requests.exceptions.ConnectionError:
|
||||
flash("后端服务不可用", "danger")
|
||||
|
||||
return redirect(url_for('users.list_users'))
|
||||
323
frontend/templates/_order_table.html
Normal file
323
frontend/templates/_order_table.html
Normal file
@ -0,0 +1,323 @@
|
||||
{% set page = page | default(1) %}
|
||||
{% set total_pages = total_pages | default(1) %}
|
||||
{% set status = status | default('in_progress') %}
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>订单ID</th>
|
||||
<th>用户名称</th> <!-- 原用户ID改为用户名称 -->
|
||||
<th>游戏桌号</th> <!-- 原游戏桌ID改为桌号 -->
|
||||
<th>游戏人数</th>
|
||||
<th>开始时间</th>
|
||||
<th>结束时间</th>
|
||||
<th>结算时间</th>
|
||||
<th>应付金额</th>
|
||||
<th>实付金额</th>
|
||||
<th>积分使用</th> <!-- 新增 -->
|
||||
<th>优惠券</th> <!-- 新增 -->
|
||||
<th>游戏时长</th> <!-- 新增 -->
|
||||
<th>支付方式</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.order_id }}</td>
|
||||
<td>{{ order.user_name }}</td> <!-- 显示用户名 -->
|
||||
<td>{{ order.game_table_number or '未分配' }}</td> <!-- 显示桌号 -->
|
||||
<td>{{ order.num_players }}</td>
|
||||
<td>{{ order.start_datetime }}</td>
|
||||
<td>{{ order.end_datetime or '进行中' }}</td>
|
||||
<td>{{ order.settlement_time or '未付款' }}</td>
|
||||
|
||||
<td>¥{{ "%.2f"|format(order.payable_price|default('0')|float) }}</td>
|
||||
<td>¥{{ "%.2f"|format(order.paid_price|default('0')|float) }}</td>
|
||||
<td>{{ order.used_points or '0' }} 积分</td>
|
||||
<td>{{ order.coupon_id or '无' }}</td>
|
||||
<td>{{ order.game_process_time or '0' }} 分钟</td>
|
||||
|
||||
<td>
|
||||
{% if order.order_status not in ['pending', 'in_progress'] %}
|
||||
{% if order.payment_method == 'offline' %}
|
||||
线下支付
|
||||
{% else %}
|
||||
{{ order.payment_method | default('现金支付') }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if order.order_status == 'completed' %}badge-success
|
||||
{% elif order.order_status == 'cancelled' %}badge-danger
|
||||
{% else %}badge-warning{% endif %}">
|
||||
{{ order.order_status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if order.order_status == 'in_progress' %}
|
||||
<button class="btn btn-success btn-sm"
|
||||
onclick="openCompleteModal('{{ order.order_id }}')">结束订单</button>
|
||||
{% elif order.order_status == 'pending' %}
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="openSettleModal('{{ order.order_id }}')">结算订单</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="text-center" colspan="9">暂无订单数据</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
{% if page > 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" data-page="{{ page - 1 }}" href="#">上一页</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" data-page="{{ p }}" href="#">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if page < total_pages %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" data-page="{{ page + 1 }}" href="#">下一页</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="modal fade" id="completeModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form onsubmit="completeOrder(event)">
|
||||
<input id="complete-order-id" type="hidden">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>结束时间</label>
|
||||
<input class="form-control" id="end-time" required step="1" type="datetime-local">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="submit">提交</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="settleModal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form onsubmit="settleOrder(event)">
|
||||
<input id="settle-order-id" type="hidden">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p>订单号:<span id="settle-order-number"></span></p>
|
||||
<p>用户名称:<span id="user-name"></span></p>
|
||||
<p>联系电话:<span id="user-phone"></span></p>
|
||||
<p>可用积分:<span id="available-points"></span></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p>开始时间:<span id="start-time"></span></p>
|
||||
<p>结束时间:<span id="game-end-time"></span></p>
|
||||
<p>游戏时长:<span id="duration"></span> 分钟</p>
|
||||
<p>应付金额:<span id="payable-price"></span> 元</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>使用积分</label>
|
||||
<input class="form-control" id="used-points" min="0" type="number" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>优惠券编号</label>
|
||||
<input class="form-control" id="coupon-code" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="submit">确认结算</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 分页事件监听
|
||||
document.querySelectorAll('.page-link').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const page = this.dataset.page;
|
||||
fetchOrders(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchOrders(page) {
|
||||
try {
|
||||
const response = await fetch(`/orders/list?page=${page}`);
|
||||
const html = await response.text();
|
||||
document.querySelector('table').innerHTML =
|
||||
new DOMParser().parseFromString(html, 'text/html')
|
||||
.querySelector('table').innerHTML;
|
||||
} catch (error) {
|
||||
console.error('加载订单失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:格式化日期为 YYYY-MM-DDTHH:mm 格式(本地时间)
|
||||
function formatLocalDateTime(date) {
|
||||
const pad = n => n < 10 ? '0' + n : n;
|
||||
return date.getFullYear() + '-' +
|
||||
pad(date.getMonth() + 1) + '-' +
|
||||
pad(date.getDate()) + 'T' +
|
||||
pad(date.getHours()) + ':' +
|
||||
pad(date.getMinutes());
|
||||
}
|
||||
|
||||
// 结束订单功能:打开模态框时预填本地时间
|
||||
function openCompleteModal(orderId) {
|
||||
// 获取当前本地时间并格式化
|
||||
const now = new Date();
|
||||
const timeString = formatLocalDateTime(now);
|
||||
|
||||
document.getElementById('end-time').value = timeString;
|
||||
document.getElementById('complete-order-id').value = orderId;
|
||||
$('#completeModal').modal('show');
|
||||
}
|
||||
|
||||
// 完成订单功能:提交订单完成的时间到后台
|
||||
async function completeOrder(event) {
|
||||
event.preventDefault();
|
||||
const orderId = document.getElementById('complete-order-id').value;
|
||||
const endTime = document.getElementById('end-time').value; // 格式为 "YYYY-MM-DDTHH:mm",代表本地时间
|
||||
|
||||
try {
|
||||
const response = await fetch(`/orders/complete/${orderId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${sessionStorage.getItem('token')}`
|
||||
},
|
||||
// 直接提交本地时间字符串,后台可按需求解析
|
||||
body: JSON.stringify({
|
||||
end_datetime: endTime
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
$('#completeModal').modal('hide');
|
||||
currentPage = document.querySelector('.page-item.active .page-link').dataset.page;
|
||||
fetchOrders(currentPage); // 刷新订单列表
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`操作失败: ${error.detail}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function openSettleModal(orderId) {
|
||||
try {
|
||||
// 先获取订单详情
|
||||
const order = await fetch(`/orders/details/${orderId}`)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`订单请求失败: ${res.status}`);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
console.log(order.start_datetime);
|
||||
// 再根据用户ID获取用户信息
|
||||
const user = await fetch(`/users/search`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
query_mode: "uid",
|
||||
query_value: order.user_id.toString()
|
||||
})
|
||||
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error(`用户请求失败: ${res.status}`);
|
||||
return res.json();
|
||||
});
|
||||
console.log(user);
|
||||
console.log(user[0].user_id);
|
||||
|
||||
// 填充结算模态框数据
|
||||
document.getElementById('settle-order-id').value = orderId;
|
||||
console.log(user[0].username);
|
||||
document.getElementById('settle-order-number').textContent = orderId;
|
||||
document.getElementById('user-name').textContent = user[0].username;
|
||||
document.getElementById('user-phone').textContent = user[0].phone_number;
|
||||
document.getElementById('start-time').textContent =
|
||||
new Date(order.start_datetime).toLocaleString();
|
||||
console.log(order.start_datetime);
|
||||
document.getElementById('game-end-time').textContent =
|
||||
new Date(order.end_datetime).toLocaleString();
|
||||
document.getElementById('duration').textContent =
|
||||
Math.round((new Date(order.end_datetime) - new Date(order.start_datetime)) / 60000);
|
||||
console.log(Math.round((new Date(order.end_datetime) - new Date(order.start_datetime)) / 60000));
|
||||
// 假设 available-points 为存在的字段
|
||||
document.getElementById('available-points').textContent = user[0].points;
|
||||
console.log(order.payable_price);
|
||||
console.log(order.paid_price);
|
||||
document.getElementById('payable-price').textContent = order.payable_price;
|
||||
|
||||
$('#settleModal').modal('show');
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function settleOrder(event) {
|
||||
event.preventDefault();
|
||||
const orderId = document.getElementById('settle-order-id').value;
|
||||
console.log(orderId);
|
||||
const payload = {
|
||||
used_points: parseInt(document.getElementById('used-points').value) || 0,
|
||||
coupon_id: document.getElementById('coupon-code').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/orders/settle/${orderId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
$('#settleModal').modal('hide');
|
||||
currentPage = document.querySelector('.page-item.active .page-link').dataset.page;
|
||||
fetchOrders(currentPage); // 刷新列表
|
||||
alert('结算成功!');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`结算失败: ${error.detail}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
88
frontend/templates/announcements.html
Normal file
88
frontend/templates/announcements.html
Normal file
@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>公告管理</h2>
|
||||
<button class="btn btn-primary mb-3" data-toggle="modal" data-target="#createModal">新建公告</button>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>内容</th>
|
||||
<th>时间范围</th>
|
||||
<th>颜色</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ann in announcements %}
|
||||
<tr>
|
||||
<td>{{ ann.id }}</td>
|
||||
<td>{{ ann.text }}</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ ann.start_time|datetime }} 至 {{ ann.end_time|datetime }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="color-preview" style="width:30px;height:30px;background-color:{{ ann.color }}"></div>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('announcements.delete_announcement', announcement_id=ann.id) }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('确定删除该公告?')">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">暂无公告</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 创建模态框 -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">新建公告</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('announcements.create_announcement') }}">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>内容</label>
|
||||
<textarea name="text" class="form-control" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>开始时间</label>
|
||||
<input type="datetime-local" name="start_time" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>结束时间</label>
|
||||
<input type="datetime-local" name="end_time" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>背景颜色</label>
|
||||
<input type="color" name="color" class="form-control" value="#ffffff">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
147
frontend/templates/base.html
Normal file
147
frontend/templates/base.html
Normal file
@ -0,0 +1,147 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}桌游厅点单系统管理后台{% endblock %}</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.nav-link { margin-right: 10px; }
|
||||
@media (min-width: 1200px) {
|
||||
.container,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container-xl {
|
||||
max-width: 2000px !important;
|
||||
}
|
||||
}
|
||||
/* 添加全局通知样式 */
|
||||
.global-notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: #ffc107;
|
||||
color: #333;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 9999;
|
||||
transition: bottom 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
|
||||
<a class="navbar-brand" href="{{ url_for('dashboard.index') }}">桌游厅点单系统</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{% if session.token %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('dashboard.index') }}">首页</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('users.list_users') }}">用户管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('orders.index') }}">订单管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('games.list_games') }}">游戏管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('tables.list_tables') }}">桌台管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('games.manage_tags') }}">游戏标签</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('groups.manage_groups') }}">拼团管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('announcements.manage_announcements') }}">公告管理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('coupons.list_coupons') }}">优惠券</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('messages.list_messages') }}">留言管理</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if session.token %}
|
||||
<span class="navbar-text">
|
||||
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-danger">退出登录</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="关闭">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<!-- jQuery 和 Bootstrap JS -->
|
||||
<!-- 1) 替换为完整版本的 jQuery,而非 slim -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 建立全局WebSocket连接
|
||||
const adminWS = new WebSocket(`ws://192.168.5.16:8000/bell/ws/admin`);
|
||||
|
||||
// 处理接收到的消息
|
||||
adminWS.onmessage = function(event) {
|
||||
const tableNumber = event.data;
|
||||
// 显示通知
|
||||
if (Notification.permission === "granted") {
|
||||
new Notification("用户呼叫", {
|
||||
body: `桌号 ${tableNumber} 需要服务`,
|
||||
icon: "/static/images/notification.png"
|
||||
});
|
||||
}
|
||||
|
||||
// 或在页面右下角显示浮动通知
|
||||
showGlobalNotification(tableNumber);
|
||||
};
|
||||
|
||||
function showGlobalNotification(tableNumber) {
|
||||
const notifications = document.querySelectorAll('.global-notification');
|
||||
const newNotification = document.createElement('div');
|
||||
newNotification.className = 'global-notification';
|
||||
newNotification.innerHTML = `桌号 ${tableNumber} 需要服务!`;
|
||||
document.body.appendChild(newNotification);
|
||||
|
||||
// 旧的通知上移
|
||||
notifications.forEach((notification, index) => {
|
||||
const rect = notification.getBoundingClientRect();
|
||||
const newBottom = parseFloat(getComputedStyle(notification).bottom) + rect.height + 10;
|
||||
notification.style.bottom = `${newBottom}px`;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
newNotification.remove();
|
||||
// 重新调整剩余通知的位置
|
||||
const remainingNotifications = document.querySelectorAll('.global-notification');
|
||||
remainingNotifications.forEach((notification, index) => {
|
||||
const newBottom = 20 + (remainingNotifications.length - 1 - index) * (notification.offsetHeight + 10);
|
||||
notification.style.bottom = `${newBottom}px`;
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
</script>
|
||||
<!-- 2) 新增 scripts block,供子模板插入自定义脚本 -->
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
117
frontend/templates/coupons/list.html
Normal file
117
frontend/templates/coupons/list.html
Normal file
@ -0,0 +1,117 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}优惠券管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>优惠券管理</h2>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">创建新优惠券</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ url_for('coupons.create_coupon') }}">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>优惠码</label>
|
||||
<input type="text" name="coupon_code" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增折扣类型 -->
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label>折扣类型</label>
|
||||
<select name="discount_type" class="form-control" required>
|
||||
<option value="fixed">满减</option>
|
||||
<option value="percentage">打折</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增折扣值 -->
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label>折扣值(打折类型优惠券打 95折直接输入 95!!!)</label>
|
||||
<input type="number" name="discount_amount" class="form-control"
|
||||
min="0" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增最低消费 -->
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label>最低消费</label>
|
||||
<input type="number" name="min_order_amount" class="form-control"
|
||||
min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增有效期 -->
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>有效期开始</label>
|
||||
<input type="datetime-local" name="valid_from"
|
||||
class="form-control" step="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>有效期结束</label>
|
||||
<input type="datetime-local" name="valid_to"
|
||||
class="form-control" step="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 现有库存字段 -->
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label>库存数量</label>
|
||||
<input type="number" name="quantity" class="form-control"
|
||||
min="1" value="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 align-self-end">
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>优惠券ID</th>
|
||||
<th>优惠码</th>
|
||||
<th>折扣类型</th>
|
||||
<th>折扣值</th>
|
||||
<th>最低消费</th>
|
||||
<th>有效期</th>
|
||||
<th>库存</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for coupon in coupons %}
|
||||
<tr>
|
||||
<td>{{ coupon.coupon_id }}</td>
|
||||
<td>{{ coupon.coupon_code }}</td>
|
||||
<td>{{ coupon.discount_type }}</td>
|
||||
<td>{{ coupon.discount_amount }}</td>
|
||||
<td>{{ coupon.min_order_amount or '无限制' }}</td>
|
||||
<td>{{ coupon.valid_from }} 至 {{ coupon.valid_to }}</td>
|
||||
<td>{{ coupon.quantity }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('coupons.delete_coupon', coupon_id=coupon.coupon_id) }}" method="post">
|
||||
<button type="submit" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('确定删除该优惠券吗?')">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
10
frontend/templates/dashboard.html
Normal file
10
frontend/templates/dashboard.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}管理首页{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<h1 class="display-4">欢迎来到桌游厅点单系统管理后台</h1>
|
||||
<p class="lead">请选择导航栏中的功能进行操作。</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
frontend/templates/games/add.html
Normal file
66
frontend/templates/games/add.html
Normal file
@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>添加新游戏</h2>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>游戏名称:</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>游戏类型:</label>
|
||||
<select name="type" class="form-control" required>
|
||||
<option value="script">剧本杀</option>
|
||||
<option value="board">桌游</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>游戏描述:</label>
|
||||
<textarea name="desc" class="form-control" rows="1"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>游戏长描述:</label>
|
||||
<textarea name="long_desc" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 form-group">
|
||||
<label>最少人数:</label>
|
||||
<input type="number" name="min_players" class="form-control" min="1" required>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label>最多人数:</label>
|
||||
<input type="number" name="max_players" class="form-control" min="1" required>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label>时长(分钟):</label>
|
||||
<input type="text" name="duration" class="form-control" required value="0">
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label>价格:</label>
|
||||
<input type="number" name="price" class="form-control" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>难度等级(1-10):</label>
|
||||
<input type="number" name="difficulty" class="form-control" min="1" max="10" required>
|
||||
<label>库存数量:</label>
|
||||
<input type="number" name="quantity" class="form-control" min="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>是否可用: </label>
|
||||
<select name="is_available" class="form-control" required>
|
||||
<option value="1">是</option>
|
||||
<option value="0">否</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
<a href="{{ url_for('games.list_games') }}" class="btn btn-secondary">返回</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
83
frontend/templates/games/edit.html
Normal file
83
frontend/templates/games/edit.html
Normal file
@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>编辑游戏信息</h2>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>游戏名称:</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ game.game_name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>游戏类型:</label>
|
||||
<select name="type" class="form-control" required>
|
||||
<option value="script" {% if game.game_type == 0 %}selected{% endif %}>剧本杀</option>
|
||||
<option value="board" {% if game.game_type == 1 %}selected{% endif %}>桌游</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>游戏描述:</label>
|
||||
<textarea name="desc" class="form-control" rows="1">{{ game.description }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>游戏长描述:</label>
|
||||
<textarea name="long_desc" class="form-control" rows="3">{{ game.long_description }}</textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 form-group">
|
||||
<label>最少人数:</label>
|
||||
<input type="number" name="min_players" class="form-control"
|
||||
value="{{ game.min_players }}" min="1" required>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label>最多人数:</label>
|
||||
<input type="number" name="max_players" class="form-control"
|
||||
value="{{ game.max_players }}" min="1" required>
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label>时长(分钟):</label>
|
||||
<input type="text" name="duration" class="form-control"
|
||||
value="{{ game.duration }}" required >
|
||||
</div>
|
||||
<div class="col-md-3 form-group">
|
||||
<label>价格:</label>
|
||||
<input type="number" name="price" class="form-control"
|
||||
value="{{ game.price }}" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label>难度等级(1-10):</label>
|
||||
<input type="number" name="difficulty" class="form-control"
|
||||
value="{{ game.difficulty_level }}" min="1" max="10" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label>库存数量:</label>
|
||||
<input type="number" name="quantity" class="form-control"
|
||||
value="{{ game.quantity }}" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label>是否可用:</label>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="available" class="form-check-input" id="available" {% if game.is_available %}checked{% endif %}>
|
||||
<label class="form-check-label" for="available">是</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">保存修改</button>
|
||||
<a href="{{ url_for('games.list_games') }}" class="btn btn-secondary">返回</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
90
frontend/templates/games/list.html
Normal file
90
frontend/templates/games/list.html
Normal file
@ -0,0 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>游戏管理</h2>
|
||||
<a href="{{ url_for('games.add_game') }}" class="btn btn-primary mb-3">添加新游戏</a>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>游戏名称</th>
|
||||
<th>库存数量</th>
|
||||
<th>类型</th>
|
||||
<th>人数范围</th>
|
||||
<th>时长(分钟)</th>
|
||||
<th>价格倍率</th>
|
||||
<th>可用</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for game in games %}
|
||||
<tr>
|
||||
<td>{{ game.game_name }}</td>
|
||||
<td>{{ game.quantity }}</td>
|
||||
<td>{{ '剧本杀' if game.game_type == 0 else '桌游' }}</td>
|
||||
<td>{{ game.min_players }}-{{ game.max_players }}人</td>
|
||||
<td>{{ game.duration }}</td>
|
||||
<td>{{ "%.2f"|format(game.price) }}</td>
|
||||
<td>{{ '是' if game.is_available else '否' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('games.edit_game', game_id=game.game_id) }}"
|
||||
class="btn btn-sm btn-warning">编辑</a>
|
||||
<form action="{{ url_for('games.delete_game', game_id=game.game_id) }}"
|
||||
method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('确认删除?')">删除</button>
|
||||
</form>
|
||||
<a href="#" class="btn btn-sm btn-success set-photo-btn" data-game-id="{{ game.game_id }}">设置封面</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 模态框 -->
|
||||
<div class="modal fade" id="setPhotoModal" tabindex="-1" aria-labelledby="setPhotoModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<!-- 表单提交到 manage_photos 路由 -->
|
||||
<form id="setPhotoForm" method="post" action="">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="setPhotoModalLabel">设置游戏封面</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="关闭">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="photoUrlInput">图片网址:</label>
|
||||
<input type="text" class="form-control" id="photoUrlInput" name="photo_url" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">确认</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
// 点击“设置封面”按钮时
|
||||
$('.set-photo-btn').click(function(e){
|
||||
e.preventDefault();
|
||||
var gameId = $(this).data('game-id');
|
||||
// 设置表单提交的 action 为当前游戏对应的 URL
|
||||
$('#setPhotoForm').attr('action', '/games/photos/' + gameId);
|
||||
// 弹出模态框
|
||||
$('#setPhotoModal').modal('show');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
185
frontend/templates/games/tags.html
Normal file
185
frontend/templates/games/tags.html
Normal file
@ -0,0 +1,185 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<!-- 标签创建表单 -->
|
||||
<div class="col-md-4">
|
||||
<!-- 保持原有创建表单不变 -->
|
||||
<div class="card">
|
||||
<div class="card-header">新建标签</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('games.manage_tags') }}">
|
||||
<div class="form-group">
|
||||
<input type="text" name="tag_name" class="form-control"
|
||||
placeholder="输入新标签名称" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">创建</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量关联表单 -->
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">批量关联标签与游戏</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('games.link_tags') }}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<select name="tag_id" class="form-control" required>
|
||||
<option value="">选择标签</option>
|
||||
{% for tag in tags %}
|
||||
<option value="{{ tag.tag_id }}">{{ tag.tag_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<div class="game-checkbox-list" style="max-height: 300px; overflow-y: auto;">
|
||||
{% for game in all_games %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
name="game_ids"
|
||||
value="{{ game.game_id }}"
|
||||
id="game_{{ game.game_id }}">
|
||||
<label class="form-check-label" for="game_{{ game.game_id }}">
|
||||
{{ game.game_name }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">关联选中游戏</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签列表 -->
|
||||
<div class="col-12 mt-4">
|
||||
<!-- 保持原有标签列表不变 -->
|
||||
<div class="card">
|
||||
<div class="card-header">已有标签</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标签ID</th>
|
||||
<th>标签名称</th>
|
||||
<th>关联游戏数</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tag in tags %}
|
||||
<tr onclick="showLinkedGames({{ tag.tag_id }})" style="cursor: pointer;">
|
||||
<td>{{ tag.tag_id }}</td>
|
||||
<td>{{ tag.tag_name }}</td>
|
||||
<td>{{ tag.game_count }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
onclick="event.stopPropagation();deleteTag({{ tag.tag_id }})">移除</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模态框保持不变 -->
|
||||
<div class="modal fade" id="linkedGamesModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">已关联游戏</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="linkedGamesList">
|
||||
<!-- 动态加载内容 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function deleteTag(tagId) {
|
||||
if (!confirm('确定要永久删除此标签吗?')) return;
|
||||
|
||||
fetch(`/games/delete_tag`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
tag_id: tagId,
|
||||
token: "{{ session.token }}"
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload(); // 刷新页面
|
||||
} else {
|
||||
response.json().then(data => {
|
||||
alert(data.detail || '删除失败');
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('网络请求失败');
|
||||
});
|
||||
}
|
||||
function showLinkedGames(tagId) {
|
||||
// 修改请求路径为Flask路由
|
||||
fetch(`/games/get_game_tags`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({tag_id: tagId, token: "{{ session.token }}"})
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('请求失败');
|
||||
return res.json();
|
||||
})
|
||||
.then(games => {
|
||||
const html = games.map(g => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>${g.game_name}</span>
|
||||
<button class="btn btn-sm btn-danger"
|
||||
onclick="unlinkGame(${tagId}, ${g.game_id})">取消关联</button>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('linkedGamesList').innerHTML = html;
|
||||
$('#linkedGamesModal').modal('show');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('获取关联游戏失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 取消关联操作
|
||||
function unlinkGame(tagId, gameId) {
|
||||
fetch(`/games/unlink_game`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
tag_id: tagId,
|
||||
game_id: gameId,
|
||||
token: "{{ session.token }}"
|
||||
})
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error('操作失败');
|
||||
$('#linkedGamesModal').modal('hide')
|
||||
location.reload()
|
||||
}).catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('取消关联失败');
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
71
frontend/templates/groups/list.html
Normal file
71
frontend/templates/groups/list.html
Normal file
@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>拼团管理</h2>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>拼团ID</th>
|
||||
<th>团队名称</th>
|
||||
<th>发起人</th>
|
||||
<th>最大人数</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>{{ group.group_id }}</td>
|
||||
<td>{{ group.group_name }}</td>
|
||||
<td>{{ group.leader_name or '未知' }}</td>
|
||||
<td>{{ group.max_members }}</td>
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if group.group_status == 'recruiting' %}bg-success
|
||||
{% elif group.group_status == 'full' %}bg-warning
|
||||
{% else %}bg-secondary{% endif %}">
|
||||
{{ '招募中' if group.group_status == 'recruiting'
|
||||
else '已满员' if group.group_status == 'full'
|
||||
else '已结束' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ group.created_at }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('groups.delete_group') }}" method="POST"
|
||||
onsubmit="return confirm('确定删除该拼团?此操作不可逆!');">
|
||||
<input type="hidden" name="group_id" value="{{ group.group_id }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="bi bi-trash"></i> 删除
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted">暂无进行中的拼团</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
frontend/templates/login.html
Normal file
26
frontend/templates/login.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}管理员登录{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<h1 class="my-4 text-center">管理员登录</h1>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" name="username" id="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" name="password" id="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" name="remember_me" id="remember_me" class="form-check-input">
|
||||
<label class="form-check-label" for="remember_me">记住我</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
58
frontend/templates/messages/list.html
Normal file
58
frontend/templates/messages/list.html
Normal file
@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}留言管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>用户留言管理</h2>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户ID</th>
|
||||
<th>用户名</th>
|
||||
<th>留言内容</th>
|
||||
<th>留言时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for message in messages %}
|
||||
<tr>
|
||||
<td>{{ message.user_id }}</td>
|
||||
<td>{{ message.username }}</td>
|
||||
<td>{{ message.message_content }}</td>
|
||||
<td>{{ message.created_at }}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm"
|
||||
onclick="deleteMessage({{ message.message_id }})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function deleteMessage(messageId) {
|
||||
if (confirm('确定删除这条留言吗?')) {
|
||||
// 调用Flask路由
|
||||
fetch(`/admin/messages/${messageId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer {{ session.token }}'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else if (response.status === 403) {
|
||||
alert('权限不足');
|
||||
} else {
|
||||
response.json().then(data => alert(data.detail));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
72
frontend/templates/orders.html
Normal file
72
frontend/templates/orders.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="my-4">订单列表</h1>
|
||||
|
||||
<!-- 状态导航 -->
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-status="in_progress" href="#">进行中</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-status="pending" href="#">待处理</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-status="completed" href="#">已完成</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-status="cancelled" href="#">已取消</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 订单表格 -->
|
||||
<div id="orderTable">
|
||||
{% include '_order_table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// 初始化:默认加载 "in_progress" 状态的订单
|
||||
loadOrders('in_progress', 1);
|
||||
|
||||
// 切换选项卡
|
||||
$('.nav-link[data-status]').click(function(e) {
|
||||
e.preventDefault();
|
||||
const status = $(this).data('status');
|
||||
$('.nav-link').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
loadOrders(status, 1);
|
||||
});
|
||||
|
||||
// 加载订单列表
|
||||
function loadOrders(status, page) {
|
||||
console.log('Sending request with status:', status, 'and page:', page);
|
||||
$.ajax({
|
||||
url: "{{ url_for('orders.list_orders') }}",
|
||||
data: {
|
||||
status: status,
|
||||
page: page
|
||||
},
|
||||
success: function(data) {
|
||||
$('#orderTable').html(data);
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error('请求失败:', xhr.statusText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 分页链接点击
|
||||
$('#orderTable').on('click', '.page-link', function(e) {
|
||||
e.preventDefault();
|
||||
const page = $(this).data('page');
|
||||
const status = $('.nav-link.active').data('status');
|
||||
loadOrders(status, page);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
15
frontend/templates/refresh.html
Normal file
15
frontend/templates/refresh.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>正在跳转…</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>登录成功,3秒后将自动跳转到后台首页…</p>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
window.location.href = "{{ url_for('users.list_users') }}";
|
||||
}, 3000); // 3秒=3000毫秒
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
24
frontend/templates/tables/add.html
Normal file
24
frontend/templates/tables/add.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}添加桌台{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>添加新桌台</h2>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label>桌号</label>
|
||||
<input type="text" name="game_table_number" class="form-control" required >
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>容量(人数)</label>
|
||||
<input type="number" name="capacity" class="form-control" required min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>价格倍率</label>
|
||||
<input type="number" name="price" class="form-control" step="0.01" required min="1">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
<a href="{{ url_for('tables.list_tables') }}" class="btn btn-secondary">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
frontend/templates/tables/edit.html
Normal file
27
frontend/templates/tables/edit.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}编辑桌台{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>编辑桌台 #{{ table.game_table_number }}</h2>
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label>桌号</label>
|
||||
<input type="text" name="game_table_number" class="form-control"
|
||||
value="{{ table.game_table_number }}" required >
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>容量(人数)</label>
|
||||
<input type="number" name="capacity" class="form-control"
|
||||
value="{{ table.capacity }}" required min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>价格倍率</label>
|
||||
<input type="number" name="price" class="form-control"
|
||||
value="{{ table.price }}" step="0.01" required min="1">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
<a href="{{ url_for('tables.list_tables') }}" class="btn btn-secondary">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
frontend/templates/tables/list.html
Normal file
40
frontend/templates/tables/list.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}桌台管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2>桌台管理</h2>
|
||||
<a href="{{ url_for('tables.add_table') }}" class="btn btn-primary mb-3">添加新桌台</a>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>桌号</th>
|
||||
<th>容量</th>
|
||||
<th>价格倍率</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for table in tables %}
|
||||
<tr>
|
||||
<td>{{ table.table_id }}</td>
|
||||
<td>{{ table.game_table_number }}</td>
|
||||
<td>{{ table.capacity }}人</td>
|
||||
<td>{{ "%.2f"|format(table.price) }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('tables.edit_table', table_id=table.table_id) }}"
|
||||
class="btn btn-sm btn-warning">编辑</a>
|
||||
<form action="{{ url_for('tables.delete_table', table_id=table.table_id) }}"
|
||||
method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('确认删除该桌台?')">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
374
frontend/templates/users.html
Normal file
374
frontend/templates/users.html
Normal file
@ -0,0 +1,374 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}用户管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<!-- 标题和搜索框 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h1 class="my-4">用户列表</h1>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group mt-4">
|
||||
<select class="form-select" id="queryMode">
|
||||
<option value="uid">UID</option>
|
||||
<option value="phone_number">手机号</option>
|
||||
<option value="username">用户名</option>
|
||||
<option value="email">邮箱</option>
|
||||
</select>
|
||||
<input type="text" class="form-control" id="queryValue" placeholder="搜索用户...">
|
||||
<button class="btn btn-primary" type="button" onclick="performSearch()">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>当前用户总数:{{ total_users }}</p>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>用户ID</th>
|
||||
<th>用户名</th>
|
||||
<th>积分</th>
|
||||
<th>性别</th>
|
||||
<th>手机号</th>
|
||||
<th>邮箱</th>
|
||||
<th>用户类型</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.user_id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.points }}</td>
|
||||
<td>{{ user.gender or '' }}</td>
|
||||
<td>{{ user.phone_number or '' }}</td>
|
||||
<td>{{ user.email or '' }}</td>
|
||||
<td>{{ user.user_type }}</td>
|
||||
<td>{{ user.created_at }}</td>
|
||||
<td>{{ user.updated_at }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-info btn-sm"
|
||||
onclick="openPointsModal('{{ user.user_id }}')">
|
||||
调整积分
|
||||
</button>
|
||||
<!-- 更新按钮 -->
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="openUpdateModal('{{ user.user_id }}', '{{ user.username }}', '{{ user.email }}', '{{ user.phone_number or '' }}', '{{ user.gender or '' }}', '{{ user.user_type }}')">
|
||||
更新
|
||||
</button>
|
||||
<!-- 重置密码按钮 -->
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
onclick="openResetPasswordModal('{{ user.user_id }}')">
|
||||
重置密码
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
onclick="openIssueCouponModal('{{ user.user_id }}')">
|
||||
发放优惠券
|
||||
</button>
|
||||
<!-- 删除按钮 -->
|
||||
<form action="{{ url_for('users.delete_user', page=page) }}" method="post" style="display:inline;">
|
||||
<input type="hidden" name="uid" value="{{ user.user_id }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('确定删除该用户吗?');">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 分页导航 -->
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
{% if page > 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('users.list_users', page=page - 1) }}">上一页</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">上一页</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('users.list_users', page=p) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if page < total_pages %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('users.list_users', page=page + 1) }}">下一页</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">下一页</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- 更新用户模态框 -->
|
||||
<div class="modal fade" id="updateUserModal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="updateUserModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<form action="{{ url_for('users.update_user', page=page) }}" method="post">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="updateUserModalLabel">更新用户信息</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- uid, username, email, phone_number 保持原样 -->
|
||||
<input type="hidden" id="update-uid" name="uid">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="update-username">用户名</label>
|
||||
<input type="text" class="form-control" id="update-username" name="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="update-email">邮箱</label>
|
||||
<input type="email" class="form-control" id="update-email" name="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="update-phone_number">手机号</label>
|
||||
<input type="text" class="form-control" id="update-phone_number" name="phone_number">
|
||||
</div>
|
||||
|
||||
<!-- 性别下拉:male, female, other -->
|
||||
<div class="form-group">
|
||||
<label for="update-gender">性别</label>
|
||||
<select class="form-control" id="update-gender" name="gender">
|
||||
<option value="">请选择</option>
|
||||
<option value="male">male</option>
|
||||
<option value="female">female</option>
|
||||
<option value="other">other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 用户类型改为下拉选单:player, admin -->
|
||||
<div class="form-group">
|
||||
<label for="update-user_type">用户类型</label>
|
||||
<select class="form-control" id="update-user_type" name="user_type">
|
||||
<option value="">请选择</option>
|
||||
<option value="player">player</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> <!-- end modal-body -->
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
|
||||
<button type="submit" class="btn btn-primary">保存更改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重置密码模态框 -->
|
||||
<div class="modal fade" id="resetPasswordModal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<!-- 注意:此处提交的路由 'users.reset_password' 只是一个占位,目前没有实际的重置密码逻辑 -->
|
||||
<form action="{{ url_for('users.reset_password', page=page) }}" method="post">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="resetPasswordModalLabel">重置密码</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="reset-uid" name="uid">
|
||||
<div class="form-group">
|
||||
<label for="reset-password">新密码</label>
|
||||
<input type="password" class="form-control" id="reset-password" name="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
|
||||
<button type="submit" class="btn btn-primary">重置密码</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加积分调整模态框 -->
|
||||
<div class="modal fade" id="pointsModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ url_for('users.adjust_points') }}" method="post">
|
||||
<input type="hidden" name="uid" id="points-uid">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>调整数值(正数为增加,负数为扣除)</label>
|
||||
<input type="number" name="points" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>调整原因</label>
|
||||
<input type="text" name="reason" class="form-control" required value="管理员后台更改">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="issueCouponModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{{ url_for('users.issue_coupon') }}">
|
||||
<input type="hidden" name="user_id" id="issue-user-id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">发放优惠券</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>选择优惠券</label>
|
||||
<select class="form-control" name="coupon_id" required>
|
||||
<option value="">请选择优惠券</option>
|
||||
{% for coupon in coupons %}
|
||||
<option value="{{ coupon.coupon_id }}">{{ coupon.coupon_code }} - {{ coupon.discount_type }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">发放</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function performSearch() {
|
||||
const queryMode = document.getElementById("queryMode");
|
||||
const queryValue = document.getElementById("queryValue").value.trim();
|
||||
const tableBody = document.getElementById("userTableBody");
|
||||
|
||||
// 若搜索框为空,重新加载当前页面,等同于清空搜索
|
||||
if (!queryValue) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/users/search", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query_mode: queryMode.value,
|
||||
query_value: queryValue
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
tableBody.innerHTML = "";
|
||||
if (data.length === 0) {
|
||||
tableBody.innerHTML =
|
||||
`<tr><td colspan="9" class="text-center">未找到匹配的用户</td></tr>`;
|
||||
return;
|
||||
}
|
||||
data.forEach(user => {
|
||||
let row = `<tr>
|
||||
<td>${user.user_id}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.points}</td>
|
||||
<td>${user.gender || ''}</td>
|
||||
<td>${user.phone_number || ''}</td>
|
||||
<td>${user.email || ''}</td>
|
||||
<td>${user.user_type}</td>
|
||||
<td>${user.created_at}</td>
|
||||
<td>${user.updated_at}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-info btn-sm"
|
||||
onclick="openPointsModal('${user.user_id}')">
|
||||
调整积分
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
onclick="openUpdateModal('${user.user_id}', '${user.username}', '${user.email}', '${user.phone_number || ''}', '${user.gender || ''}', '${user.user_type}')">
|
||||
更新
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
onclick="openResetPasswordModal('${user.user_id}')">
|
||||
重置密码
|
||||
</button>
|
||||
<form action="{{ url_for('users.delete_user', page=page) }}" method="post" style="display:inline;">
|
||||
<input type="hidden" name="uid" value="${user.user_id}">
|
||||
<button type="submit" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm('确定删除该用户吗?');">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>`;
|
||||
tableBody.innerHTML += row;
|
||||
});
|
||||
})
|
||||
.catch(error => console.error("搜索用户失败:", error));
|
||||
}
|
||||
|
||||
function openUpdateModal(uid, username, email, phone_number, gender, user_type) {
|
||||
document.getElementById('update-uid').value = uid;
|
||||
document.getElementById('update-username').value = username;
|
||||
document.getElementById('update-email').value = email;
|
||||
document.getElementById('update-phone_number').value = phone_number;
|
||||
|
||||
// 下拉选单初始化
|
||||
// gender 可能是 "male", "female" 或 "other";若为空则使用 ""
|
||||
let genderSelect = document.getElementById('update-gender');
|
||||
genderSelect.value = gender || "";
|
||||
|
||||
// user_type 可能是 "player", "admin";若为空则使用 ""
|
||||
let userTypeSelect = document.getElementById('update-user_type');
|
||||
userTypeSelect.value = user_type || "";
|
||||
|
||||
$('#updateUserModal').modal('show');
|
||||
}
|
||||
|
||||
function openResetPasswordModal(uid) {
|
||||
document.getElementById('reset-uid').value = uid;
|
||||
$('#resetPasswordModal').modal('show');
|
||||
}
|
||||
|
||||
function openPointsModal(uid) {
|
||||
document.getElementById('points-uid').value = uid;
|
||||
$('#pointsModal').modal('show');
|
||||
}
|
||||
|
||||
function openIssueCouponModal(userId) {
|
||||
$('#issue-user-id').val(userId);
|
||||
$('#issueCouponModal').modal('show');
|
||||
}
|
||||
|
||||
// 监听优惠券选择变化
|
||||
$('select[name="coupon_id"]').on('change', function() {
|
||||
const selected = $(this).find('option:selected');
|
||||
const quantity = selected.data('quantity') || 0;
|
||||
$('#coupon-quantity-hint').text(`剩余库存: ${quantity}`);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user