价格计算策略更新

This commit is contained in:
ahao 2025-03-20 04:41:04 +08:00
parent 127a2734ef
commit adbe039e3d
17 changed files with 1296 additions and 195 deletions

12
.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="tg01@localhost" uuid="3d275efa-2b35-4106-ba1e-b855e94a8dfa">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/tg01</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@ -20,6 +20,7 @@ from .routers import user_coupon
from .routers import admin_message
from .routers import user_messages
from .routers import bell
from .routers import admin_strategy
@ -61,6 +62,7 @@ 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(admin_strategy.router, prefix="/admin", tags=["Admin-Strategy"])

View File

@ -0,0 +1,57 @@
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List
from ..services.admin_strategy_service import (
get_table_strategy_list,
create_table_strategy,
delete_table_strategy,
bind_table_to_strategy
)
router = APIRouter()
class get_list(BaseModel):
token: str
class StrategyData(BaseModel):
strategy_name: str
segment1_threshold: int
segment1_price: float
segment2_threshold: int
segment2_price: float
segment3_price: float
description: str | None = None
segment3_threshold: int | None = None
class create_strategy(BaseModel):
token: str
strategy_data: StrategyData
class delete_strategy(BaseModel):
token: str
strategy_id: int
class update_strategy(BaseModel):
token: str
table_ids: List[int]
strategy_id: int
@router.post("/table-strategies/list")
def api_get_table_strategy_list(request: get_list):
return get_table_strategy_list(request.token)
@router.post("/table-strategies/create")
def api_create_table_strategy(request: create_strategy):
return create_table_strategy(request.token, request.strategy_data.model_dump())
@router.post("/table-strategies/delete")
def api_delete_table_strategy(request: delete_strategy):
return delete_table_strategy(request.token, request.strategy_id)
@router.post("/table-strategies/bind")
def api_bind_table_to_strategy(request: update_strategy):
return bind_table_to_strategy(request.token, request.strategy_id, request.table_ids)

View File

@ -3,16 +3,17 @@ from pydantic import BaseModel
class TableCreate(BaseModel):
game_table_number: str
capacity: int
price: float
strategy_id: int
strategy_name: str
class TableCreateRequest(BaseModel):
token: str
game_table_number: str
capacity: int
price: float
strategy_id: int
class TableResponse(BaseModel):
table_id: int
game_table_number: str
capacity: int
price: float
strategy_name: str

View File

@ -0,0 +1,145 @@
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 get_table_strategy_list(token: str):
"""
获取 table 的策略列表
"""
_verify_admin_permission(token)
print("get_table_strategy_list")
connection = get_connection()
cursor = None
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT * FROM table_pricing_strategies")
strategies = cursor.fetchall()
return strategies
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if cursor:
cursor.close()
connection.close()
def create_table_strategy(token: str, strategy_data: dict):
"""
创建 table 的策略
"""
_verify_admin_permission(token)
connection = get_connection()
cursor = None
required_fields = ['strategy_name', 'segment1_threshold', 'segment1_price',
'segment2_threshold', 'segment2_price', 'segment3_price']
missing_fields = [field for field in required_fields if field not in strategy_data]
if missing_fields:
raise HTTPException(status_code=400, detail=f"缺少必填字段: {', '.join(missing_fields)}")
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("""
INSERT INTO table_pricing_strategies (
strategy_name,
description,
segment1_threshold,
segment1_price,
segment2_threshold,
segment2_price,
segment3_price,
segment3_threshold
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""", (
strategy_data['strategy_name'],
strategy_data.get('description'), # 可选字段
strategy_data['segment1_threshold'],
strategy_data['segment1_price'],
strategy_data['segment2_threshold'],
strategy_data['segment2_price'],
strategy_data['segment3_price'],
strategy_data.get('segment3_threshold') # 可选字段
))
connection.commit()
return {"message": "Table 策略创建成功"}
except Exception as e:
connection.rollback()
raise HTTPException(status_code=500, detail=str(e))
finally:
if cursor:
cursor.close()
connection.close()
def delete_table_strategy(token: str, strategy_id: int):
"""
删除 table 的策略
"""
_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 table_pricing_strategy_id = %s", (strategy_id,))
result = cursor.fetchone()
if result['count'] > 0:
raise HTTPException(status_code=400, detail="该策略正在被使用,不可删除")
# 删除策略
cursor.execute("DELETE FROM table_pricing_strategies WHERE strategy_id = %s", (strategy_id,))
connection.commit()
return {"message": "Table 策略删除成功"}
except Exception as e:
connection.rollback()
raise HTTPException(status_code=500, detail=str(e))
finally:
if cursor:
cursor.close()
connection.close()
def bind_table_to_strategy(token: str, strategy_id: int, table_ids: list):
"""
绑定桌子到指定策略
"""
_verify_admin_permission(token)
connection = get_connection()
cursor = None
try:
cursor = connection.cursor(dictionary=True)
# 批量更新桌子对应的策略
update_query = "UPDATE game_tables SET table_pricing_strategy_id = %s WHERE table_id = %s"
params = [(strategy_id, table_id) for table_id in table_ids]
cursor.executemany(update_query, params)
connection.commit()
return {"message": f"成功绑定{len(table_ids)}张桌子到指定策略"}
except Exception as e:
connection.rollback()
raise HTTPException(status_code=500, detail=str(e))
finally:
if cursor:
cursor.close()
connection.close()

View File

