From e2f60254c01f0bf48fa68496db8f5277de953f5a Mon Sep 17 00:00:00 2001 From: haochen Date: Mon, 10 Mar 2025 08:35:19 +0800 Subject: [PATCH] add --- .idea/.gitignore | 8 + .idea/inspectionProfiles/Project_Default.xml | 54 ++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/modules.xml | 8 + .idea/table_game_project.iml | 15 + .idea/vcs.xml | 6 + README.md | 0 backend/app/db.py | 54 ++ backend/app/main.py | 82 +++ backend/app/models/admin_user.py | 6 + backend/app/routers/admin_announcement.py | 86 +++ backend/app/routers/admin_coupon.py | 54 ++ backend/app/routers/admin_game.py | 121 ++++ backend/app/routers/admin_group.py | 23 + backend/app/routers/admin_login.py | 18 + backend/app/routers/admin_message.py | 90 +++ backend/app/routers/admin_order.py | 58 ++ backend/app/routers/admin_table.py | 39 ++ backend/app/routers/admin_user.py | 109 ++++ backend/app/routers/bell.py | 36 ++ backend/app/routers/user_announcement.py | 24 + backend/app/routers/user_auth.py | 120 ++++ backend/app/routers/user_coupon.py | 21 + backend/app/routers/user_game.py | 426 ++++++++++++++ backend/app/routers/user_group.py | 534 ++++++++++++++++++ backend/app/routers/user_info.py | 27 + backend/app/routers/user_messages.py | 51 ++ backend/app/routers/user_order.py | 349 ++++++++++++ backend/app/routers/user_table.py | 41 ++ backend/app/schemas/admin_auth.py | 10 + backend/app/schemas/game_info.py | 79 +++ backend/app/schemas/message_info.py | 12 + backend/app/schemas/order_info.py | 56 ++ backend/app/schemas/table_info.py | 18 + backend/app/schemas/user_auth.py | 30 + backend/app/schemas/user_info.py | 59 ++ backend/app/services/admin_coupon_service.py | 91 +++ backend/app/services/admin_game_service.py | 326 +++++++++++ backend/app/services/admin_group_service.py | 59 ++ backend/app/services/admin_order_service.py | 221 ++++++++ backend/app/services/admin_table_service.py | 128 +++++ backend/app/services/admin_user_service.py | 394 +++++++++++++ backend/app/services/auth_service.py | 42 ++ backend/app/services/coupons.py | 28 + backend/app/services/user_coupon_service.py | 114 ++++ backend/app/services/user_getInfo_service.py | 124 ++++ backend/app/services/user_login_service.py | 221 ++++++++ backend/app/services/user_order_service.py | 327 +++++++++++ backend/app/services/user_table_service.py | 82 +++ backend/app/utils/create_database.py | 264 +++++++++ backend/app/utils/jwt_handler.py | 30 + backend/app/utils/sms_sender.py | 25 + frontend/app.py | 8 + frontend/config.py | 5 + frontend/routes/auth.py | 29 + frontend/routes/coupons.py | 73 +++ frontend/routes/dashboard.py | 10 + frontend/routes/games.py | 274 +++++++++ frontend/routes/groups.py | 38 ++ frontend/routes/messages.py | 47 ++ frontend/routes/orders.py | 120 ++++ frontend/routes/tables.py | 124 ++++ frontend/routes/users.py | 206 +++++++ frontend/templates/_order_table.html | 323 +++++++++++ frontend/templates/announcements.html | 88 +++ frontend/templates/base.html | 147 +++++ frontend/templates/coupons/list.html | 117 ++++ frontend/templates/dashboard.html | 10 + frontend/templates/games/add.html | 66 +++ frontend/templates/games/edit.html | 83 +++ frontend/templates/games/list.html | 90 +++ frontend/templates/games/tags.html | 185 ++++++ frontend/templates/groups/list.html | 71 +++ frontend/templates/login.html | 26 + frontend/templates/messages/list.html | 58 ++ frontend/templates/orders.html | 72 +++ frontend/templates/refresh.html | 15 + frontend/templates/tables/add.html | 24 + frontend/templates/tables/edit.html | 27 + frontend/templates/tables/list.html | 40 ++ frontend/templates/users.html | 374 ++++++++++++ 81 files changed, 7956 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/table_game_project.iml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 backend/app/db.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/admin_user.py create mode 100644 backend/app/routers/admin_announcement.py create mode 100644 backend/app/routers/admin_coupon.py create mode 100644 backend/app/routers/admin_game.py create mode 100644 backend/app/routers/admin_group.py create mode 100644 backend/app/routers/admin_login.py create mode 100644 backend/app/routers/admin_message.py create mode 100644 backend/app/routers/admin_order.py create mode 100644 backend/app/routers/admin_table.py create mode 100644 backend/app/routers/admin_user.py create mode 100644 backend/app/routers/bell.py create mode 100644 backend/app/routers/user_announcement.py create mode 100644 backend/app/routers/user_auth.py create mode 100644 backend/app/routers/user_coupon.py create mode 100644 backend/app/routers/user_game.py create mode 100644 backend/app/routers/user_group.py create mode 100644 backend/app/routers/user_info.py create mode 100644 backend/app/routers/user_messages.py create mode 100644 backend/app/routers/user_order.py create mode 100644 backend/app/routers/user_table.py create mode 100644 backend/app/schemas/admin_auth.py create mode 100644 backend/app/schemas/game_info.py create mode 100644 backend/app/schemas/message_info.py create mode 100644 backend/app/schemas/order_info.py create mode 100644 backend/app/schemas/table_info.py create mode 100644 backend/app/schemas/user_auth.py create mode 100644 backend/app/schemas/user_info.py create mode 100644 backend/app/services/admin_coupon_service.py create mode 100644 backend/app/services/admin_game_service.py create mode 100644 backend/app/services/admin_group_service.py create mode 100644 backend/app/services/admin_order_service.py create mode 100644 backend/app/services/admin_table_service.py create mode 100644 backend/app/services/admin_user_service.py create mode 100644 backend/app/services/auth_service.py create mode 100644 backend/app/services/coupons.py create mode 100644 backend/app/services/user_coupon_service.py create mode 100644 backend/app/services/user_getInfo_service.py create mode 100644 backend/app/services/user_login_service.py create mode 100644 backend/app/services/user_order_service.py create mode 100644 backend/app/services/user_table_service.py create mode 100644 backend/app/utils/create_database.py create mode 100644 backend/app/utils/jwt_handler.py create mode 100644 backend/app/utils/sms_sender.py create mode 100644 frontend/app.py create mode 100644 frontend/config.py create mode 100644 frontend/routes/auth.py create mode 100644 frontend/routes/coupons.py create mode 100644 frontend/routes/dashboard.py create mode 100644 frontend/routes/games.py create mode 100644 frontend/routes/groups.py create mode 100644 frontend/routes/messages.py create mode 100644 frontend/routes/orders.py create mode 100644 frontend/routes/tables.py create mode 100644 frontend/routes/users.py create mode 100644 frontend/templates/_order_table.html create mode 100644 frontend/templates/announcements.html create mode 100644 frontend/templates/base.html create mode 100644 frontend/templates/coupons/list.html create mode 100644 frontend/templates/dashboard.html create mode 100644 frontend/templates/games/add.html create mode 100644 frontend/templates/games/edit.html create mode 100644 frontend/templates/games/list.html create mode 100644 frontend/templates/games/tags.html create mode 100644 frontend/templates/groups/list.html create mode 100644 frontend/templates/login.html create mode 100644 frontend/templates/messages/list.html create mode 100644 frontend/templates/orders.html create mode 100644 frontend/templates/refresh.html create mode 100644 frontend/templates/tables/add.html create mode 100644 frontend/templates/tables/edit.html create mode 100644 frontend/templates/tables/list.html create mode 100644 frontend/templates/users.html diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..51753fc --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,54 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..403a8d8 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/table_game_project.iml b/.idea/table_game_project.iml new file mode 100644 index 0000000..bac11bc --- /dev/null +++ b/.idea/table_game_project.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/db.py b/backend/app/db.py new file mode 100644 index 0000000..aaf7388 --- /dev/null +++ b/backend/app/db.py @@ -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() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7b3278d --- /dev/null +++ b/backend/app/main.py @@ -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 +# } \ No newline at end of file diff --git a/backend/app/models/admin_user.py b/backend/app/models/admin_user.py new file mode 100644 index 0000000..328b2d4 --- /dev/null +++ b/backend/app/models/admin_user.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +class AdminUser(BaseModel): + username: str + password: str + user_type: str diff --git a/backend/app/routers/admin_announcement.py b/backend/app/routers/admin_announcement.py new file mode 100644 index 0000000..8966473 --- /dev/null +++ b/backend/app/routers/admin_announcement.py @@ -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() diff --git a/backend/app/routers/admin_coupon.py b/backend/app/routers/admin_coupon.py new file mode 100644 index 0000000..c55fcc2 --- /dev/null +++ b/backend/app/routers/admin_coupon.py @@ -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) diff --git a/backend/app/routers/admin_game.py b/backend/app/routers/admin_game.py new file mode 100644 index 0000000..4010d8e --- /dev/null +++ b/backend/app/routers/admin_game.py @@ -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) diff --git a/backend/app/routers/admin_group.py b/backend/app/routers/admin_group.py new file mode 100644 index 0000000..3125b7c --- /dev/null +++ b/backend/app/routers/admin_group.py @@ -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 + ) diff --git a/backend/app/routers/admin_login.py b/backend/app/routers/admin_login.py new file mode 100644 index 0000000..56a0af5 --- /dev/null +++ b/backend/app/routers/admin_login.py @@ -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 + } diff --git a/backend/app/routers/admin_message.py b/backend/app/routers/admin_message.py new file mode 100644 index 0000000..2858568 --- /dev/null +++ b/backend/app/routers/admin_message.py @@ -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() + diff --git a/backend/app/routers/admin_order.py b/backend/app/routers/admin_order.py new file mode 100644 index 0000000..08afd04 --- /dev/null +++ b/backend/app/routers/admin_order.py @@ -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) + diff --git a/backend/app/routers/admin_table.py b/backend/app/routers/admin_table.py new file mode 100644 index 0000000..949c512 --- /dev/null +++ b/backend/app/routers/admin_table.py @@ -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) diff --git a/backend/app/routers/admin_user.py b/backend/app/routers/admin_user.py new file mode 100644 index 0000000..e9b2537 --- /dev/null +++ b/backend/app/routers/admin_user.py @@ -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} + diff --git a/backend/app/routers/bell.py b/backend/app/routers/bell.py new file mode 100644 index 0000000..e715bd4 --- /dev/null +++ b/backend/app/routers/bell.py @@ -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"} + diff --git a/backend/app/routers/user_announcement.py b/backend/app/routers/user_announcement.py new file mode 100644 index 0000000..fb8a987 --- /dev/null +++ b/backend/app/routers/user_announcement.py @@ -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() diff --git a/backend/app/routers/user_auth.py b/backend/app/routers/user_auth.py new file mode 100644 index 0000000..72d37b2 --- /dev/null +++ b/backend/app/routers/user_auth.py @@ -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, "微信绑定服务暂时不可用") \ No newline at end of file diff --git a/backend/app/routers/user_coupon.py b/backend/app/routers/user_coupon.py new file mode 100644 index 0000000..0e720e4 --- /dev/null +++ b/backend/app/routers/user_coupon.py @@ -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) diff --git a/backend/app/routers/user_game.py b/backend/app/routers/user_game.py new file mode 100644 index 0000000..4907db4 --- /dev/null +++ b/backend/app/routers/user_game.py @@ -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() diff --git a/backend/app/routers/user_group.py b/backend/app/routers/user_group.py new file mode 100644 index 0000000..201a95e --- /dev/null +++ b/backend/app/routers/user_group.py @@ -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() diff --git a/backend/app/routers/user_info.py b/backend/app/routers/user_info.py new file mode 100644 index 0000000..83e19e2 --- /dev/null +++ b/backend/app/routers/user_info.py @@ -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) diff --git a/backend/app/routers/user_messages.py b/backend/app/routers/user_messages.py new file mode 100644 index 0000000..f0c29c9 --- /dev/null +++ b/backend/app/routers/user_messages.py @@ -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() diff --git a/backend/app/routers/user_order.py b/backend/app/routers/user_order.py new file mode 100644 index 0000000..c07f63e --- /dev/null +++ b/backend/app/routers/user_order.py @@ -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() diff --git a/backend/app/routers/user_table.py b/backend/app/routers/user_table.py new file mode 100644 index 0000000..4b3f2d4 --- /dev/null +++ b/backend/app/routers/user_table.py @@ -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) \ No newline at end of file diff --git a/backend/app/schemas/admin_auth.py b/backend/app/schemas/admin_auth.py new file mode 100644 index 0000000..a73d59f --- /dev/null +++ b/backend/app/schemas/admin_auth.py @@ -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 diff --git a/backend/app/schemas/game_info.py b/backend/app/schemas/game_info.py new file mode 100644 index 0000000..a44025a --- /dev/null +++ b/backend/app/schemas/game_info.py @@ -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="管理员身份验证令牌" + ) \ No newline at end of file diff --git a/backend/app/schemas/message_info.py b/backend/app/schemas/message_info.py new file mode 100644 index 0000000..6df6a8d --- /dev/null +++ b/backend/app/schemas/message_info.py @@ -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 diff --git a/backend/app/schemas/order_info.py b/backend/app/schemas/order_info.py new file mode 100644 index 0000000..d89cbea --- /dev/null +++ b/backend/app/schemas/order_info.py @@ -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 diff --git a/backend/app/schemas/table_info.py b/backend/app/schemas/table_info.py new file mode 100644 index 0000000..a63d0c9 --- /dev/null +++ b/backend/app/schemas/table_info.py @@ -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 diff --git a/backend/app/schemas/user_auth.py b/backend/app/schemas/user_auth.py new file mode 100644 index 0000000..2e7bb0b --- /dev/null +++ b/backend/app/schemas/user_auth.py @@ -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 \ No newline at end of file diff --git a/backend/app/schemas/user_info.py b/backend/app/schemas/user_info.py new file mode 100644 index 0000000..cfb4293 --- /dev/null +++ b/backend/app/schemas/user_info.py @@ -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 + diff --git a/backend/app/services/admin_coupon_service.py b/backend/app/services/admin_coupon_service.py new file mode 100644 index 0000000..e4c304e --- /dev/null +++ b/backend/app/services/admin_coupon_service.py @@ -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)) diff --git a/backend/app/services/admin_game_service.py b/backend/app/services/admin_game_service.py new file mode 100644 index 0000000..65743fd --- /dev/null +++ b/backend/app/services/admin_game_service.py @@ -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() + diff --git a/backend/app/services/admin_group_service.py b/backend/app/services/admin_group_service.py new file mode 100644 index 0000000..d32c232 --- /dev/null +++ b/backend/app/services/admin_group_service.py @@ -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() diff --git a/backend/app/services/admin_order_service.py b/backend/app/services/admin_order_service.py new file mode 100644 index 0000000..255d17f --- /dev/null +++ b/backend/app/services/admin_order_service.py @@ -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() diff --git a/backend/app/services/admin_table_service.py b/backend/app/services/admin_table_service.py new file mode 100644 index 0000000..142645b --- /dev/null +++ b/backend/app/services/admin_table_service.py @@ -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() diff --git a/backend/app/services/admin_user_service.py b/backend/app/services/admin_user_service.py new file mode 100644 index 0000000..1a233cd --- /dev/null +++ b/backend/app/services/admin_user_service.py @@ -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() + diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..1d0380d --- /dev/null +++ b/backend/app/services/auth_service.py @@ -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()) diff --git a/backend/app/services/coupons.py b/backend/app/services/coupons.py new file mode 100644 index 0000000..fdf70eb --- /dev/null +++ b/backend/app/services/coupons.py @@ -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 diff --git a/backend/app/services/user_coupon_service.py b/backend/app/services/user_coupon_service.py new file mode 100644 index 0000000..1b6be49 --- /dev/null +++ b/backend/app/services/user_coupon_service.py @@ -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() \ No newline at end of file diff --git a/backend/app/services/user_getInfo_service.py b/backend/app/services/user_getInfo_service.py new file mode 100644 index 0000000..c975565 --- /dev/null +++ b/backend/app/services/user_getInfo_service.py @@ -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() diff --git a/backend/app/services/user_login_service.py b/backend/app/services/user_login_service.py new file mode 100644 index 0000000..2debf40 --- /dev/null +++ b/backend/app/services/user_login_service.py @@ -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() diff --git a/backend/app/services/user_order_service.py b/backend/app/services/user_order_service.py new file mode 100644 index 0000000..28bc636 --- /dev/null +++ b/backend/app/services/user_order_service.py @@ -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() diff --git a/backend/app/services/user_table_service.py b/backend/app/services/user_table_service.py new file mode 100644 index 0000000..bc00c9a --- /dev/null +++ b/backend/app/services/user_table_service.py @@ -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() diff --git a/backend/app/utils/create_database.py b/backend/app/utils/create_database.py new file mode 100644 index 0000000..3ee836c --- /dev/null +++ b/backend/app/utils/create_database.py @@ -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() diff --git a/backend/app/utils/jwt_handler.py b/backend/app/utils/jwt_handler.py new file mode 100644 index 0000000..843a8f5 --- /dev/null +++ b/backend/app/utils/jwt_handler.py @@ -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") diff --git a/backend/app/utils/sms_sender.py b/backend/app/utils/sms_sender.py new file mode 100644 index 0000000..bd5880f --- /dev/null +++ b/backend/app/utils/sms_sender.py @@ -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 diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..d807ba2 --- /dev/null +++ b/frontend/app.py @@ -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) diff --git a/frontend/config.py b/frontend/config.py new file mode 100644 index 0000000..95e72d3 --- /dev/null +++ b/frontend/config.py @@ -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') diff --git a/frontend/routes/auth.py b/frontend/routes/auth.py new file mode 100644 index 0000000..524ded1 --- /dev/null +++ b/frontend/routes/auth.py @@ -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')) diff --git a/frontend/routes/coupons.py b/frontend/routes/coupons.py new file mode 100644 index 0000000..3100209 --- /dev/null +++ b/frontend/routes/coupons.py @@ -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/', 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')) + diff --git a/frontend/routes/dashboard.py b/frontend/routes/dashboard.py new file mode 100644 index 0000000..06de4d6 --- /dev/null +++ b/frontend/routes/dashboard.py @@ -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 diff --git a/frontend/routes/games.py b/frontend/routes/games.py new file mode 100644 index 0000000..5f75fd2 --- /dev/null +++ b/frontend/routes/games.py @@ -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/', 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/', 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/', 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 \ No newline at end of file diff --git a/frontend/routes/groups.py b/frontend/routes/groups.py new file mode 100644 index 0000000..2966e74 --- /dev/null +++ b/frontend/routes/groups.py @@ -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')) diff --git a/frontend/routes/messages.py b/frontend/routes/messages.py new file mode 100644 index 0000000..ab0cdf6 --- /dev/null +++ b/frontend/routes/messages.py @@ -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/', 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 + + diff --git a/frontend/routes/orders.py b/frontend/routes/orders.py new file mode 100644 index 0000000..7a45bd8 --- /dev/null +++ b/frontend/routes/orders.py @@ -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/', 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/', 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/', 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 \ No newline at end of file diff --git a/frontend/routes/tables.py b/frontend/routes/tables.py new file mode 100644 index 0000000..ae362c3 --- /dev/null +++ b/frontend/routes/tables.py @@ -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/', 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/', 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')) diff --git a/frontend/routes/users.py b/frontend/routes/users.py new file mode 100644 index 0000000..ed2eaa3 --- /dev/null +++ b/frontend/routes/users.py @@ -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')) diff --git a/frontend/templates/_order_table.html b/frontend/templates/_order_table.html new file mode 100644 index 0000000..3b5cd19 --- /dev/null +++ b/frontend/templates/_order_table.html @@ -0,0 +1,323 @@ +{% set page = page | default(1) %} +{% set total_pages = total_pages | default(1) %} +{% set status = status | default('in_progress') %} + + + + + + + + + + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
订单ID用户名称 游戏桌号 游戏人数开始时间结束时间结算时间应付金额实付金额积分使用 优惠券 游戏时长 支付方式状态操作
{{ order.order_id }}{{ order.user_name }} {{ order.game_table_number or '未分配' }} {{ order.num_players }}{{ order.start_datetime }}{{ order.end_datetime or '进行中' }}{{ order.settlement_time or '未付款' }}¥{{ "%.2f"|format(order.payable_price|default('0')|float) }}¥{{ "%.2f"|format(order.paid_price|default('0')|float) }}{{ order.used_points or '0' }} 积分{{ order.coupon_id or '无' }}{{ order.game_process_time or '0' }} 分钟 + {% if order.order_status not in ['pending', 'in_progress'] %} + {% if order.payment_method == 'offline' %} + 线下支付 + {% else %} + {{ order.payment_method | default('现金支付') }} + {% endif %} + {% else %} + - + {% endif %} + + + {{ order.order_status }} + + + {% if order.order_status == 'in_progress' %} + + {% elif order.order_status == 'pending' %} + + {% endif %} +
暂无订单数据
+ + + + + + + + + + diff --git a/frontend/templates/announcements.html b/frontend/templates/announcements.html new file mode 100644 index 0000000..560977d --- /dev/null +++ b/frontend/templates/announcements.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} + +{% block content %} +
+

