This commit is contained in:
haochen 2025-03-10 08:35:19 +08:00
commit e2f60254c0
81 changed files with 7956 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View 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>

View 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
View 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
View 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
View 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>

0
README.md Normal file
View File

54
backend/app/db.py Normal file
View 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
View 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
# }

View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
class AdminUser(BaseModel):
username: str
password: str
user_type: str

View 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()

View 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)

View 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)

View 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
)

View 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
}

View 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()

View 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)

View 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)

View 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}

View 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"}

View 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()

View 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, "微信绑定服务暂时不可用")

View 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)

View 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()

View 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()

View 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)

View 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()

View 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()

View 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)

View 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

View 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="管理员身份验证令牌"
)

View 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

View 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

View 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

View 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

View 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

View 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))

View 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()

View 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()

View 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()

View 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()

View 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()

View 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())

View 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

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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")

View 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
View 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
View 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
View 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'))

View 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'))

View 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
View 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
View 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'))

View 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
View 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
View 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
View 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'))

View 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>

View 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">&times;</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 %}

View 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">&times;</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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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">&times;</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 %}

View 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">&times;</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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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">&times;</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">&times;</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 %}