@ -30,6 +30,9 @@ def create_table(token: str, table_data: dict) -> dict:
try:
cursor = connection.cursor(dictionary=True)
# 设置默认价格倍率(原本的价格计算方法,已经弃用,防止报错)
default_price = 1.00
# 检查桌号是否已存在
cursor.execute(
"SELECT COUNT(*) AS count FROM game_tables WHERE game_table_number = %s",
@ -38,9 +41,18 @@ def create_table(token: str, table_data: dict) -> dict:
if cursor.fetchone()['count'] > 0:
raise HTTPException(status_code=400, detail="桌号已存在")
if 'strategy_id' in table_data and table_data['strategy_id']:
cursor.execute(
"SELECT COUNT(*) AS count FROM table_pricing_strategies WHERE strategy_id = %s",
(table_data['strategy_id'],)
)
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'])
"INSERT INTO game_tables (game_table_number, capacity, price, table_pricing_strategy_id) VALUES (%s, %s, %s, %s)",
(
table_data['game_table_number'], table_data['capacity'], default_price, table_data.get('strategy_id'))
)
connection.commit()
return {"message": "桌台创建成功"}
@ -93,13 +105,18 @@ def update_table(token: str, table_id: int, table_data: dict) -> dict:
if cursor.fetchone()['count'] > 0:
raise HTTPException(status_code=400, detail="桌号已存在")
# 设置默认价格倍率(原本的价格计算方法,已经弃用,防止报错)
default_price = 1.00
cursor.execute(
"""UPDATE game_tables SET
game_table_number=%s,
capacity=%s,
price=%s
price=%s,
table_pricing_strategy_id=%s
WHERE table_id=%s""",
(table_data['game_table_number'], table_data['capacity'], table_data['price'], table_id)
(
table_data['game_table_number'], table_data['capacity'], default_price, table_data.get('strategy_id'),
table_id)
)
if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="桌台不存在")
@ -119,10 +136,27 @@ def list_tables_service(token: str):
cursor = None
try:
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT * FROM game_tables")
# 修改查询语句,关联 pricing_strategies 表获取 strategy_name
cursor.execute("""
SELECT
gt.table_id,
gt.game_table_number,
gt.capacity,
gt.price,
gt.table_pricing_strategy_id,
tps.strategy_name
FROM
game_tables gt
JOIN
table_pricing_strategies tps
ON
gt.table_pricing_strategy_id = tps.strategy_id;
""")
return cursor.fetchall()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if cursor: cursor.close()
if cursor:
cursor.close()
connection.close()

View File

@ -38,7 +38,7 @@ def check_table_occupancy_service(table_id: int):
) AS is_occupied
""", (table_id,))
result = cursor.fetchone()
return {"is_occupied": "false"} # 不检测桌子状态
return {"is_occupied": 0} # 不检测桌子状态
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:

View File

@ -44,25 +44,26 @@ def calculate_segmented_price(duration, strategy):
if s3 is not None and duration > s3:
duration = s3 # 限制最大计费时间
price = (s1 * p1) + ((s2 - s1) * p2) + ((duration - s2) * p3)
return price
def calculate_overtime_fee(start_datetime, end_datetime):
"""计算超时费用(深夜时段收费)"""
"""计算超时费用(深夜时段收费,按整小时计费)"""
# 读取配置文件
config = configparser.ConfigParser()
config.read('backend/config.conf')
stay_up_late = config['stay_up_late']
start_time = datetime.strptime(stay_up_late['start_time'], '%H:%M:%S').time()
end_time = datetime.strptime(stay_up_late['end_time'], '%H:%M:%S').time()
price_per_minute = Decimal(stay_up_late['price_minutes'])
price_per_hour = Decimal(stay_up_late['price_minutes']) # 每小时收费
overtime_fee = Decimal(0)
current_time = start_datetime
current_time = start_datetime.replace(minute=0, second=0, microsecond=0) # 取整到整点
while current_time < end_datetime:
while current_time + timedelta(hours=1) <= end_datetime:
next_hour = current_time + timedelta(hours=1)
if start_time <= current_time.time() or current_time.time() < end_time:
overtime_fee += price_per_minute
current_time += timedelta(minutes=1)
overtime_fee += price_per_hour # 满一小时才计费
current_time = next_hour # 跳到下一个整点
return overtime_fee
@ -100,22 +101,22 @@ def calculate_order_price(order_id):
raise HTTPException(status_code=400, detail="价格策略未找到")
# 计算价格
unit_price = calculate_segmented_price(duration_dec, pricing_strategy)
# unit_price = calculate_segmented_price(duration_dec, pricing_strategy)
# 原本的基础价格废用,桌子价格为主要逻辑
table_price = calculate_segmented_price(duration_dec, table_strategy)
# 计算总价格
base_price = (unit_price * order['num_players']) + table_price
# base_price = (unit_price * order['num_players']) + table_price
base_price = table_price * order['num_players']
overtime_fee = calculate_overtime_fee(start_time, end_time)
total_price = base_price + overtime_fee
# print("总价: ", unit_price * order['num_players'], " 桌费: ", table_price, " 时长: ", duration_dec, " 超时费: ", overtime_fee)
# 更新订单价格
cursor.execute("""
UPDATE orders
SET payable_price = %s,
game_process_time = %s,
overtime_fee = %s
game_process_time = %s
WHERE order_id = %s
""", (total_price, duration_dec, overtime_fee, order_id))
""", (total_price, duration_dec, order_id))
connection.commit()
return True

View File