公告管理

+ + + + + + + + + + + + + + {% for ann in announcements %} + + + + + + + + {% else %} + + + + {% endfor %} + +
ID内容时间范围颜色操作
{{ ann.id }}{{ ann.text }} + + {{ ann.start_time|datetime }} 至 {{ ann.end_time|datetime }} + + +
+
+
+ +
+
暂无公告
+ + + +
+{% endblock %} diff --git a/frontend/templates/base.html b/frontend/templates/base.html new file mode 100644 index 0000000..f2b2d11 --- /dev/null +++ b/frontend/templates/base.html @@ -0,0 +1,147 @@ + + + + + {% block title %}桌游厅点单系统管理后台{% endblock %} + + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/frontend/templates/coupons/list.html b/frontend/templates/coupons/list.html new file mode 100644 index 0000000..e06c33e --- /dev/null +++ b/frontend/templates/coupons/list.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} +{% block title %}优惠券管理{% endblock %} + +{% block content %} +
+

优惠券管理

+
+
创建新优惠券
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + {% for coupon in coupons %} + + + + + + + + + + + {% endfor %} + +
优惠券ID优惠码折扣类型折扣值最低消费有效期库存操作
{{ coupon.coupon_id }}{{ coupon.coupon_code }}{{ coupon.discount_type }}{{ coupon.discount_amount }}{{ coupon.min_order_amount or '无限制' }}{{ coupon.valid_from }} 至 {{ coupon.valid_to }}{{ coupon.quantity }} +
+ +
+
+
+{% endblock %} diff --git a/frontend/templates/dashboard.html b/frontend/templates/dashboard.html new file mode 100644 index 0000000..ed687af --- /dev/null +++ b/frontend/templates/dashboard.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}管理首页{% endblock %} + +{% block content %} +
+