@ -19,6 +19,23 @@ SQL_SCRIPT = """
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for announcements
-- ----------------------------
DROP TABLE IF EXISTS `announcements`;
CREATE TABLE `announcements` (
`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,
`created_at` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for coupons
-- ----------------------------
DROP TABLE IF EXISTS `coupons`;
CREATE TABLE `coupons` (
`coupon_id` int NOT NULL AUTO_INCREMENT,
@ -26,31 +43,24 @@ CREATE TABLE `coupons` (
`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,
`valid_from` datetime DEFAULT NULL,
`valid_to` datetime 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,
`quantity` int DEFAULT NULL,
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;
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for game_groups
-- ----------------------------
DROP TABLE IF EXISTS `game_groups`;
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,
`user_id` int NOT NULL,
`group_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
`start_date` date DEFAULT NULL,
`start_time` datetime DEFAULT NULL,
@ -59,49 +69,43 @@ CREATE TABLE `game_groups` (
`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,
`play_start_time` datetime DEFAULT NULL,
`play_end_time` datetime DEFAULT NULL,
PRIMARY KEY (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for game_tables
-- ----------------------------
DROP TABLE IF EXISTS `game_tables`;
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;
`table_pricing_strategy_id` int DEFAULT NULL,
PRIMARY KEY (`table_id`),
KEY `tables_price` (`table_pricing_strategy_id`),
CONSTRAINT `tables_price` FOREIGN KEY (`table_pricing_strategy_id`) REFERENCES `table_pricing_strategies` (`strategy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for game_tags
-- ----------------------------
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`)
`game_id` int NOT NULL,
`tag_id` int NOT NULL,
PRIMARY KEY (`game_id`,`tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `game_tags_ibfk_1` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`),
CONSTRAINT `game_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for games
-- ----------------------------
DROP TABLE IF EXISTS `games`;
CREATE TABLE `games` (
`game_id` int NOT NULL AUTO_INCREMENT,
`game_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
@ -109,17 +113,21 @@ CREATE TABLE `games` (
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
`min_players` int DEFAULT NULL,
`max_players` int DEFAULT NULL,
`duration` int DEFAULT NULL,
`duration` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
`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,
`photo_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`long_description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
PRIMARY KEY (`game_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for group_members
-- ----------------------------
DROP TABLE IF EXISTS `group_members`;
CREATE TABLE `group_members` (
`group_member_id` int NOT NULL AUTO_INCREMENT,
@ -130,8 +138,11 @@ CREATE TABLE `group_members` (
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;
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for order_coupons
-- ----------------------------
DROP TABLE IF EXISTS `order_coupons`;
CREATE TABLE `order_coupons` (
`order_coupon_id` int NOT NULL AUTO_INCREMENT,
@ -144,6 +155,9 @@ CREATE TABLE `order_coupons` (
CONSTRAINT `order_coupons_ibfk_2` FOREIGN KEY (`coupon_id`) REFERENCES `coupons` (`coupon_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`order_id` int NOT NULL COMMENT '订单 id',
@ -156,34 +170,96 @@ CREATE TABLE `orders` (
`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 '优惠后价格',
`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,
`wx_transaction_id` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
`out_trade_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`pricing_strategy_id` int NOT NULL COMMENT '绑定的价格策略 ID',
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`),
KEY `orders_ibfk_4` (`pricing_strategy_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`)
CONSTRAINT `orders_ibfk_3` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`),
CONSTRAINT `orders_ibfk_4` FOREIGN KEY (`pricing_strategy_id`) REFERENCES `pricing_strategies` (`strategy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for player_messages
-- ----------------------------
DROP TABLE IF EXISTS `player_messages`;
CREATE TABLE `player_messages` (
`message_id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`message_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `player_messages_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for player_reviews
-- ----------------------------
DROP TABLE IF EXISTS `player_reviews`;
CREATE TABLE `player_reviews` (
`review_id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`game_id` int NOT NULL,
`rating` tinyint NOT NULL COMMENT '玩家的打分,范围可以根据实际情况设定,如 1 - 5 星',
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '玩家的评论内容,可以为空',
`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 `player_reviews_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
CONSTRAINT `player_reviews_ibfk_2` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for points_history
-- ----------------------------
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,
`reason` varchar(255) CHARACTER SET utf8mb4 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;
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for pricing_strategies
-- ----------------------------
DROP TABLE IF EXISTS `pricing_strategies`;
CREATE TABLE `pricing_strategies` (
`strategy_id` int NOT NULL AUTO_INCREMENT COMMENT '价格策略 ID',
`strategy_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '策略名称',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '策略描述',
`segment1_threshold` int NOT NULL COMMENT '第一阶段时间(分钟)',
`segment1_price` decimal(10,2) NOT NULL COMMENT '第一阶段单价(元/人/分钟)',
`segment2_threshold` int NOT NULL COMMENT '第二阶段时间(分钟)',
`segment2_price` decimal(10,2) NOT NULL COMMENT '第二阶段单价(元/人/分钟)',
`segment3_price` decimal(10,2) NOT NULL COMMENT '第三阶段单价(元/人/分钟)',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`segment3_threshold` int DEFAULT NULL COMMENT '第三阶段时间上限(分钟,可选)',
PRIMARY KEY (`strategy_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for reviews
-- ----------------------------
DROP TABLE IF EXISTS `reviews`;
CREATE TABLE `reviews` (
`review_id` int NOT NULL AUTO_INCREMENT,
@ -200,6 +276,77 @@ CREATE TABLE `reviews` (
CONSTRAINT `reviews_ibfk_2` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for table_pricing_strategies
-- ----------------------------
DROP TABLE IF EXISTS `table_pricing_strategies`;
CREATE TABLE `table_pricing_strategies` (
`strategy_id` int NOT NULL AUTO_INCREMENT COMMENT '桌费策略 ID',
`strategy_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '策略名称',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '策略描述',
`segment1_threshold` int NOT NULL COMMENT '第一阶段时间(分钟)',
`segment1_price` decimal(10,2) NOT NULL COMMENT '第一阶段单价(元/分钟)',
`segment2_threshold` int NOT NULL COMMENT '第二阶段时间(分钟)',
`segment2_price` decimal(10,2) NOT NULL COMMENT '第二阶段单价(元/分钟)',
`segment3_price` decimal(10,2) NOT NULL COMMENT '第三阶段单价(元/分钟)',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`segment3_threshold` int DEFAULT NULL COMMENT '第三阶段时间上限(分钟,可选)',
PRIMARY KEY (`strategy_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Table structure for tags
-- ----------------------------
DROP TABLE IF EXISTS `tags`;
CREATE TABLE `tags` (
`tag_id` int NOT NULL AUTO_INCREMENT,
`tag_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`tag_id`),
UNIQUE KEY `tag_name` (`tag_name`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for user_coupons
-- ----------------------------
DROP TABLE IF EXISTS `user_coupons`;
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`),
KEY `coupon_id` (`coupon_id`),
KEY `idx_user_status` (`user_id`,`is_used`),
CONSTRAINT `user_coupons_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
CONSTRAINT `user_coupons_ibfk_2` FOREIGN KEY (`coupon_id`) REFERENCES `coupons` (`coupon_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for user_game_rating
-- ----------------------------
DROP TABLE IF EXISTS `user_game_rating`;
CREATE TABLE `user_game_rating` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`game_id` int NOT NULL,
`rating` tinyint(1) NOT NULL COMMENT '拉为 1踩为 0',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `game_id` (`game_id`),
CONSTRAINT `user_game_rating_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
CONSTRAINT `user_game_rating_ibfk_2` FOREIGN KEY (`game_id`) REFERENCES `games` (`game_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`user_id` int NOT NULL AUTO_INCREMENT,
@ -212,11 +359,13 @@ CREATE TABLE `users` (
`points` int DEFAULT '0',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`wx_openid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `phone_number` (`phone_number`)
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
SET FOREIGN_KEY_CHECKS = 1;
"""

View File

@ -19,6 +19,7 @@ def create_app():
from frontend.routes.announcement import announcements_bp
from frontend.routes.coupons import coupons_bp
from frontend.routes.messages import messages_bp
from frontend.routes.strategies import strategies_bp
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
toolbar = DebugToolbarExtension(app)
@ -34,6 +35,7 @@ def create_app():
app.register_blueprint(announcements_bp)
app.register_blueprint(coupons_bp)
app.register_blueprint(messages_bp)
app.register_blueprint(strategies_bp)
# 添加自定义过滤器
@app.template_filter('datetime')

View File

@ -0,0 +1,129 @@
from flask import Blueprint, render_template, session, redirect, url_for, flash, request
import requests
from frontend.config import Config
strategies_bp = Blueprint('strategies', __name__)
@strategies_bp.route('/strategies')
def list_strategies():
if not session.get('token'):
flash("请先登录", "warning")
return redirect(url_for('auth.login'))
token = session.get('token')
try:
resp = requests.post(
f"{Config.BASE_API_URL}/admin/table-strategies/list",
json={"token": token}
)
if resp.status_code == 200:
return render_template('strategies/list.html', strategies=resp.json())
else:
flash("获取策略列表失败", "danger")
except Exception as e:
flash(f"网络错误: {str(e)}", "danger")
return redirect(url_for('dashboard.index'))
@strategies_bp.route('/strategies/create', methods=['POST'])
def create_strategy():
if not session.get('token'):
return redirect(url_for('auth.login'))
try:
# 从JSON请求体获取数据
request_data = request.get_json()
strategy_data = request_data.get('strategy_data')
print(strategy_data.get('strategy_name'))
# 添加数据验证
if not strategy_data:
flash("缺少策略数据", "danger")
return redirect(url_for('strategies.list_strategies'))
# 获取表单数据并转换为下划线格式
form_data = {
"strategy_name": strategy_data.get('strategy_name'),
"segment1_threshold": int(strategy_data.get('segment1_threshold')),
"segment1_price": float(strategy_data.get('segment1_price')),
"segment2_threshold": int(strategy_data.get('segment2_threshold')),
"segment2_price": float(strategy_data.get('segment2_price')),
"segment3_price": float(strategy_data.get('segment3_price')),
"description": strategy_data.get('description'),
"segment3_threshold": int(strategy_data['segment3_threshold']) if strategy_data.get(
'segment3_threshold') else None
}
resp = requests.post(
f"{Config.BASE_API_URL}/admin/table-strategies/create",
json={
"token": session['token'],
"strategy_data": form_data
}
)
if resp.status_code == 200:
flash("策略创建成功", "success")
return {"status": "success"}, 200
else:
error_msg = resp.json().get('detail', '未知错误')
flash(f"创建失败: {error_msg}", "danger")
except ValueError as e:
flash(f"数据类型错误: {str(e)}", "danger")
except Exception as e:
flash(f"网络错误: {str(e)}", "danger")
return redirect(url_for('strategies.list_strategies'))
@strategies_bp.route('/strategies/delete/<int:strategy_id>', methods=['POST'])
def delete_strategy(strategy_id):
if not session.get('token'):
return redirect(url_for('auth.login'))
try:
resp = requests.post(
f"{Config.BASE_API_URL}/admin/table-strategies/delete",
json={
"token": session['token'],
"strategy_id": strategy_id
}
)
if resp.status_code == 200:
flash("策略删除成功", "success")
return {"status": "success"}, 200
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('strategies.list_strategies'))
@strategies_bp.route('/strategies/bind_tables', methods=['POST'])
def bind_tables():
if not session.get('token'):
return {'status': 'error', 'message': '请先登录'}, 401
data = request.get_json()
strategy_id = data.get('strategy_id')
table_ids = data.get('table_ids')
if not strategy_id or not table_ids:
return {'status': 'error', 'message': '缺少必要参数'}, 400
try:
resp = requests.post(
f"{Config.BASE_API_URL}/admin/table-strategies/bind",
json={
"token": session['token'],
"strategy_id": strategy_id,
"table_ids": table_ids
}
)
if resp.status_code == 200:
return {'status': 'success', 'message': '绑定成功'}
else:
error_msg = resp.json().get('detail', '未知错误')
return {'status': 'error', 'message': f'绑定失败: {error_msg}'}, resp.status_code
except Exception as e:
return {'status': 'error', 'message': f'网络错误: {str(e)}'}, 500