欢迎来到桌游厅点单系统管理后台

+

请选择导航栏中的功能进行操作。

+
+{% endblock %} diff --git a/frontend/templates/games/add.html b/frontend/templates/games/add.html new file mode 100644 index 0000000..82091af --- /dev/null +++ b/frontend/templates/games/add.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% block content %} +
+

添加新游戏

+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+
+ + +
+ + + 返回 +
+
+{% endblock %} diff --git a/frontend/templates/games/edit.html b/frontend/templates/games/edit.html new file mode 100644 index 0000000..671688d --- /dev/null +++ b/frontend/templates/games/edit.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block content %} +
+

编辑游戏信息

+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + +
+
+ + + + + 返回 +
+
+{% endblock %} diff --git a/frontend/templates/games/list.html b/frontend/templates/games/list.html new file mode 100644 index 0000000..a101310 --- /dev/null +++ b/frontend/templates/games/list.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} + +{% block content %} +
+

游戏管理

+ 添加新游戏 + + + + + + + + + + + + + + + + {% for game in games %} + + + + + + + + + + + {% endfor %} + +
游戏名称库存数量类型人数范围时长(分钟)价格倍率可用操作
{{ game.game_name }}{{ game.quantity }}{{ '剧本杀' if game.game_type == 0 else '桌游' }}{{ game.min_players }}-{{ game.max_players }}人{{ game.duration }}{{ "%.2f"|format(game.price) }}{{ '是' if game.is_available else '否' }} + 编辑 +
+ +
+ 设置封面 +
+
+ + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/frontend/templates/games/tags.html b/frontend/templates/games/tags.html new file mode 100644 index 0000000..be5db81 --- /dev/null +++ b/frontend/templates/games/tags.html @@ -0,0 +1,185 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ +
+ +
+
新建标签
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
批量关联标签与游戏
+
+
+
+
+ +
+
+
+ {% for game in all_games %} +
+ + +
+ {% endfor %} +
+
+
+ +
+
+
+
+ + +
+ +
+
已有标签
+
+ + + + + + + + + + + {% for tag in tags %} + + + + + + + {% endfor %} + +
标签ID标签名称关联游戏数操作
{{ tag.tag_id }}{{ tag.tag_name }}{{ tag.game_count }} + +
+
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/frontend/templates/groups/list.html b/frontend/templates/groups/list.html new file mode 100644 index 0000000..114c5d8 --- /dev/null +++ b/frontend/templates/groups/list.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block content %} +
+