View File

@ -18,6 +18,7 @@ def list_tables():
if resp.status_code != 200:
flash("获取桌台列表失败", "danger")
return redirect(url_for('dashboard.index'))
# print (resp.json())
return render_template('tables/list.html', tables=resp.json())
except Exception as e:
@ -29,13 +30,24 @@ def add_table():
if not session.get('token'):
return redirect(url_for('auth.login'))
strategies = []
try:
strategy_resp = requests.post(
f"{Config.BASE_API_URL}/admin/table-strategies/list",
json={"token": session['token']}
)
strategies = strategy_resp.json() if strategy_resp.status_code == 200 else []
except Exception as e:
flash(f"获取策略列表失败: {str(e)}", "warning")
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'])
"strategy_id" : int(request.form['strategy_id'])
}
resp = requests.post(
f"{Config.BASE_API_URL}/admin/tables/create",
@ -54,20 +66,33 @@ def add_table():
except Exception as e:
flash(f"网络错误: {str(e)}", "danger")
return render_template('tables/add.html')
return render_template('tables/add.html', strategies = strategies )
@tables_bp.route('/tables/edit/<int:table_id>', methods=['GET', 'POST'])
def edit_table(table_id):
if not session.get('token'):
return redirect(url_for('auth.login'))
# 获取策略列表
strategies = []
try:
# 获取策略列表
strategy_resp = requests.post(
f"{Config.BASE_API_URL}/admin/table-strategies/list",
json={"token": session['token']}
)
strategies = strategy_resp.json() if strategy_resp.status_code == 200 else []
except Exception as e:
flash(f"获取策略列表失败: {str(e)}", "warning")
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'])
"strategy_id" : int(request.form['strategy_id'])
}
resp = requests.put(
f"{Config.BASE_API_URL}/admin/tables/{table_id}",
@ -96,7 +121,7 @@ def edit_table(table_id):
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)
return render_template('tables/edit.html', table=table, strategies=strategies)
flash("桌台不存在", "danger")
except Exception as e:
flash(f"获取数据失败: {str(e)}", "danger")
@ -122,3 +147,18 @@ def delete_table(table_id):
flash(f"网络错误: {str(e)}", "danger")
return redirect(url_for('tables.list_tables'))
@tables_bp.route('/admin/tables/list', methods=['GET'])
def list_all_tables():
if not session.get('token'):
return {"detail": "未认证"}, 401
try:
resp = requests.get(
f"{Config.BASE_API_URL}/admin/tables",
headers={"Authorization": f"Bearer {session['token']}"}
)
return resp.json()
except Exception as e:
return {"detail": str(e)}, 500

View File