拼团管理

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+ + + + + + + + + + + + + + {% for group in groups %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
拼团ID团队名称发起人最大人数状态创建时间操作
{{ group.group_id }}{{ group.group_name }}{{ group.leader_name or '未知' }}{{ group.max_members }} + + {{ '招募中' if group.group_status == 'recruiting' + else '已满员' if group.group_status == 'full' + else '已结束' }} + + {{ group.created_at }} +
+ + +
+
暂无进行中的拼团
+
+
+
+
+{% endblock %} diff --git a/frontend/templates/login.html b/frontend/templates/login.html new file mode 100644 index 0000000..40b975c --- /dev/null +++ b/frontend/templates/login.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}管理员登录{% endblock %} + +{% block content %} +
+
+

管理员登录

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/frontend/templates/messages/list.html b/frontend/templates/messages/list.html new file mode 100644 index 0000000..3c69a42 --- /dev/null +++ b/frontend/templates/messages/list.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block title %}留言管理{% endblock %} + +{% block content %} +
+

用户留言管理

+ + + + + + + + + + + + + {% for message in messages %} + + + + + + + + {% endfor %} + +
用户ID用户名留言内容留言时间操作
{{ message.user_id }}{{ message.username }}{{ message.message_content }}{{ message.created_at }} + +
+
+ + +{% endblock %} diff --git a/frontend/templates/orders.html b/frontend/templates/orders.html new file mode 100644 index 0000000..1ad6a18 --- /dev/null +++ b/frontend/templates/orders.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block content %} +
+

订单列表

+ + + + + +
+ {% include '_order_table.html' %} +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/frontend/templates/refresh.html b/frontend/templates/refresh.html new file mode 100644 index 0000000..d0c20e4 --- /dev/null +++ b/frontend/templates/refresh.html @@ -0,0 +1,15 @@ + + + + + 正在跳转… + + +

登录成功,3秒后将自动跳转到后台首页…

+ + + \ No newline at end of file diff --git a/frontend/templates/tables/add.html b/frontend/templates/tables/add.html new file mode 100644 index 0000000..0fa31e1 --- /dev/null +++ b/frontend/templates/tables/add.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}添加桌台{% endblock %} + +{% block content %} +
+

添加新桌台

+
+
+ + +
+
+ + +
+
+ + +
+ + 取消 +
+
+{% endblock %} diff --git a/frontend/templates/tables/edit.html b/frontend/templates/tables/edit.html new file mode 100644 index 0000000..e500c9a --- /dev/null +++ b/frontend/templates/tables/edit.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block title %}编辑桌台{% endblock %} + +{% block content %} +
+

编辑桌台 #{{ table.game_table_number }}

+
+
+ + +
+
+ + +
+
+ + +
+ + 取消 +
+
+{% endblock %} diff --git a/frontend/templates/tables/list.html b/frontend/templates/tables/list.html new file mode 100644 index 0000000..9cdf3c0 --- /dev/null +++ b/frontend/templates/tables/list.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block title %}桌台管理{% endblock %} + +{% block content %} +
+

桌台管理

+ 添加新桌台 + + + + + + + + + + + + + {% for table in tables %} + + + + + + + + {% endfor %} + +
id桌号容量价格倍率操作
{{ table.table_id }}{{ table.game_table_number }}{{ table.capacity }}人{{ "%.2f"|format(table.price) }} + 编辑 +
+ +
+
+
+{% endblock %} diff --git a/frontend/templates/users.html b/frontend/templates/users.html new file mode 100644 index 0000000..cf20105 --- /dev/null +++ b/frontend/templates/users.html @@ -0,0 +1,374 @@ +{% extends "base.html" %} +{% block title %}用户管理{% endblock %} + +{% block content %} +
+ +
+
+

用户列表

+
+
+
+ + + +
+
+
+

当前用户总数:{{ total_users }}

+ + + + + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + + + + {% endfor %} + +
用户ID用户名积分性别手机号邮箱用户类型创建时间更新时间操作
{{ user.user_id }}{{ user.username }}{{ user.points }}{{ user.gender or '' }}{{ user.phone_number or '' }}{{ user.email or '' }}{{ user.user_type }}{{ user.created_at }}{{ user.updated_at }} + + + + + + + +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + +{% endblock %}