@ -4,10 +4,16 @@
<meta charset="utf-8">
<title>{% block title %}桌游厅点单系统管理后台{% endblock %}</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
body { font-family: Arial, sans-serif; }
.nav-link { margin-right: 10px; }
body {
font-family: Arial, sans-serif;
}
.nav-link {
margin-right: 10px;
}
@media (min-width: 1200px) {
.container,
.container-lg,
@ -17,8 +23,9 @@
max-width: 2000px !important;
}
}
/* 添加全局通知样式 */
.global-notification {
.global-notification {
position: fixed;
bottom: 20px;
right: 20px;
@ -33,76 +40,79 @@
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<a class="navbar-brand" href="{{ url_for('dashboard.index') }}">桌游厅点单系统</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
{% if session.token %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard.index') }}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('users.list_users') }}">用户管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('orders.index') }}">订单管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('games.list_games') }}">游戏管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('tables.list_tables') }}">桌台管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('games.manage_tags') }}">游戏标签</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('groups.manage_groups') }}">拼团管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('announcements.manage_announcements') }}">公告管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('coupons.list_coupons') }}">优惠券</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('messages.list_messages') }}">留言管理</a>
</li>
{% endif %}
</ul>
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<a class="navbar-brand" href="{{ url_for('dashboard.index') }}">桌游厅点单系统</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
{% if session.token %}
<span class="navbar-text">
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-danger">退出登录</a>
</span>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard.index') }}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('users.list_users') }}">用户管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('orders.index') }}">订单管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('games.list_games') }}">游戏管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('tables.list_tables') }}">桌台管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('games.manage_tags') }}">游戏标签</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('groups.manage_groups') }}">拼团管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('announcements.manage_announcements') }}">公告管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('coupons.list_coupons') }}">优惠券</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('messages.list_messages') }}">留言管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('strategies.list_strategies') }}">策略管理</a>
</li>
{% endif %}
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="关闭">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</ul>
{% if session.token %}
<span class="navbar-text">
<a class="btn btn-outline-danger" href="{{ url_for('auth.logout') }}">退出登录</a>
</span>
{% endif %}
</div>
<!-- jQuery 和 Bootstrap JS -->
<!-- 1) 替换为完整版本的 jQuery而非 slim -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button aria-label="关闭" class="close" data-dismiss="alert" type="button">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<!-- jQuery 和 Bootstrap JS -->
<!-- 1) 替换为完整版本的 jQuery而非 slim -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
<script>
// 建立全局WebSocket连接
const adminWS = new WebSocket(`ws://192.168.5.16:8000/bell/ws/admin`);
// 处理接收到的消息
adminWS.onmessage = function(event) {
adminWS.onmessage = function (event) {
const tableNumber = event.data;
// 显示通知
if (Notification.permission === "granted") {
@ -141,7 +151,7 @@
}, 10000);
}
</script>
<!-- 2) 新增 scripts block供子模板插入自定义脚本 -->
{% block scripts %}{% endblock %}
<!-- 2) 新增 scripts block供子模板插入自定义脚本 -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,398 @@
{% extends "base.html" %}
{% block title %}收款策略管理{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>收款策略管理</h2>
<div class="card mb-4">
<div class="card-header">创建新策略</div>
<div class="card-body">
<form id="strategyForm" onsubmit="submitStrategy(event)">
<!-- 策略基本信息 -->
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
<label>策略名称 <span class="text-danger">*</span></label>
<input type="text" id="strategyName" class="form-control" required>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label>策略描述</label>
<textarea id="description" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
<!-- 价格分段设置 -->
<div class="border-top pt-3">
<h5 class="text-muted mb-3">价格分段设置</h5>
<!-- 第一阶段 -->
<div class="row alert alert-light">
<div class="col-md-3">
<label>第一阶段 <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" id="segment1Threshold" class="form-control" placeholder="分钟"
required>
<div class="input-group-append">
<span class="input-group-text">分钟内</span>
</div>
</div>
</div>
<div class="col-md-3">
<label>单价 <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" step="0.01" id="segment1Price" class="form-control" required>
<div class="input-group-append">
<span class="input-group-text">元/分钟</span>
</div>
</div>
</div>
</div>
<!-- 第二阶段 -->
<div class="row alert alert-light">
<div class="col-md-3">
<label>第二阶段 <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" id="segment2Threshold" class="form-control" placeholder="分钟"
required>
<div class="input-group-append">
<span class="input-group-text">分钟内</span>
</div>
</div>
</div>
<div class="col-md-3">
<label>单价 <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" step="0.01" id="segment2Price" class="form-control" required>
<div class="input-group-append">
<span class="input-group-text">元/分钟</span>
</div>
</div>
</div>
</div>
<!-- 第三阶段 -->
<div class="row alert alert-light">
<div class="col-md-3">
<label>第三阶段(可选)</label>
<div class="input-group">
<input type="number" id="segment3Threshold" class="form-control" placeholder="分钟">
<div class="input-group-append">
<span class="input-group-text">分钟后</span>
</div>
</div>
<small class="form-text text-muted">填写后超过该部分的价格将不计算</small>
</div>
<div class="col-md-3">
<label>固定单价 <span class="text-danger">*</span></label>
<div class="input-group">
<input type="number" step="0.01" id="segment3Price" class="form-control" required>
<div class="input-group-append">
<span class="input-group-text">元/分钟</span>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mt-3">创建策略</button>
</form>
</div>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>策略ID</th>
<th>策略名称</th>
<th>分段1阈值</th>
<th>分段1价格</th>
<th>分段2阈值</th>
<th>分段2价格</th>
<th>分段3阈值</th>
<th>分段3价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for strategy in strategies %}
<tr>
<td>{{ strategy.strategy_id }}</td>
<td>{{ strategy.strategy_name }}</td>
<td>{{ strategy.segment1_threshold }}分钟</td>
<td>{{ strategy.segment1_price }}元</td>
<td>{{ strategy.segment2_threshold }}分钟</td>
<td>{{ strategy.segment2_price }}元</td>
<td>{{ strategy.segment3_threshold or '无' }}分钟</td>
<td>{{ strategy.segment3_price or '无' }}元</td>
<td>
<button class="btn btn-sm btn-danger"
onclick="deleteStrategy({{ strategy.strategy_id }})">删除
</button>
<button class="btn btn-sm btn-info"
onclick="openBindTableModal({{ strategy.strategy_id }})">绑定桌子
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 绑定桌子模态框 -->
<!-- 修改后的绑定桌子模态框 -->
<div class="modal fade" id="bindTableModal">
<div class="modal-dialog">
<div class="modal-content">
<form id="bindTableForm" onsubmit="submitBindTable(event)">
<div class="modal-header">
<h5 class="modal-title">绑定桌子</h5>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<div class="table-checkbox-list" style="max-height: 300px; overflow-y: auto;">
<!-- 这里会通过JavaScript动态加载桌子列表 -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success">绑定选中桌子</button>
</div>
</form>
</div>
</div>
</div>
<script>
let currentStrategyId = null;
async function submitStrategy(event) {
event.preventDefault();
const payload = {
strategy_name: document.getElementById('strategyName').value,
description: document.getElementById('description').value || null,
segment1_threshold: parseInt(document.getElementById('segment1Threshold').value),
segment1_price: parseFloat(document.getElementById('segment1Price').value),
segment2_threshold: parseInt(document.getElementById('segment2Threshold').value),
segment2_price: parseFloat(document.getElementById('segment2Price').value),
segment3_price: parseFloat(document.getElementById('segment3Price').value),
segment3_threshold: document.getElementById('segment3Threshold').value
? parseInt(document.getElementById('segment3Threshold').value)
: null
};
// 添加数据验证
if (payload.segment3_threshold !== null && payload.segment3_threshold <= payload.segment2_threshold) {
alert("第三阶段阈值必须大于第二阶段");
return;
}
try {
const url = '/strategies/create';
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: "{{ session.token }}",
strategy_data: payload
})
});
const result = await resp.json();
if (result.status === "success") {
alert("创建成功");
window.location.reload(); // 手动刷新页面
} else {
alert(`失败: ${result.message}`);
}
} catch (error) {
console.error('请求失败:', error);
alert('网络请求异常,请检查连接');
}
}
async function deleteStrategy(strategyId) {
if (!confirm('确定删除该策略?')) return;
try {
const resp = await fetch(`/strategies/delete/${strategyId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${"{{ session.token }}"}`
},
body: JSON.stringify({
token: "{{ session.token }}"
})
});
if (resp.ok) {
location.reload();
} else {
const error = await resp.json();
alert(`删除失败: ${error.detail}`);
}
} catch (error) {
console.error('请求失败:', error);
alert('网络请求异常,请检查连接');
}
}
// ... existing code ...
async function openBindTableModal(strategyId) {
currentStrategyId = strategyId;
try {
const resp = await fetch('/admin/tables/list', {
headers: {
'Authorization': `Bearer ${"{{ session.token }}"}`
}
});
const tables = await resp.json();
// 清空并重新渲染桌子列表
const container = document.querySelector('.table-checkbox-list');
container.innerHTML = tables.map(table => `
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="table_ids"
value="${table.table_id}"
id="table_${table.table_id}">
<label class="form-check-label" for="table_${table.table_id}">
${table.game_table_number}
</label>
</div>
`).join('');
$('#bindTableModal').modal('show');
} catch (error) {
alert('获取桌子列表失败');
}
}
async function submitBindTable(event) {
event.preventDefault();
const tableIds = Array.from(document.querySelectorAll('#bindTableForm input[name="table_ids"]:checked')).map(input => input.value);
try {
const resp = await fetch('/strategies/bind_tables', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
strategy_id: currentStrategyId,
table_ids: tableIds.map(id => parseInt(id))
})
});
const result = await resp.json();
if (resp.ok) {
alert(result.message);
$('#bindTableModal').modal('hide');
} else {
alert(result.message);
}
} catch (error) {
console.error('请求失败:', error);
alert('网络请求异常,请检查连接');
}
}
// 初始化图表
let chart = null;
function generateChartData() {
const s1Threshold = parseInt(document.getElementById('segment1Threshold').value) || 0;
const s1Price = parseFloat(document.getElementById('segment1Price').value) || 0;
const s2Threshold = parseInt(document.getElementById('segment2Threshold').value) || s1Threshold + 30;
const s2Price = parseFloat(document.getElementById('segment2Price').value) || 0;
const s3Threshold = parseInt(document.getElementById('segment3Threshold').value);
const s3Price = parseFloat(document.getElementById('segment3Price').value) || 0;
// 确定X轴范围
const maxX = s3Threshold ? s3Threshold + 10 : s2Threshold + 10;
const dataPoints = [];
// 生成价格点
for (let x = 0; x <= maxX; x++) {
let price;
if (x <= s1Threshold) {
price = s1Price;
} else if (x <= s2Threshold) {
price = s2Price;
} else {
price = s3Price || s2Price;
}
dataPoints.push({x, price});
}
return {
labels: dataPoints.map(p => p.x + '分钟'),
datasets: [{
label: '单价 (元/分钟)',
data: dataPoints.map(p => p.price),
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
};
}
function updateChart() {
const data = generateChartData();
if (chart) {
chart.destroy();
}
chart = new Chart(document.getElementById('priceChart'), {
type: 'line',
data: data,
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '价格 (元)'
}
},
x: {
title: {
display: true,
text: '使用时长'
}
}
}
}
});
}
// 添加输入监听
const inputs = document.querySelectorAll('#strategyForm input[type="number"]');
inputs.forEach(input => {
input.addEventListener('input', () => {
if (document.getElementById('strategyName').value) {
updateChart();
}
});
});
// 初始化时创建图表
document.addEventListener('DOMContentLoaded', updateChart);
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}

View File

@ -7,18 +7,32 @@
<form method="post">
<div class="form-group">
<label>桌号</label>
<input type="text" name="game_table_number" class="form-control" required >
<input type="text" name="game_table_number" class="form-control" required>
</div>
<div class="form-group">
<label>容量(人数)</label>
<input type="number" name="capacity" class="form-control" required min="1">
</div>
<div class="form-group">
<label>价格倍率</label>
<input type="number" name="price" class="form-control" step="0.01" required min="1">
<label>收款策略 <span class="text-danger">*</span></label>
<select name="strategy_id" class="form-control" required style="white-space: normal;">
<option value="">请选择策略...</option>
{% for strategy in strategies %}
<option value="{{ strategy.strategy_id }}" style="white-space: normal;">
{{ strategy.strategy_name }} - {{ strategy.description }}
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">提交</button>
<a href="{{ url_for('tables.list_tables') }}" class="btn btn-secondary">取消</a>
</form>
</div>
<style>
.select-wrapper select option {
white-space: normal !important;
word-wrap: break-word;
}
</style>
{% endblock %}

View File

@ -8,7 +8,7 @@
<div class="form-group">
<label>桌号</label>
<input type="text" name="game_table_number" class="form-control"
value="{{ table.game_table_number }}" required >
value="{{ table.game_table_number }}" required>
</div>
<div class="form-group">
<label>容量(人数)</label>
@ -16,12 +16,26 @@
value="{{ table.capacity }}" required min="1">
</div>
<div class="form-group">
<label>价格倍率</label>
<input type="number" name="price" class="form-control"
value="{{ table.price }}" step="0.01" required min="1">
<label>收款策略 <span class="text-danger">*</span></label>
<select name="strategy_id" class="form-control" required style="white-space: normal;">
<option value="">请选择策略...</option>
{% for strategy in strategies %}
<option value="{{ strategy.strategy_id }}"
{% if table.table_pricing_strategy_id== strategy.strategy_id %}selected{% endif %}
style="white-space: normal;">
{{ strategy.strategy_name }} - {{ strategy.description }}
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<a href="{{ url_for('tables.list_tables') }}" class="btn btn-secondary">取消</a>
</form>
</div>
{% block extra_css %}
<style>
.select2-container--bootstrap-5 .select2-results__option {
white-space: normal !important;
}
</style>
{% endblock %}
{% endblock %}

View File

@ -1,40 +1,133 @@
{% extends "base.html" %}
{% block title %}台管理{% endblock %}
{% block title %}台管理{% endblock %}
{% block content %}
<div class="container mt-4">
<h2>桌台管理</h2>
<a href="{{ url_for('tables.add_table') }}" class="btn btn-primary mb-3">添加新桌台</a>
<table class="table table-striped">
<thead>
<tr>
<th>id</th>
<th>桌号</th>
<th>容量</th>
<th>价格倍率</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for table in tables %}
<tr>
<td>{{ table.table_id }}</td>
<td>{{ table.game_table_number }}</td>
<td>{{ table.capacity }}人</td>
<td>{{ "%.2f"|format(table.price) }}</td>
<td>
<a href="{{ url_for('tables.edit_table', table_id=table.table_id) }}"
class="btn btn-sm btn-warning">编辑</a>
<form action="{{ url_for('tables.delete_table', table_id=table.table_id) }}"
method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('确认删除该桌台?')">删除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<body>
<div class="container mt-4">
<h1>桌台列表</h1>
<a href="{{ url_for('tables.add_table') }}" class="btn btn-primary mb-3">添加桌台</a>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>桌号</th>
<th>容量</th>
<th>收款策略</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for table in tables %}
<tr>
<td>{{ table.table_id }}</td>
<td>{{ table.game_table_number }}</td>
<td>{{ table.capacity }}</td>
<td>{{ table.strategy_name }}</td>
<td>
<a href="{{ url_for('tables.edit_table', table_id=table.table_id) }}"
class="btn btn-warning btn-sm">编辑</a>
<form action="{{ url_for('tables.delete_table', table_id=table.table_id) }}" method="post"
class="d-inline">
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('确认删除该桌台吗?')">删除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 选择策略模态框 -->
<div class="modal fade" id="selectStrategyModal" tabindex="-1" aria-labelledby="selectStrategyModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="selectStrategyModalLabel">选择收款策略</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form onsubmit="selectStrategy(event)">
<input type="hidden" id="selectedTableId">
<div class="modal-body">
<div class="form-group">
<label for="strategySelect">收款策略</label>
<select class="form-control" id="strategySelect" required>
<!-- 动态填充策略选项 -->
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
async function openSelectStrategyModal(tableId) {
try {
const token = "{{ session['token'] }}";
const response = await fetch('/admin/strategies/list', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const strategies = await response.json();
const strategySelect = document.getElementById('strategySelect');
strategySelect.innerHTML = '';
strategies.forEach(strategy => {
const option = document.createElement('option');
option.value = strategy.strategy_id;
option.textContent = strategy.strategy_name;
strategySelect.appendChild(option);
});
document.getElementById('selectedTableId').value = tableId;
const myModal = new bootstrap.Modal(document.getElementById('selectStrategyModal'));
myModal.show();
} catch (error) {
console.error('获取策略列表失败:', error);
}
}
async function selectStrategy(event) {
event.preventDefault();
const tableId = document.getElementById('selectedTableId').value;
const strategyId = document.getElementById('strategySelect').value;
const token = "{{ session['token'] }}";
try {
const response = await fetch(`/admin/tables/${tableId}/strategy`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
strategy_id: strategyId
})
});
if (response.ok) {
const myModal = bootstrap.Modal.getInstance(document.getElementById('selectStrategyModal'));
myModal.hide();
location.reload();
} else {
const error = await response.json();
alert(`更新失败: ${error.detail}`);
}
} catch (error) {
console.error('更新策略失败:', error);
}
}
</script>
</body>
{% endblock %}