Compare commits

...

2 Commits
main ... master

3
.gitignore vendored

@ -0,0 +1,3 @@
.idea
__pycache__/
*.log

@ -0,0 +1,7 @@
FROM python:3.12.4-slim
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip config set global.index-url http://mirrors.aliyun.com/pypi/simple && pip config set install.trusted-host mirrors.aliyun.com
RUN pip install -r requirements.txt
COPY . ./
CMD ["gunicorn", "-w", "4", "--bind", "0.0.0.0:3002", "myapp:app" ]

@ -0,0 +1,84 @@
# app/__init__.py
# 导入 os 模块,用于与操作系统进行交互,如获取环境变量等操作
import os
# 导入 timedelta 类,用于表示时间间隔,在设置 JWT 令牌过期时间时会用到
from datetime import timedelta
# 导入 redis 模块,用于与 Redis 数据库进行交互
import redis
# 从 flask 模块导入 Flask 类和 request 对象Flask 类用于创建 Flask 应用实例request 对象用于处理 HTTP 请求
from flask import Flask, request
# 从 flask_cors 模块导入 CORS 类,用于处理跨域资源共享问题
from flask_cors import CORS
# 从 flask_jwt_extended 模块导入 JWTManager 类,用于管理 JSON Web Token (JWT)
from flask_jwt_extended import JWTManager
# 从 flask_migrate 模块导入 Migrate 类,用于数据库迁移管理
from flask_migrate import Migrate
# 从 flask_sqlalchemy 模块导入 SQLAlchemy 类,用于集成 SQLAlchemy 数据库 ORM
from flask_sqlalchemy import SQLAlchemy
# 从 loguru 模块导入 logger 对象,用于日志记录
from loguru import logger
# 从 config 模块导入 Config 类,用于加载应用配置
from config import Config
# 从 app.log_service 模块导入 LogService 类,用于配置日志服务
from app.log_service import LogService
# 创建 SQLAlchemy 数据库实例
db = SQLAlchemy()
# 获取 Redis 服务器的主机地址,优先从环境变量中获取,如果环境变量未设置,则使用默认值 'localhost'
redis_host_url = os.getenv('REDIS_URL', 'localhost')
# 创建 Redis 客户端实例,连接到指定的 Redis 服务器
redis_client = redis.StrictRedis(host=redis_host_url, port=6379, db=0, decode_responses=True)
def create_app():
"""
创建并配置 Flask 应用实例
Returns:
Flask: 配置好的 Flask 应用实例
"""
# 创建 Flask 应用实例,指定静态文件夹为 'mini12306'
app = Flask(__name__, static_folder='mini12306')
# 启用跨域资源共享支持
CORS(app)
# 推送应用上下文,确保应用上下文在后续操作中可用
app.app_context().push()
# 从 Config 类加载应用配置
app.config.from_object(Config)
# 设置 JWT 访问令牌的过期时间为 30 天
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=30)
# 初始化 SQLAlchemy 数据库实例
db.init_app(app)
# 初始化数据库迁移管理
Migrate(app, db)
# 初始化 JWT 管理器
JWTManager(app)
# 配置日志服务
LogService.configure_logging()
# 导入各个蓝图模块
from app.login_manager import login_bp
from app.passenger_manager import register_bp
from app.query_manager import query_bp
from app.station_manager import station_bp
from app.mobile_server import mobile_bp
from app.train_manager import trains_bp
from app.bank_server import bank_bp
from app.id_card_server import id_card_bp
from app.order_manager import order_bp
# 注册各个蓝图到应用实例
app.register_blueprint(login_bp)
app.register_blueprint(register_bp)
app.register_blueprint(query_bp)
app.register_blueprint(station_bp)
app.register_blueprint(mobile_bp)
app.register_blueprint(trains_bp)
app.register_blueprint(bank_bp)
app.register_blueprint(id_card_bp)
app.register_blueprint(order_bp)
return app

@ -0,0 +1,77 @@
# 导入 re 模块,用于使用正则表达式进行字符串匹配
import re
# 导入 datetime 模块,用于处理日期和时间
import datetime
# 从 flask 模块导入 Blueprint、jsonify 和 request 类
# Blueprint 用于创建 Flask 应用的蓝图,方便组织路由
# jsonify 用于将 Python 字典或列表转换为 JSON 响应
# request 用于处理客户端的 HTTP 请求
from flask import Blueprint, jsonify, request
# 从 app 包中导入 LogService 类,用于记录日志
from app import LogService
# 从 utils 模块导入 create_response 函数和 StateCode 类
# create_response 用于创建统一格式的响应
# StateCode 用于定义响应状态码
from utils import create_response, StateCode
# 创建一个名为 'bank_server' 的蓝图
bank_bp = Blueprint('bank_server', __name__)
@bank_bp.route('/bank/card_verify', methods=['POST'])
def bankCardVerify():
"""
验证银行卡号是否有效
该函数接收一个包含银行卡号的 POST 请求
并使用正则表达式验证银行卡号是否为 13 19 位的数字字符串
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 定义匹配 13 到 19 位数字字符串的正则表达式模式
pattern = r'^\d{13,19}$'
# 从请求的 JSON 数据中获取银行卡号
number = request.json.get('bankCard')
# 定义验证结果的字典
state = {
# 使用 re.match 函数验证银行卡号是否匹配模式
"result": bool(re.match(pattern, number))
}
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建响应,包含成功状态码和验证结果
return jsonify(create_response(StateCode.SUCCESS, data=state)), 200
@bank_bp.route('/bank/pay', methods=['POST'])
def pay():
"""
模拟支付操作
该函数接收一个 POST 请求模拟支付成功并返回支付状态和支付时间
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 定义支付状态和支付时间的字典
state = {"state": "successful", "pay_time": datetime.datetime.now()}
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建响应,包含成功状态码和支付结果
return jsonify(create_response(StateCode.SUCCESS, data=state)), 200
@bank_bp.route('/bank/query', methods=['POST'])
def query():
"""
模拟查询支付状态操作
该函数接收一个 POST 请求模拟查询成功并返回支付状态和支付时间
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 定义查询结果的字典,包含支付状态和当前时间作为支付时间
state = {"state": "successful", "pay_time": datetime.datetime.now()}
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建响应,包含成功状态码和查询结果
return jsonify(create_response(StateCode.SUCCESS, data=state)), 200

@ -0,0 +1,40 @@
import re
# 从 flask 模块导入 Blueprint、jsonify 和 request 类
# Blueprint 用于创建 Flask 应用的蓝图,方便组织路由
# jsonify 用于将 Python 字典或列表转换为 JSON 响应
# request 用于处理客户端的 HTTP 请求
from flask import Blueprint, jsonify, request
# 从 app 包中导入 LogService 类,用于记录日志
from app import LogService
# 从 utils 模块导入 create_response 函数和 StateCode 类
# create_response 用于创建统一格式的响应
# StateCode 用于定义响应状态码
from utils import create_response, StateCode
# 创建一个名为 'id_card_server' 的蓝图
id_card_bp = Blueprint('id_card_server', __name__)
@id_card_bp.route('/id_card/verify', methods=['POST'])
def idCardVerify():
"""
验证身份证号码是否有效
该函数接收一个包含身份证号码的 POST 请求
并使用正则表达式验证身份证号码是否为 18 位数字字符串
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 定义匹配 18 位数字字符串的正则表达式模式
pattern = r'^\d{18}$'
# 从请求的 JSON 数据中获取身份证号码
id_number = request.json.get('idCardNo')
# 定义验证结果的字典
state = {
# 使用 re.match 函数验证身份证号码是否匹配模式
"result": bool(re.match(pattern, id_number))
}
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建响应,包含成功状态码和验证结果
return jsonify(create_response(StateCode.SUCCESS, data=state)), 200

@ -0,0 +1,53 @@
# 从 flask 模块导入 request 对象,用于获取 HTTP 请求的相关信息
from flask import request
# 从 loguru 模块导入 logger 对象,用于进行日志记录
from loguru import logger
class LogService:
"""
LogService 类用于配置日志记录和记录请求信息
"""
@staticmethod
def configure_logging():
"""
配置日志记录的方式
移除默认的日志配置并添加自定义的日志配置
包括日志文件的名称轮换规则保留时间日志级别日志格式等
"""
# 移除默认的日志配置
logger.remove()
# 添加自定义的日志配置
logger.add(
# 日志文件名,日志将被记录到 logs/app.log 文件中
"logs/app.log",
# 日志文件的轮换规则,每周轮换一次
rotation="1 week",
# 日志文件的保留时间,保留一个月的日志
retention="1 month",
# 记录 INFO 及以上级别的日志
level="INFO",
# 日志的格式,包含时间、日志级别和日志消息
format="{time} - {level} - {message}",
# 打印完整的异常堆栈
backtrace=True,
# 打印详细的异常信息
diagnose=True
)
@staticmethod
def log():
"""
记录当前请求的相关信息
包括请求的路径请求方法请求头请求参数和请求体
"""
# 记录请求的路径
logger.info(f"Request Path: {request.path}")
# 记录请求的方法
logger.info(f"Request Method: {request.method}")
# 记录请求的头信息
logger.info(f"Request Headers: {request.headers}")
# 记录请求的参数
logger.info(f"Request args: {request.args}")
# 记录请求的体信息,将其以文本形式获取
logger.info(f"Request Body: {request.get_data(as_text=True)}")

@ -0,0 +1,61 @@
# 从 flask 模块导入 Blueprint、request 和 jsonify 类
# Blueprint 用于创建 Flask 应用的蓝图,方便组织路由
# request 用于处理客户端的 HTTP 请求
# jsonify 用于将 Python 字典或列表转换为 JSON 响应
from flask import Blueprint, request, jsonify
# 从 flask_jwt_extended 模块导入 create_access_token 函数,用于创建 JWT 访问令牌
from flask_jwt_extended import create_access_token
# 从 app 包中导入 LogService 类,用于记录日志
from app import LogService
# 从 app.models 模块导入 Passenger 类,用于处理乘客相关的数据操作
from app.models import Passenger
# 从 presenter 模块导入 PassengerPresenter 类,用于处理乘客数据的展示逻辑
from presenter import PassengerPresenter
# 从 utils 模块导入 StateCode 类和 create_response 函数
# StateCode 用于定义响应状态码
# create_response 用于创建统一格式的响应
from utils import StateCode, create_response
# 创建一个名为 'login' 的蓝图
login_bp = Blueprint('login', __name__)
@login_bp.route('/login', methods=['POST'])
def login():
"""
处理用户登录请求
该函数接收一个包含账户名和密码的 POST 请求
验证账户名和密码是否正确若正确则生成 JWT 访问令牌
并返回包含用户信息和令牌的响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 从请求的 JSON 数据中获取所有信息
data = request.json
# 从数据中提取账户名
account = data.get('account')
# 从数据中提取密码
password = data.get('password')
# 检查账户名或密码是否为空
if not account or not password:
# 若为空,使用 create_response 函数创建包含参数错误状态码的响应
return jsonify(create_response(StateCode.PARAMS_ERROR)), 400
# 调用 Passenger 类的 verifyPassenger 方法验证账户名和密码
user = Passenger.verifyPassenger(account, password)
# 检查用户是否存在(即验证是否成功)
if user:
# 若验证成功,使用 create_access_token 函数为用户创建 JWT 访问令牌
access_token = create_access_token(identity=user.id)
# 使用 PassengerPresenter 类处理用户信息,并将令牌添加到展示数据中
user_presenter = PassengerPresenter(user, {"token": access_token}).as_dict()
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建包含成功状态码和用户展示数据的响应
return jsonify(create_response(StateCode.SUCCESS, data=user_presenter)), 200
# 若验证失败,使用 create_response 函数创建包含密码错误状态码的响应
return jsonify(create_response(StateCode.PASSWORD_INCORRECT)), 400

@ -0,0 +1,119 @@
import random
import re
# 从 flask 模块导入 current_app、jsonify、request 和 Blueprint 类
# current_app 用于获取当前活动的 Flask 应用实例
# jsonify 用于将 Python 字典或列表转换为 JSON 响应
# request 用于处理客户端的 HTTP 请求
# Blueprint 用于创建 Flask 应用的蓝图,方便组织路由
from flask import current_app, jsonify, request, Blueprint
# 导入自定义的 utils 模块,可能包含一些工具函数
import utils
# 从 app 包中导入 redis_client 和 LogService 类
# redis_client 用于与 Redis 数据库进行交互
# LogService 用于记录日志
from app import redis_client, LogService
# 从 presenter 模块导入 MobileCodePresenter 类,用于处理验证码相关的展示逻辑
from presenter import MobileCodePresenter
# 从 utils 模块导入 create_response 函数和 StateCode 类
# create_response 用于创建统一格式的响应
# StateCode 用于定义响应状态码
from utils import create_response, StateCode
# 从当前应用的配置中获取 SECRET_KEY
key = current_app.config['SECRET_KEY']
# 创建一个名为 'mobile_server' 的蓝图
mobile_bp = Blueprint('mobile_server', __name__)
@mobile_bp.route('/mobile/get_verify_code', methods=['POST'])
def getVerifyCode():
"""
处理获取手机验证码的请求
该函数接收一个包含手机号码的 POST 请求
验证手机号码的格式若格式正确则生成验证码并存储到 Redis
最后返回包含验证码信息的响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 从请求的 JSON 数据中获取手机号码
mobile_no = request.json.get('mobileNo')
# 调用 utils 模块中的 checkMobile 函数验证手机号码格式
if not utils.checkMobile(mobile_no):
# 若格式不正确,使用 create_response 函数创建包含手机号码错误状态码的响应
return jsonify(create_response(StateCode.MOBILE_ERROR)), 400
# 调用 getVerificationCode 函数生成并获取验证码
sent_code = getVerificationCode(mobile_no)
# 使用 MobileCodePresenter 类处理验证码信息,并转换为字典格式
mobile_present = MobileCodePresenter(sent_code).as_dict()
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建包含成功状态码和验证码展示数据的响应
return jsonify(create_response(StateCode.SUCCESS, data=mobile_present)), 200
@mobile_bp.route('/mobile/check', methods=['POST'])
def checkMobile():
"""
验证手机号码格式是否正确
该函数接收一个包含手机号码的 POST 请求
使用正则表达式验证手机号码格式并返回验证结果
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 定义匹配中国手机号码的正则表达式模式
# 手机号码以 1 开头,第二位是 3 - 9 之间的数字,后面跟着 9 位数字
pattern = r'^1[3-9]\d{9}$'
# 从请求的 JSON 数据中获取手机号码
number = request.json.get('mobileNo')
# 定义验证结果的字典
state = {
# 使用 re.match 函数验证手机号码是否匹配模式
"result": bool(re.match(pattern, number))
}
# 调用 LogService 的 log 方法记录日志
LogService.log()
# 使用 create_response 函数创建包含成功状态码和验证结果的响应
return jsonify(create_response(StateCode.SUCCESS, data=state)), 200
def getVerificationCode(mobile_no):
"""
生成并存储手机验证码到 Redis
该函数生成一个随机的 6 位数字验证码
并将其存储到 Redis 设置过期时间为 300
Args:
mobile_no (str): 手机号码
Returns:
str: 生成的验证码如果存储失败则返回 None
"""
# 调用 generateRandomNumber 函数生成验证码
verification_code = generateRandomNumber()
# 将验证码存储到 Redis 中,设置过期时间为 300 秒
if redis_client.set(mobile_no, verification_code, 300):
return verification_code
else:
# 若存储失败,打印 Redis 客户端信息
print(f"{redis_client.client}")
# 生成 6 位随机数字
def generateRandomNumber(length=6):
"""
生成指定长度的随机数字
该函数生成一个指定长度的随机数字确保生成的数字至少有指定长度
即使前几位是 0
Args:
length (int, optional): 随机数字的长度默认为 6
Returns:
int: 生成的随机数字
"""
# 生成一个指定长度的随机数字,范围是从 10 的 (length - 1) 次方到 10 的 length 次方减 1
number = random.randint(10 ** (length - 1), 10 ** length - 1)
return number

@ -0,0 +1,10 @@
# 从 app 模块导入 db 对象,不过该导入未被使用,后续可考虑移除
from app import db
# 从当前包的 order_lib 模块导入 Order 类
from .order_lib import Order
# 从当前包的 passenger_lib 模块导入 Passenger 类
from .passenger_lib import Passenger
# 从当前包的 station_lib 模块导入 Station 类
from .station_lib import Station
# 从当前包的 train_lib 模块导入 Train 类
from .train_lib import Train

@ -0,0 +1,147 @@
import pdb
import random
import time
import uuid
from datetime import timedelta, datetime
from sqlalchemy import func
from app.models import db
from app.models.ticket_lib import Ticket
from app.models.train_station_lib import TrainStation
# 定义 Order 类,继承自 db.Model用于表示数据库中的订单表
class Order(db.Model):
# 指定数据库表名为 'order'
__tablename__ = 'order'
# 定义主键列,类型为整数,自动递增
id = db.Column(db.Integer, primary_key=True)
# 定义订单编号列,类型为字符串,最大长度 120唯一且不能为空
order_no = db.Column(db.String(120), unique=True, nullable=False)
# 定义订单价格列,类型为数值,总位数 8小数位数 2
price = db.Column(db.Numeric(8, 2))
# 定义支付时间列,类型为日期时间
payment_time = db.Column(db.DateTime)
# 定义订单状态列,类型为整数,默认值为 0
state = db.Column(db.Integer, default=0)
# 定义乘客 ID 列,类型为整数,关联到 'passenger' 表的 'id' 列,不能为空,并创建索引
passenger_id = db.Column(db.Integer, db.ForeignKey('passenger.id'), nullable=False, index=True)
# 定义创建时间列,类型为日期时间,默认值为当前时间
created_at = db.Column(db.DateTime, default=func.now())
# 定义更新时间列,类型为日期时间,默认值为当前时间
updated_at = db.Column(db.DateTime, default=func.now())
# 定义与 Passenger 模型的关联关系,通过 backref 可以从 Passenger 模型访问相关的订单
passenger = db.relationship('Passenger', backref=db.backref('orders'))
def __repr__(self):
"""
返回订单对象的字符串表示形式方便调试和日志记录
:return: 包含订单 ID 的字符串
"""
return f'<Order {self.id}>'
@classmethod
def setOrderState(cls, order_no, passenger_id, **kwargs):
"""
根据订单编号和乘客 ID 更新订单状态及其他属性并在状态更新为 1 时更新相关车票状态
:param order_no: 订单编号
:param passenger_id: 乘客 ID
:param kwargs: 要更新的订单属性键值对
:return: 更新后的订单对象如果未找到订单则返回 None
"""
# 根据订单编号和乘客 ID 查询订单
order = cls.query.filter_by(order_no=order_no, passenger_id=passenger_id).first()
if order:
# 遍历传入的关键字参数,更新订单的属性
for key, value in kwargs.items():
# 使用 setattr 函数设置订单对象的属性
setattr(order, key, value)
# 如果更新的属性是 'state' 且值为 1
if key == 'state' and value == 1:
# 遍历订单中的所有车票
for ticket in order.tickets:
# 调用 Ticket 类的 updateState 方法更新车票状态为 1
Ticket.updateState(ticket) # Update state to 1
# 提交更改到数据库
db.session.commit()
# 返回更新后的订单对象
return order
else:
# 处理订单未找到的情况,返回 None
return None
def generate_unique_id(length=4):
# 获取当前的秒级时间戳,用于确保生成的 ID 在时间维度上具有唯一性
timestamp_seconds = int(time.time())
# 生成 4 位随机数,增加 ID 的随机性
random_number = random.randint(0, 10 ** length - 1)
# 确保随机数是 4 位数字,如果不足 4 位,在前面补 0
random_number_str = str(random_number).zfill(length)
# 生成唯一标识符,将时间戳和 4 位随机数拼接在一起
unique_id = f"{timestamp_seconds}{random_number_str}"
return unique_id
def generateOrder(params, passenger_id):
"""
根据传入的参数和乘客 ID 生成一个新的订单对象
:param params: 包含车票信息的字典
:param passenger_id: 乘客的 ID
:return: 生成的订单对象
"""
# 创建一个新的 Order 对象
order = Order(
# 调用 generate_unique_id 函数生成 5 位长度的唯一订单编号
order_no=generate_unique_id(5),
# 关联乘客 ID
passenger_id=passenger_id
)
# 遍历传入参数中的车票信息
for e in params['tickets']:
# 创建 Ticket 对象,调用 Ticket 类的 generateTicket 方法
ticket = Ticket.generateTicket(e, passenger_id)
# 从数据库中查询起始站信息,根据车次和起始站名称
from_station = TrainStation.query.filter_by(train_no=ticket.train_no, station_name=ticket.from_station).one()
# 从数据库中查询终点站信息,根据车次和终点站名称
to_station = TrainStation.query.filter_by(train_no=ticket.train_no, station_name=ticket.to_station).one()
# 查询车次中从起始站到终点站的所有站点信息
train_stations = TrainStation.query.filter(
TrainStation.train_no == ticket.train_no,
TrainStation.index.between(from_station.index, to_station.index)
).all()
# 计算车票价格,将区间内所有站点的价格相加
ticket.price = sum(e.price for e in train_stations)
# 计算当前车次和日期下已售出车票的数量
seat_count = db.session.query(func.count(Ticket.id)).filter_by(
train_no=ticket.train_no,
date=ticket.date
).scalar()
# 为车票分配座位号,座位号为已售车票数量加 1
ticket.seat_no = seat_count + 1
# 计算车票日期与起始站出发日期的天数差
interval_days = (
datetime.strptime(ticket.date, "%Y/%m/%d") - datetime.combine(from_station.departure_time.date(),
datetime.min.time())).days
# 计算车票的出发时间,考虑日期差
ticket.departure_time = from_station.departure_time + timedelta(days=interval_days)
# 计算车票的到达时间,考虑日期差
ticket.arrival_time = to_station.arrival_time + timedelta(days=interval_days)
# 将生成的车票添加到订单的车票列表中
order.tickets.append(ticket)
# 计算订单总价,将订单中所有车票的价格相加
order.price = sum(ticket.price for ticket in order.tickets)
return order

@ -0,0 +1,113 @@
# 从 sqlalchemy 模块导入 func 函数,用于在数据库查询中使用函数,如获取当前时间
from sqlalchemy import func
# 从 app.models 包中导入 db 对象,它通常是 SQLAlchemy 的数据库实例
from app.models import db
# 从 werkzeug.security 模块导入生成密码哈希和检查密码哈希的函数
from werkzeug.security import generate_password_hash, check_password_hash
def isAvailable(account):
"""
检查指定账户是否可用即是否已存在于数据库中
:param account: 要检查的账户名
"""
# 尝试从数据库中查询具有指定账户名的乘客记录
Passenger.query.filter_by(account=account).first()
class Passenger(db.Model):
"""
Passenger 类表示数据库中的乘客表继承自 SQLAlchemy db.Model
"""
__tablename__ = 'passenger'
# 定义主键列,存储乘客的唯一标识
id = db.Column(db.Integer, primary_key=True)
# 定义存储乘客姓名的列,最大长度为 120
name = db.Column(db.String(120))
# 定义存储乘客账户名的列,唯一且不能为空,添加索引以提高查询效率
account = db.Column(db.String(120), unique=True, nullable=False, index=True)
# 定义存储密码哈希值的列,最大长度为 2000
password_digest = db.Column(db.String(2000), nullable=False)
# 定义存储乘客身份证号的列,最大长度为 120
id_card_no = db.Column(db.String(120))
# 定义存储乘客手机号码的列,最大长度为 120
mobile_no = db.Column(db.String(120))
# 定义存储乘客银行卡号的列,最大长度为 120
bank_card_no = db.Column(db.String(120))
# 定义存储乘客状态的列,默认为 0
state = db.Column(db.Integer, default=0)
# 定义存储乘客会员类型的列
member_type = db.Column(db.Integer)
# 定义存储乘客最后登录时间的列
last_login_time = db.Column(db.DateTime)
# 定义存储乘客记录创建时间的列,默认值为当前时间
created_at = db.Column(db.DateTime, default=func.now())
# 定义存储乘客记录更新时间的列,默认值为当前时间
updated_at = db.Column(db.DateTime, default=func.now())
def check_password(self, password):
"""
验证传入的密码是否与存储的密码哈希值匹配
:param password: 要验证的明文密码
:return: 如果匹配返回 True否则返回 False
"""
return check_password_hash(self.password_digest, password) # 验证密码
@classmethod
def verifyPassenger(cls, account, password):
"""
根据账户名和密码验证乘客信息
:param account: 要验证的账户名
:param password: 要验证的密码
:return: 如果验证成功返回乘客对象否则返回 None
"""
# 从数据库中查询具有指定账户名的乘客记录
passenger = cls.query.filter_by(account=account).first()
# 检查乘客记录是否存在且密码是否匹配
if passenger and passenger.check_password(password):
return passenger
return None
@classmethod
def create(cls, data):
"""
根据传入的数据创建新的乘客记录
:param data: 包含乘客信息的字典如账户名密码等
:return: 创建成功的乘客对象
"""
# 创建新的乘客对象
passenger = cls(
account=data.get('account'),
# 生成密码的哈希值
password_digest=generate_password_hash(data.get('password')),
name=data.get('name'),
id_card_no=data.get('idCardNo'),
mobile_no=data.get('mobileNo'),
bank_card_no=data.get('bankCard'),
)
# 将新的乘客对象添加到数据库会话中
db.session.add(passenger)
# 提交数据库会话,将更改保存到数据库
db.session.commit()
return passenger
@classmethod
def destroy(cls, passenger_id):
"""
根据乘客 ID 删除对应的乘客记录
:param passenger_id: 要删除的乘客的 ID
:return: 如果删除成功返回 True否则返回 False
"""
# 从数据库中查询具有指定 ID 的乘客记录
passenger = cls.query.get(passenger_id)
if passenger:
# 如果记录存在,从数据库会话中删除该记录
db.session.delete(passenger)
# 提交数据库会话,将更改保存到数据库
db.session.commit()
return True
return False

@ -0,0 +1,86 @@
import pdb
from datetime import datetime
# 从 pypinyin 库导入 pinyin 函数和 Style 类,用于将中文转换为拼音
from pypinyin import pinyin, Style
# 从 sqlalchemy 导入 func 函数,用于数据库操作中的函数调用,如获取当前时间
from sqlalchemy import func
# 从 app.models 模块导入数据库实例
from app.models import db
class Station(db.Model):
"""
定义 Station 模型对应数据库中的 'station' 用于存储车站信息
"""
__tablename__ = 'station'
# 定义车站 ID 列,作为主键,自增整数类型
id = db.Column(db.Integer, primary_key=True)
# 定义车站名称列,最大长度为 120 的字符串类型,唯一且不能为空,添加索引以提高查询效率
name = db.Column(db.String(120), unique=True, nullable=False, index=True)
# 定义车站名称的拼音列,最大长度为 120 的字符串类型
pinyin = db.Column(db.String(120))
# 定义车站所在省份列,最大长度为 120 的字符串类型
province = db.Column(db.String(120))
# 定义车站所在城市列,最大长度为 120 的字符串类型
city = db.Column(db.String(120))
# 定义车站所在地区列,最大长度为 120 的字符串类型
district = db.Column(db.String(120))
# 定义车站记录创建时间列,日期时间类型,默认值为当前时间
created_at = db.Column(db.DateTime, default=func.now())
# 定义车站记录更新时间列,日期时间类型,默认值为当前时间
updated_at = db.Column(db.DateTime, default=func.now())
def __repr__(self):
"""
返回一个可打印的字符串表示该车站对象方便调试和日志记录
:return: 包含车站名称的字符串
"""
return f'<Station {self.name}>'
@classmethod
def create(cls, data):
"""
根据传入的数据创建新的车站记录
:param data: 包含车站信息的字典如名称省份城市等
:return: 创建成功的车站对象
"""
# 创建新的车站对象
station = cls(
name=data.get('name'),
# 将车站名称转换为大写拼音并拼接成字符串
pinyin=''.join([item[0] for item in pinyin(data.get('name'), style=Style.NORMAL)]).upper(),
province=data.get('province'),
city=data.get('city'),
# 原代码这里使用了未导入的 datetime应改为 func.now()
# created_at=datetime.now(),
created_at=func.now(),
# 原代码这里使用了未导入的 datetime应改为 func.now()
# updated_at=datetime.now(),
updated_at=func.now(),
)
# 将新车站对象添加到数据库会话
db.session.add(station)
# 提交数据库会话,将更改保存到数据库
db.session.commit()
return station
@classmethod
def destroy(cls, station_id):
"""
根据车站 ID 删除对应的车站记录
:param station_id: 要删除的车站 ID
:return: 如果删除成功返回 True否则返回 False
"""
# 根据车站 ID 查询车站记录
station = cls.query.get(station_id)
if station:
# 如果记录存在,从数据库会话中删除该车站对象
db.session.delete(station)
# 提交数据库会话,将更改保存到数据库
db.session.commit()
return True
return False

@ -0,0 +1,79 @@
# 从 sqlalchemy 导入 func 函数,用于在数据库操作中使用函数,如获取当前时间
from sqlalchemy import func
# 从 app.models 模块导入数据库实例
from app.models import db
class Ticket(db.Model):
"""
Ticket 类代表数据库中的车票表继承自 SQLAlchemy db.Model
"""
__tablename__: str = 'ticket'
# 定义车票的主键,为整数类型,自动递增
id = db.Column(db.Integer, primary_key=True)
# 定义车票座位号,最大长度为 120 的字符串
seat_no = db.Column(db.String(120))
# 定义车票座位等级,最大长度为 120 的字符串
seat_class = db.Column(db.String(120))
# 定义车票价格,使用 Numeric 类型,总位数 8 位,小数位 2 位
price = db.Column(db.Numeric(8, 2))
# 定义车票状态,整数类型,默认值为 0
state = db.Column(db.Integer, default=0)
# 定义车次编号,关联到 train 表的 train_no 字段,不能为空,并添加索引
train_no = db.Column(db.String(120), db.ForeignKey('train.train_no'), nullable=False, index=True)
# 定义乘客 ID关联到 passenger 表的 id 字段,不能为空,并添加索引
passenger_id = db.Column(db.Integer, db.ForeignKey('passenger.id'), nullable=False, index=True)
# 定义订单 ID关联到 order 表的 id 字段,不能为空,并添加索引
order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False, index=True)
# 定义出发站,最大长度为 120 的字符串
from_station = db.Column(db.String(120))
# 定义到达站,最大长度为 120 的字符串
to_station = db.Column(db.String(120))
# 定义乘车日期,最大长度为 120 的字符串
date = db.Column(db.String(120))
# 定义出发时间,日期时间类型
departure_time = db.Column(db.DateTime)
# 定义到达时间,日期时间类型
arrival_time = db.Column(db.DateTime)
# 定义车票记录创建时间,默认值为当前时间
created_at = db.Column(db.DateTime, default=func.now())
# 定义车票记录更新时间,默认值为当前时间
updated_at = db.Column(db.DateTime, default=func.now())
# 定义与 Order 模型的关联关系,通过 backref 可以从 Order 访问相关的车票
order = db.relationship('Order', backref=db.backref('tickets'))
# 定义与 Passenger 模型的关联关系,通过 backref 可以从 Passenger 访问相关的车票
passenger = db.relationship('Passenger', backref=db.backref('tickets'))
def __repr__(self):
"""
返回车票对象的字符串表示形式方便调试和日志记录
:return: 包含车票 ID 的字符串
"""
return f'<Ticket {self.id}>'
def updateState(self):
"""
更新车票的状态为 1
:return: 更新状态后的车票对象
"""
self.state = 1
return self
@classmethod
def generateTicket(cls, item, passenger_id):
"""
根据传入的车票信息和乘客 ID 生成一个新的车票对象
:param item: 包含车票信息的字典如座位等级车次出发站等
:param passenger_id: 乘客的 ID
:return: 生成的车票对象
"""
# 创建一个新的 Ticket 对象
ticket = Ticket(
seat_class=item["seatClass"],
train_no=item["trainNo"],
from_station=item["from"],
to_station=item["to"],
date=item["date"],
passenger_id=passenger_id
)
return ticket

@ -0,0 +1,136 @@
# 从 sqlalchemy 导入 func 函数,用于在数据库查询中使用函数,如获取当前时间
from sqlalchemy import func
# 从 app.models 模块导入数据库实例
from app.models import db
# 从 app.models.train_station_lib 模块导入 TrainStation 模型
from app.models.train_station_lib import TrainStation
class Train(db.Model):
"""
定义 Train 模型对应数据库中的 'train' 用于存储列车信息
"""
__tablename__: str = 'train'
# 定义列车 ID 列,作为主键,自增整数类型
id = db.Column(db.Integer, primary_key=True)
# 定义列车编号列,最大长度为 120 的字符串类型,唯一且不能为空,添加索引以提高查询效率
train_no = db.Column(db.String(120), unique=True, nullable=False, index=True)
# 定义出发站列,最大长度为 120 的字符串类型
departure_station = db.Column(db.String(120))
# 定义到达站列,最大长度为 120 的字符串类型
arrival_station = db.Column(db.String(120))
# 定义出发时间列,日期时间类型
departure_time = db.Column(db.DateTime)
# 定义过期时间列,日期时间类型
expiration_time = db.Column(db.DateTime)
# 定义生效时间列,日期时间类型
effective_time = db.Column(db.DateTime)
# 定义到达时间列,日期时间类型
arrival_time = db.Column(db.DateTime)
# 定义列车记录创建时间列,日期时间类型,默认值为当前时间
created_at = db.Column(db.DateTime, default=func.now())
# 定义列车记录更新时间列,日期时间类型,默认值为当前时间
updated_at = db.Column(db.DateTime, default=func.now())
def __repr__(self):
"""
返回一个可打印的字符串表示该列车对象方便调试和日志记录
:return: 包含列车 ID 的字符串
"""
return f'<Train {self.id}>'
@classmethod
def create(cls, new_train):
"""
将新的列车对象添加到数据库并提交更改
:param new_train: 要创建的列车对象
:return: 创建成功的列车对象
"""
# 将新列车对象添加到数据库会话
db.session.add(new_train)
# 提交数据库会话,将更改保存到数据库
db.session.commit()
return new_train
@classmethod
def queryTrains(cls, from_station, to_station, date):
"""
根据出发站到达站和日期查询符合条件的列车
:param from_station: 出发站名称
:param to_station: 到达站名称
:param date: 查询日期
:return: 符合条件的列车列表
"""
# 查询出发站名称匹配 `from_station` 的所有列车站点记录
from_train = TrainStation.query.filter_by(station_name=from_station).all()
# 查询到达站名称匹配 `to_station` 的所有列车站点记录
to_train = TrainStation.query.filter_by(station_name=to_station).all()
# 从出发站查询结果中提取列车编号,存储在集合中以确保唯一性
from_train_nos = {ts.train_no for ts in from_train}
# 从到达站查询结果中提取列车编号,存储在集合中以确保唯一性
to_train_nos = {ts.train_no for ts in to_train}
# 找出两个集合中共同的列车编号
common_train_nos = from_train_nos & to_train_nos
# 过滤出出发站索引小于到达站索引的列车编号
valid_train_nos = [
train_no for train_no in common_train_nos
if next(ts.index for ts in from_train if ts.train_no == train_no) <
next(ts.index for ts in to_train if ts.train_no == train_no)
]
# 根据过滤后的列车编号和给定日期查询列车信息
trains = Train.query.filter(
Train.effective_time >= date,
Train.train_no.in_(valid_train_nos)
).all()
# 假设存在一个用于处理列车数据的展示器或序列化器
return trains
def buildTrain(params):
"""
根据传入的参数构建一个新的列车对象并关联相应的列车站点信息
:param params: 包含列车和站点信息的字典
:return: 构建好的列车对象
"""
# 创建一个新的 Train 对象
train = Train(
train_no=params['trainNo'],
effective_time=params['effective_time'],
expiration_time=params['expiration_time']
)
# 从站点信息中提取索引值
indexes = [e["index"] for e in params['stations']]
for e in params['stations']:
# 创建并关联 TrainStation 对象
train_station = TrainStation(
station_name=e["name"],
price=e["price"],
departure_time=e["depTime"],
arrival_time=e["arrTime"],
index=e["index"]
)
# 将列车站点对象添加到列车的站点列表中
train.train_stations.append(train_station)
# 确定第一站的出发时间和站点
if e["index"] == 0:
train.departure_time = e["depTime"]
train.departure_station = e["name"]
# 确定最后一站的到达时间和站点
if e["index"] == max(indexes):
train.arrival_time = e["arrTime"]
train.arrival_station = e["name"]
return train

@ -0,0 +1,36 @@
# 从 sqlalchemy 导入 func 函数,用于数据库操作中获取当前时间等功能
from sqlalchemy import func
# 从 app.models 导入数据库实例 db
from app.models import db
class TrainStation(db.Model):
"""
TrainStation 类表示数据库中的列车站点表继承自 SQLAlchemy db.Model
用于存储列车在各个站点的相关信息
"""
__tablename__: str = 'train_station'
# 定义列车编号列,作为外键关联到 train 表的 train_no 字段
# 同时作为联合主键的一部分,添加索引以提高查询效率
train_no = db.Column(db.String(120), db.ForeignKey('train.train_no'), primary_key=True, index=True)
# 定义站点名称列,作为外键关联到 station 表的 name 字段
# 同时作为联合主键的一部分
station_name = db.Column(db.String(120), db.ForeignKey('station.name'), primary_key=True)
# 定义车票价格列,使用 Numeric 类型存储,总位数 8 位,小数位 2 位
price = db.Column(db.Numeric(8, 2))
# 定义列车到达该站点的时间列,使用 DateTime 类型
arrival_time = db.Column(db.DateTime)
# 定义列车从该站点出发的时间列,使用 DateTime 类型
departure_time = db.Column(db.DateTime)
# 定义站点在列车路线中的索引列,整数类型,默认值为 0
index = db.Column(db.Integer, default=0)
# 定义记录创建时间列,使用 DateTime 类型,默认值为当前时间
created_at = db.Column(db.DateTime, default=func.now())
# 定义记录更新时间列,使用 DateTime 类型,默认值为当前时间
updated_at = db.Column(db.DateTime, default=func.now())
# 定义与 Station 模型的关联关系,通过 backref 可以从 Station 访问相关的 TrainStation 记录
station = db.relationship('Station', backref=db.backref('train_stations'))
# 定义与 Train 模型的关联关系,通过 backref 可以从 Train 访问相关的 TrainStation 记录
train = db.relationship('Train', backref=db.backref('train_stations'))

@ -0,0 +1,109 @@
# 从 flask 导入 Blueprint、request 和 jsonify用于创建蓝图、处理请求和返回 JSON 响应
from flask import Blueprint, request, jsonify
# 从 flask_jwt_extended 导入 get_jwt_identity 和 jwt_required用于处理 JWT 身份验证
from flask_jwt_extended import get_jwt_identity, jwt_required
# 从 app 导入数据库实例 db 和日志服务 LogService
from app import db, LogService
# 从 app.models.order_lib 导入生成订单的函数 generateOrder 和订单模型 Order
from app.models.order_lib import generateOrder, Order
# 从 presenter.order 导入订单展示器 OrderPresenter用于将订单数据转换为适合展示的格式
from presenter.order import OrderPresenter
# 从 utils 导入创建响应的函数 create_response 和状态码类 StateCode
from utils import create_response, StateCode
# 从 utils.server 导入验证订单支付的函数 verifyOrderPayment
from utils.server import verifyOrderPayment
# 创建一个名为 'order' 的蓝图
order_bp = Blueprint('order', __name__)
@order_bp.route('/orders', methods=['POST'])
@jwt_required()
def createOrder():
"""
创建新订单的路由处理函数
该函数接收一个 POST 请求使用当前用户的身份和请求中的数据生成新订单
将订单保存到数据库记录日志并返回包含订单信息的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 获取当前用户的身份信息
current_user = get_jwt_identity()
# 获取请求中的 JSON 数据
data = request.json
# 根据请求数据和当前用户生成新订单
new_order = generateOrder(data, current_user)
# 将新订单添加到数据库会话
db.session.add(new_order)
# 提交数据库会话,将新订单保存到数据库
db.session.commit()
# 使用订单展示器将订单数据转换为适合展示的字典格式
order_presenter = OrderPresenter(new_order).as_dict()
# 记录日志
LogService.log()
# 返回包含成功状态码和订单信息的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS, data=order_presenter)), 200
@order_bp.route('/orders/<string:order_no>/query_payment', methods=['POST'])
@jwt_required()
def queryPayment(order_no):
"""
查询订单支付状态的路由处理函数
该函数接收一个 POST 请求包含订单编号验证订单支付状态
如果支付成功更新订单状态记录日志并返回包含订单信息的成功响应
如果支付失败记录日志并返回包含错误状态码的响应
Args:
order_no (str): 要查询的订单编号
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 获取当前用户的身份信息
current_user = get_jwt_identity()
# 验证订单支付状态
response = verifyOrderPayment(order_no)
# 检查响应状态码是否为 200 且支付状态为成功
if response.status_code == 200 and response.json().get("data")["state"] == "successful":
# 如果支付成功,更新订单状态和支付时间
order = Order.setOrderState(order_no, current_user,
state=1, payment_time=response.json().get("data")["pay_time"])
# 使用订单展示器将订单数据转换为适合展示的字典格式
order_presenter = OrderPresenter(order).as_dict()
# 记录日志
LogService.log()
# 返回包含成功状态码和订单信息的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS, data=order_presenter)), 200
else:
# 如果支付失败,记录日志
LogService.log()
# 返回包含订单支付错误状态码的 JSON 响应,状态码为 400
return jsonify(create_response(StateCode.ORDER_PAY_ERROR)), 400
@order_bp.route('/orders', methods=['GET'])
@jwt_required()
def queryOrder():
"""
查询订单的路由处理函数
该函数接收一个 GET 请求根据请求参数中的订单状态和当前用户的身份信息
从数据库中查询符合条件的订单将订单数据转换为适合展示的格式
记录日志并返回包含订单信息的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 获取请求参数中的订单状态
state = request.args.get("state")
# 获取当前用户的身份信息
current_user = get_jwt_identity()
# 从数据库中查询符合条件的订单
orders = Order.query.filter(Order.passenger_id == current_user, Order.state == state).all()
# 使用订单展示器将订单数据转换为适合展示的字典列表
order_presenter = [OrderPresenter(order).as_dict() for order in orders]
# 记录日志
LogService.log()
# 返回包含成功状态码和订单信息的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS, data=order_presenter)), 200

@ -0,0 +1,133 @@
# 从 flask 导入 Blueprint、request 和 jsonify
# Blueprint 用于创建 Flask 蓝图,便于组织路由
# request 用于处理 HTTP 请求
# jsonify 用于将 Python 字典或列表转换为 JSON 响应
from flask import Blueprint, request, jsonify
# 从 flask_jwt_extended 导入创建访问令牌、JWT 认证装饰器和获取 JWT 身份的函数
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
# 从 app 导入 Redis 客户端和日志服务
from app import redis_client, LogService
# 从 app.models.passenger_lib 导入乘客模型和可用性检查函数
from app.models.passenger_lib import Passenger, isAvailable
# 从 presenter 导入乘客展示器,用于格式化乘客数据
from presenter import PassengerPresenter
# 从 utils 导入状态码、创建响应的函数以及一些检查函数
from utils import StateCode, create_response, checkMobile, checkIdCard, checkBankCard
# 创建一个名为'register'的蓝图
register_bp = Blueprint('register', __name__)
@register_bp.route('/register', methods=['POST'])
def register():
"""
处理用户注册请求
该函数接收一个包含用户注册信息的 POST 请求
验证各项信息的有效性若信息有效则创建新乘客记录
生成访问令牌记录日志并返回包含用户信息和令牌的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 获取请求中的 JSON 数据
data = request.json
# 从数据中提取账户名
account = data.get('account')
# 从数据中提取密码
password = data.get('password')
# 从数据中提取手机号码
mobile_no = data.get('mobileNo')
# 从数据中提取手机验证码
mobile_code = data.get('mobileCode')
# 从数据中提取身份证号码
id_card_no = data.get('idCardNo')
# 从数据中提取银行卡号码
bank_card_no = data.get('bankCard')
# 检查账户名或密码是否为空
if not account or not password:
# 若为空,返回参数错误的响应
return jsonify(create_response(StateCode.PARAMS_ERROR)), 400
# 检查手机号码格式是否正确
if not checkMobile(mobile_no):
# 若格式不正确,返回手机号码错误的响应
return jsonify(create_response(StateCode.MOBILE_ERROR)), 400
# 验证手机验证码是否正确
if not verifyCode(mobile_no, mobile_code):
# 若验证码不正确,返回手机验证码错误的响应
return jsonify(create_response(StateCode.MOBILE_CODE_ERROR)), 400
# 检查身份证号码格式是否正确
if not checkIdCard(id_card_no):
# 若格式不正确,返回身份证号码错误的响应
return jsonify(create_response(StateCode.ID_CARD_ERROR)), 400
# 检查账户名是否已存在
if isAvailable(account):
# 若已存在,返回用户已存在的响应
return jsonify(create_response(StateCode.USER_ALREADY_EXISTS)), 400
# 检查银行卡号码格式是否正确
if not checkBankCard(bank_card_no):
# 若格式不正确,返回银行卡号码错误的响应
return jsonify(create_response(StateCode.BANK_CARD_ERROR)), 400
# 创建新的乘客记录
new_passenger = Passenger.create(data=data)
# 为新乘客生成访问令牌
access_token = create_access_token(identity=new_passenger.id)
# 使用乘客展示器格式化乘客数据并添加令牌
user_presenter = PassengerPresenter(new_passenger, {"token": access_token}).as_dict()
# 记录日志
LogService.log()
# 返回包含成功状态码和用户信息的响应
return jsonify(create_response(StateCode.SUCCESS, data=user_presenter)), 200
@register_bp.route('/auth', methods=['GET'])
@jwt_required()
def auth():
"""
验证用户身份
该函数接收一个需要 JWT 认证的 GET 请求
获取当前用户的身份信息从数据库中查询用户记录
若用户存在则格式化用户信息记录日志并返回包含用户信息的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 获取当前用户的 JWT 身份信息
current_user = get_jwt_identity()
# 从数据库中查询当前用户的记录
user = Passenger.query.get(current_user)
# 检查用户是否存在
if not user:
# 若用户不存在,返回用户未找到的响应
return jsonify(create_response(StateCode.USER_NOT_FOUND)), 400
# 使用乘客展示器格式化用户数据
user_presenter = PassengerPresenter(user, {}).as_dict()
# 记录日志
LogService.log()
# 返回包含成功状态码和用户信息的响应
return jsonify(create_response(code=StateCode.SUCCESS, data=user_presenter)), 200
def verifyCode(mobile_no, code):
"""
验证手机验证码
该函数从 Redis 中获取存储的手机验证码
并与传入的验证码进行比较返回验证结果
Args:
mobile_no (str): 手机号码
code (str): 传入的验证码
Returns:
bool: 验证码是否匹配
"""
# 从 Redis 中获取存储的验证码
stored_code = redis_client.get(mobile_no)
# 比较存储的验证码和传入的验证码
return stored_code == code

@ -0,0 +1,92 @@
import iso8601
from flask import Blueprint, jsonify, request
from flask_jwt_extended import get_jwt_identity, jwt_required
from app import LogService
from app.models import Station
from app.models.ticket_lib import Ticket
from app.models.train_lib import Train
from app.station_manager import station_bp
from presenter import StationPresenter
from presenter.ticket import TicketPresenter
from presenter.train import TrainPresenter
from utils import create_response, StateCode
# 创建一个名为 'query' 的 Flask 蓝图,用于管理查询相关的路由
query_bp = Blueprint('query', __name__)
@query_bp.route('/trains/query_train', methods=['GET'])
def queryTrains():
"""
查询符合条件的车次信息
该函数从请求参数中获取出发站到达站和日期信息
验证参数的有效性若有效则查询符合条件的车次
将车次信息转换为适合展示的格式记录日志并返回包含车次信息的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 从请求参数中获取出发站
from_station = request.args.get('from')
# 从请求参数中获取到达站
to_station = request.args.get('to')
# 从请求参数中获取日期并使用 iso8601 解析为日期对象
date = iso8601.parse_date(request.args.get('date'))
# 检查出发站、到达站和日期是否为空
if not from_station or not to_station or not date:
# 若为空,返回参数错误的响应
return jsonify(create_response(StateCode.PARAMS_ERROR)), 400
# 查询符合条件的车次
trains = Train.queryTrains(from_station, to_station, date)
# 使用列车展示器将车次信息转换为适合展示的字典列表
trains_presenters = [TrainPresenter(train).as_dict() for train in trains]
# 记录日志
LogService.log()
# 返回包含成功状态码和车次信息的响应
return jsonify(create_response(StateCode.SUCCESS, data=trains_presenters)), 200
@query_bp.route('/tickets', methods=['GET'])
@jwt_required()
def getTickets():
"""
查询当前用户的车票信息
该函数需要 JWT 认证获取当前用户的身份信息
从数据库中查询该用户的车票记录
将车票信息转换为适合展示的格式记录日志并返回包含车票信息的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 获取当前用户的 JWT 身份信息
current_user = get_jwt_identity()
# 从数据库中查询当前用户的车票记录
tickets = Ticket.query.filter_by(passenger_id=current_user).all()
# 使用车票展示器将车票信息转换为适合展示的字典列表
tickets_presenters = [TicketPresenter(ticket).as_dict() for ticket in tickets]
# 记录日志
LogService.log()
# 返回包含成功状态码和车票信息的响应
return jsonify(create_response(StateCode.SUCCESS, data=tickets_presenters)), 200
@station_bp.route('/stations', methods=['GET'])
def queryStations():
"""
查询所有站点信息
该函数从数据库中查询所有站点记录
将站点信息转换为适合展示的格式记录日志并返回包含站点信息的成功响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码的元组
"""
# 从数据库中查询所有站点记录
stations = Station.query.all()
# 使用站点展示器将站点信息转换为适合展示的字典列表
stations_presenters = [StationPresenter(station).as_dict() for station in stations]
# 记录日志
LogService.log()
# 返回包含成功状态码和站点信息的响应
return jsonify(create_response(StateCode.SUCCESS, data=stations_presenters)), 200

@ -0,0 +1,67 @@
# 从 flask 导入 request、jsonify 和 Blueprint
# request 用于处理 HTTP 请求,获取请求中的数据
# jsonify 用于将 Python 数据结构转换为 JSON 格式的响应
# Blueprint 用于创建 Flask 应用的蓝图,将相关的路由组织在一起
from flask import request, jsonify, Blueprint
# 从 app 导入 LogService用于记录系统日志
from app import LogService
# 从 app.models 导入 Station 模型,用于与数据库中的站点表进行交互
from app.models import Station
# 从 presenter 导入 StationPresenter用于将 Station 模型对象转换为适合展示的数据格式
from presenter import StationPresenter
# 从 utils 导入 create_response 和 StateCode
# create_response 用于创建统一格式的响应数据
# StateCode 包含各种状态码,用于表示不同的响应状态
from utils import create_response, StateCode
# 创建一个名为 'stations' 的蓝图,用于管理站点相关的路由
station_bp = Blueprint('stations', __name__)
@station_bp.route('/stations', methods=['POST'])
def createStation():
"""
创建新站点的路由处理函数
该函数接收一个 POST 请求请求中应包含站点的相关数据
从请求中获取 JSON 数据使用该数据创建一个新的 Station 对象
将新站点对象转换为适合展示的字典格式记录日志
最后返回包含成功状态码和站点数据的 JSON 响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 从请求中获取 JSON 数据
data = request.json
# 使用请求数据创建一个新的 Station 对象
new_station = Station.create(data=data)
# 使用 StationPresenter 将新站点对象转换为适合展示的字典格式
station_presenter = StationPresenter(new_station).as_dict()
# 记录日志
LogService.log()
# 返回包含成功状态码和站点数据的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS, data=station_presenter)), 200
@station_bp.route('/stations/quantity_create', methods=['POST'])
def quantityCreate():
"""
批量创建站点的路由处理函数
该函数接收一个 POST 请求请求中应包含一个站点名称列表
从请求中获取站点名称列表遍历该列表为每个站点名称创建一个新的 Station 对象
记录日志最后返回包含成功状态码的 JSON 响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 从请求的 JSON 数据中获取站点名称列表
stations = request.json.get("stations")
# 遍历站点名称列表
for name in stations:
# 为每个站点名称创建一个包含名称的字典
station_hash = {"name": name}
# 使用该字典创建一个新的 Station 对象
Station.create(station_hash)
# 记录日志
LogService.log()
# 返回包含成功状态码的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS)), 200

@ -0,0 +1,71 @@
# 从 flask 导入 request、jsonify 和 Blueprint
# request 用于处理 HTTP 请求,获取请求中的数据
# jsonify 用于将 Python 数据结构转换为 JSON 格式的响应
# Blueprint 用于创建 Flask 应用的蓝图,将相关的路由组织在一起
from flask import request, jsonify, Blueprint
# 从 app 导入数据库实例 db 和 LogService
# db 用于与数据库进行交互
# LogService 用于记录系统日志
from app import db, LogService
# 从 app.models 导入 Train 模型,用于与数据库中的列车表进行交互
from app.models import Train
# 从 app.models.train_lib 导入 buildTrain 函数,用于根据数据构建列车对象
from app.models.train_lib import buildTrain
# 从 presenter.train 导入 TrainPresenter用于将 Train 模型对象转换为适合展示的数据格式
from presenter.train import TrainPresenter
# 从 utils 导入 create_response 和 StateCode
# create_response 用于创建统一格式的响应数据
# StateCode 包含各种状态码,用于表示不同的响应状态
from utils import create_response, StateCode
# 创建一个名为 'trains' 的蓝图,用于管理列车相关的路由
trains_bp = Blueprint('trains', __name__)
@trains_bp.route('/trains', methods=['GET'])
def queryAllTrains():
"""
查询所有列车信息的路由处理函数
该函数接收一个 GET 请求从数据库中查询所有的列车记录
遍历查询结果将每个列车对象转换为适合展示的字典格式
记录日志最后返回包含成功状态码和列车数据列表的 JSON 响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 从数据库中查询所有的列车记录
trains = Train.query.all()
# 遍历列车记录,将每个列车对象转换为适合展示的字典格式
trains_presenters = [TrainPresenter(train).as_dict() for train in trains]
# 记录日志
LogService.log()
# 返回包含成功状态码和列车数据列表的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS, data=trains_presenters)), 200
@trains_bp.route('/trains', methods=['POST'])
def createTrain():
"""
创建新列车的路由处理函数
该函数接收一个 POST 请求请求中应包含列车的相关数据
从请求中获取 JSON 数据使用 buildTrain 函数根据该数据构建一个新的列车对象
将新列车对象添加到数据库会话中并提交将新列车对象转换为适合展示的字典格式
记录日志最后返回包含成功状态码和列车数据的 JSON 响应
Returns:
tuple: 包含 JSON 响应和 HTTP 状态码 200 的元组
"""
# 从请求中获取 JSON 数据
data = request.json
# 使用 buildTrain 函数根据请求数据构建一个新的列车对象
new_train = buildTrain(data)
# 将新列车对象添加到数据库会话中
db.session.add(new_train)
# 提交数据库会话,将新列车对象保存到数据库
db.session.commit()
# 使用 TrainPresenter 将新列车对象转换为适合展示的字典格式
train_presenter = TrainPresenter(new_train).as_dict()
# 记录日志
LogService.log()
# 返回包含成功状态码和列车数据的 JSON 响应,状态码为 200
return jsonify(create_response(StateCode.SUCCESS, data=train_presenter)), 200

@ -0,0 +1,22 @@
import os
class Config:
"""
配置类用于存储应用程序的各种配置信息
"""
# SECRET_KEY 用于 Flask 应用的会话加密和 CSRF 保护
# 首先尝试从环境变量中获取 SECRET_KEY如果环境变量中未设置则使用默认值
# 这里将默认值注释掉,使用了一个硬编码的 SECRET_KEY实际生产中不建议这样做
# SECRET_KEY = os.environ.get('SECRET_KEY') or 'your_secret_key'
SECRET_KEY = '8665e31a29ab7e12cb0f92c1dbobj1e3a6a15230fb17'
# SQLALCHEMY_DATABASE_URI 是 SQLAlchemy 用于连接数据库的 URI
# 首先尝试从环境变量中获取 DATABASE_URL如果环境变量中未设置则使用默认的 PostgreSQL 数据库连接字符串
# 这里默认连接到本地的 PostgreSQL 数据库,数据库名为 mini12306_python用户名为 postgres密码为 6
# SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or
# 'postgresql://username:password@localhost:5432/your dbname'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or ('postgresql://postgres:6@localhost:5432'
'/mini12306_python')
# SQLALCHEMY_TRACK_MODIFICATIONS 用于控制 SQLAlchemy 是否跟踪对象的修改并发送信号
# 这里将其设置为 False以避免不必要的内存开销
SQLALCHEMY_TRACK_MODIFICATIONS = False

@ -0,0 +1,40 @@
version: '3.8'
services:
python:
build:
context: ./
dockerfile: ./Dockerfile
restart: always
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/mini12306_python
- REDIS_URL=redis
- SERVER_LIB_URL=http://py12306.learnerhub.net
ports:
- "3002:3002"
depends_on:
- db
- redis
volumes:
- /var/log/mini12306_python:/app/logs
redis:
image: redis:alpine
container_name: 12306_redis
volumes:
- redis_data:/data
db:
image: postgres:15
container_name: 12306_postgres
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: mini12306_python
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
redis_data:

@ -0,0 +1 @@
init folder

@ -0,0 +1 @@
Single-database configuration for Flask.

@ -0,0 +1,50 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

@ -0,0 +1,140 @@
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# 导入日志模块,用于记录 alembic 执行过程中的信息
# 从日志配置模块中导入 fileConfig 函数,用于读取日志配置文件
# 从 flask 导入 current_app它代表当前运行的 Flask 应用实例
# 从 alembic 导入 context它是 alembic 的上下文对象,用于管理数据库迁移操作
# 这是 Alembic Config 对象,提供对 .ini 文件中值的访问
config = context.config
# 解释配置文件以进行 Python 日志记录,这行代码基本上设置了日志记录器
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
"""
获取数据库引擎实例
尝试通过 Flask-SQLAlchemy 旧版本<3 Alchemical 的方式获取引擎
若失败则使用 Flask-SQLAlchemy 新版本>=3的方式获取
Returns:
sqlalchemy.engine.Engine: 数据库引擎实例
"""
try:
# 这种方式适用于 Flask-SQLAlchemy<3 和 Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# 这种方式适用于 Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
"""
获取数据库引擎的连接 URL
尝试渲染引擎的 URL 并隐藏密码同时处理百分号转义
若渲染失败则直接将 URL 转为字符串并处理转义
Returns:
str: 数据库连接 URL
"""
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# 为 alembic 配置设置 SQLAlchemy 的连接 URL该 URL 通过 get_engine_url 函数获取
config.set_main_option('sqlalchemy.url', get_engine_url())
# 获取当前 Flask 应用扩展中迁移插件对应的数据库实例
target_db = current_app.extensions['migrate'].db
# 其他从配置中获取的值,可根据 env.py 的需求进行定义
# 例如:
# my_important_option = config.get_main_option("my_important_option")
# ... 等等
def get_metadata():
"""
获取数据库元数据对象
检查目标数据库实例是否有多个元数据字典若有则返回默认的元数据
若没有则返回数据库实例的元数据
Returns:
sqlalchemy.schema.MetaData: 数据库元数据对象
"""
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""
以离线模式运行数据库迁移
此模式下仅配置迁移上下文的数据库连接 URL 和目标元数据
不创建实际的数据库引擎适用于生成迁移脚本而不执行迁移操作的场景
调用 context.execute() 时会将 SQL 语句输出到脚本文件中
"""
# 从配置中获取 SQLAlchemy 的连接 URL
url = config.get_main_option("sqlalchemy.url")
# 配置 alembic 迁移上下文,指定连接 URL、目标元数据和字面绑定
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
# 开始一个事务块,并在其中运行数据库迁移操作
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""
以在线模式运行数据库迁移
此模式下需要创建数据库引擎并建立连接
适用于直接在数据库上执行迁移操作的场景
包含一个回调函数用于检查是否有数据库模式的变更若没有则不生成迁移脚本
"""
# 该回调函数用于防止在模式没有变化时生成自动迁移
# 参考http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
# 获取当前 Flask 应用扩展中迁移插件的配置参数
conf_args = current_app.extensions['migrate'].configure_args
# 若配置参数中没有设置处理修订指令的回调函数,则添加该回调
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
# 获取数据库引擎实例
connectable = get_engine()
# 建立与数据库的连接,并配置 alembic 迁移上下文
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
# 开始一个事务块,并在其中运行数据库迁移操作
with context.begin_transaction():
context.run_migrations()
# 根据 alembic 上下文的模式判断是离线还是在线模式,然后执行相应的迁移操作
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

@ -0,0 +1,178 @@
"""init databases
Revision ID: 1b3bc6809b30
Revises:
Create Date: 2024-08-15 15:09:26.124279
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1b3bc6809b30'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('log',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('log_user_agent', sa.String(length=120), nullable=True),
sa.Column('log_method', sa.String(length=120), nullable=True),
sa.Column('log_quest_path', sa.String(length=120), nullable=True),
sa.Column('log_ip', sa.String(length=120), nullable=True),
sa.Column('log_params', sa.String(length=120), nullable=True),
sa.Column('log_controller', sa.String(length=120), nullable=True),
sa.Column('log_controller_action', sa.String(length=120), nullable=True),
sa.Column('log_controller_id', sa.String(length=120), nullable=True),
sa.Column('account_id', sa.String(length=120), nullable=True),
sa.Column('event_name', sa.String(length=120), nullable=True),
sa.Column('event_content', sa.Text(), nullable=True),
sa.Column('operator', sa.String(length=120), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('passenger',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=120), nullable=True),
sa.Column('account', sa.String(length=120), nullable=False),
sa.Column('password_digest', sa.String(length=2000), nullable=False),
sa.Column('id_card_no', sa.String(length=120), nullable=True),
sa.Column('mobile_no', sa.String(length=120), nullable=True),
sa.Column('bank_card_no', sa.String(length=120), nullable=True),
sa.Column('state', sa.Integer(), nullable=True),
sa.Column('member_type', sa.Integer(), nullable=True),
sa.Column('last_login_time', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('passenger', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_passenger_account'), ['account'], unique=True)
op.create_table('station',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=120), nullable=False),
sa.Column('pinyin', sa.String(length=120), nullable=True),
sa.Column('province', sa.String(length=120), nullable=True),
sa.Column('city', sa.String(length=120), nullable=True),
sa.Column('district', sa.String(length=120), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('station', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_station_name'), ['name'], unique=True)
op.create_table('train',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('train_no', sa.String(length=120), nullable=False),
sa.Column('departure_station', sa.String(length=120), nullable=True),
sa.Column('arrival_station', sa.String(length=120), nullable=True),
sa.Column('departure_time', sa.DateTime(), nullable=True),
sa.Column('expiration_time', sa.DateTime(), nullable=True),
sa.Column('effective_time', sa.DateTime(), nullable=True),
sa.Column('arrival_time', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('train', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_train_train_no'), ['train_no'], unique=True)
op.create_table('order',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_no', sa.String(length=120), nullable=False),
sa.Column('price', sa.Numeric(precision=8, scale=2), nullable=True),
sa.Column('payment_time', sa.DateTime(), nullable=True),
sa.Column('state', sa.Integer(), nullable=True),
sa.Column('passenger_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['passenger_id'], ['passenger.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('order_no')
)
with op.batch_alter_table('order', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_order_passenger_id'), ['passenger_id'], unique=False)
op.create_table('train_station',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('train_no', sa.String(length=120), nullable=False),
sa.Column('station_name', sa.String(length=120), nullable=False),
sa.Column('price', sa.Numeric(precision=8, scale=2), nullable=True),
sa.Column('arrival_time', sa.DateTime(), nullable=True),
sa.Column('departure_time', sa.DateTime(), nullable=True),
sa.Column('index', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['station_name'], ['station.name'], ),
sa.ForeignKeyConstraint(['train_no'], ['train.train_no'], ),
sa.PrimaryKeyConstraint('id', 'train_no', 'station_name')
)
with op.batch_alter_table('train_station', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_train_station_train_no'), ['train_no'], unique=False)
op.create_table('ticket',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('seat_no', sa.String(length=120), nullable=True),
sa.Column('seat_class', sa.String(length=120), nullable=True),
sa.Column('price', sa.Numeric(precision=8, scale=2), nullable=True),
sa.Column('state', sa.Integer(), nullable=True),
sa.Column('train_no', sa.String(length=120), nullable=False),
sa.Column('passenger_id', sa.Integer(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('from_station', sa.String(length=120), nullable=True),
sa.Column('to_station', sa.String(length=120), nullable=True),
sa.Column('date', sa.String(length=120), nullable=True),
sa.Column('departure_time', sa.DateTime(), nullable=True),
sa.Column('arrival_time', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], ),
sa.ForeignKeyConstraint(['passenger_id'], ['passenger.id'], ),
sa.ForeignKeyConstraint(['train_no'], ['train.train_no'], ),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('ticket', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_ticket_order_id'), ['order_id'], unique=False)
batch_op.create_index(batch_op.f('ix_ticket_passenger_id'), ['passenger_id'], unique=False)
batch_op.create_index(batch_op.f('ix_ticket_train_no'), ['train_no'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('ticket', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_ticket_train_no'))
batch_op.drop_index(batch_op.f('ix_ticket_passenger_id'))
batch_op.drop_index(batch_op.f('ix_ticket_order_id'))
op.drop_table('ticket')
with op.batch_alter_table('train_station', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_train_station_train_no'))
op.drop_table('train_station')
with op.batch_alter_table('order', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_order_passenger_id'))
op.drop_table('order')
with op.batch_alter_table('train', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_train_train_no'))
op.drop_table('train')
with op.batch_alter_table('station', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_station_name'))
op.drop_table('station')
with op.batch_alter_table('passenger', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_passenger_account'))
op.drop_table('passenger')
op.drop_table('log')
# ### end Alembic commands ###

@ -0,0 +1,32 @@
"""remove TrainStation id
Revision ID: 7866a6e8a75b
Revises: 1b3bc6809b30
Create Date: 2024-08-15 15:29:40.410360
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7866a6e8a75b'
down_revision = '1b3bc6809b30'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('train_station', schema=None) as batch_op:
batch_op.drop_column('id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('train_station', schema=None) as batch_op:
batch_op.add_column(sa.Column('id', sa.INTEGER(), autoincrement=False, nullable=False))
# ### end Alembic commands ###

@ -0,0 +1,13 @@
# myapp.py
# 从 app 模块导入 create_app 函数和 LogService 类
from app import create_app, LogService
# 从 utils 模块导入 response
from utils import response
# 调用 create_app 函数创建 Flask 应用实例
app = create_app()
# 当脚本作为主程序运行时
if __name__ == "__main__":
# 以调试模式运行 Flask 应用
app.run(debug=True)

@ -0,0 +1,12 @@
# 从当前包的 passenger 模块中导入 PassengerPresenter 类
# PassengerPresenter 类可能用于将乘客相关的数据进行处理和展示,
# 例如将数据库中的乘客数据转换为适合前端展示的格式
from .passenger import PassengerPresenter
# 从当前包的 station 模块中导入 StationPresenter 类
# StationPresenter 类可能用于处理和展示站点相关的数据,
# 比如将站点信息从数据库模型转换为易于使用的字典形式
from .station import StationPresenter
# 从当前包的 mobile_code 模块中导入 MobileCodePresenter 类
# MobileCodePresenter 类可能用于处理和展示手机验证码相关的数据,
# 例如将验证码数据进行格式化,以便在接口中返回给客户端
from .mobile_code import MobileCodePresenter

@ -0,0 +1,25 @@
class MobileCodePresenter:
"""
MobileCodePresenter 类用于将手机验证码数据封装成字典形式
该类接收一个验证码数据并提供一个方法将其转换为适合展示或传输的字典格式
"""
def __init__(self, data):
"""
初始化 MobileCodePresenter 类的实例
Args:
data: 要封装的手机验证码数据
"""
self.data = data
def as_dict(self):
"""
将手机验证码数据转换为字典形式
Returns:
dict: 包含手机验证码的字典键为 "code"
"""
return {
"code": self.data
}

@ -0,0 +1,40 @@
from presenter.ticket import TicketPresenter
class OrderPresenter:
"""
OrderPresenter 类用于将订单数据转换为字典格式方便在不同层之间传递和展示
它接收一个订单数据对象并将其各个属性转换为字典中的键值对
其中订单中的车票数据会通过 TicketPresenter 类进一步处理
"""
def __init__(self, data):
"""
初始化 OrderPresenter 实例
Args:
data: 订单数据对象通常是从数据库查询得到的订单记录
该对象应包含订单的各种属性 id订单号价格等
"""
self.data = data
def as_dict(self):
"""
将订单数据转换为字典格式
Returns:
dict: 包含订单详细信息的字典键包括 "id""orderNo""price"
"state""paymentTime" "tickets"
"""
return {
# 订单的唯一标识符
"id": self.data.id,
# 订单号
"orderNo": self.data.order_no,
# 订单价格
"price": self.data.price,
# 订单状态
"state": self.data.state,
# 订单支付时间
"paymentTime": self.data.payment_time,
# 订单中的车票信息,使用 TicketPresenter 类将每张车票数据转换为字典
"tickets": [TicketPresenter(ticket).as_dict() for ticket in self.data.tickets]
}

@ -0,0 +1,59 @@
class PassengerPresenter:
"""
PassengerPresenter 类用于将乘客数据转换为字典形式方便在不同层之间传递和展示
该类接收乘客数据和辅助数据并提供一个方法将这些数据组合成字典格式
"""
def __init__(self, data, aid_data=None):
"""
初始化 PassengerPresenter 实例
Args:
data: 乘客数据对象通常包含乘客的基本信息 id账户姓名等
aid_data (dict, optional): 辅助数据默认为 None可用于补充乘客数据中未包含的信息
"""
self.data = data
self.aid_data = aid_data
def as_dict(self):
"""
将乘客数据和辅助数据转换为字典形式
Returns:
dict: 包含乘客信息的字典键包括 "id""account""name""idCardNo"
"mobileNo""bankCardNo""state""memberType"并合并辅助数据
"""
return {
# 乘客的唯一标识符
"id": self.data.id,
# 乘客的账户信息
"account": self.data.account,
# 乘客的姓名
"name": self.data.name,
# 乘客的身份证号码
"idCardNo": self.data.id_card_no,
# 乘客的手机号码
"mobileNo": self.data.mobile_no,
# 乘客的银行卡号码
"bankCardNo": self.data.bank_card_no,
# 乘客的状态
"state": self.data.state,
# 乘客的会员类型
"memberType": self.data.member_type
} | self.aid_data if self.aid_data else {
# 乘客的唯一标识符
"id": self.data.id,
# 乘客的账户信息
"account": self.data.account,
# 乘客的姓名
"name": self.data.name,
# 乘客的身份证号码
"idCardNo": self.data.id_card_no,
# 乘客的手机号码
"mobileNo": self.data.mobile_no,
# 乘客的银行卡号码
"bankCardNo": self.data.bank_card_no,
# 乘客的状态
"state": self.data.state,
# 乘客的会员类型
"memberType": self.data.member_type
}

@ -0,0 +1,29 @@
class StationPresenter:
"""
StationPresenter 类用于将站点数据对象转换为字典形式
方便在不同模块或系统中进行数据传递和展示
"""
def __init__(self, data):
"""
初始化 StationPresenter 实例
Args:
data: 站点数据对象该对象应包含站点的相关属性 ID名称拼音等
"""
self.data = data
def as_dict(self):
"""
将站点数据对象转换为字典形式
Returns:
dict: 包含站点信息的字典键有 "id""name""pinyin"
"""
return {
# 站点的唯一标识符
"id": self.data.id,
# 站点的名称
"name": self.data.name,
# 站点名称的拼音
"pinyin": self.data.pinyin,
}

@ -0,0 +1,51 @@
class TicketPresenter:
"""
TicketPresenter 类用于将车票数据对象转换为字典形式
方便在不同的模块或系统中进行数据传输和展示
"""
def __init__(self, data):
"""
初始化 TicketPresenter 实例
Args:
data: 车票数据对象该对象应包含车票的各种属性
如车票 ID座位号座位类型价格等
"""
self.data = data
def as_dict(self):
"""
将车票数据对象转换为字典形式
Returns:
dict: 包含车票详细信息的字典
键有 "id""seatNo""seatClass""price"
"state""trainNo""from""orderNo""to"
"date""fromTime" "toTime"
"""
return {
# 车票的唯一标识符
"id": self.data.id,
# 车票对应的座位编号
"seatNo": self.data.seat_no,
# 座位的类型,如一等座、二等座等
"seatClass": self.data.seat_class,
# 车票的价格
"price": self.data.price,
# 车票的状态,如已售出、未售出等
"state": self.data.state,
# 列车的编号
"trainNo": self.data.train_no,
# 出发站
"from": self.data.from_station,
# 关联的订单编号
"orderNo": self.data.order.order_no,
# 到达站
"to": self.data.to_station,
# 乘车日期
"date": self.data.date,
# 出发时间
"fromTime": self.data.departure_time,
# 到达时间
"toTime": self.data.arrival_time,
}

@ -0,0 +1,40 @@
from presenter.train_station import TrainStationPresenter
class TrainPresenter:
"""
TrainPresenter 类用于将列车数据对象转换为字典形式
方便在不同模块或系统中进行数据传递和展示
"""
def __init__(self, data):
"""
初始化 TrainPresenter 实例
Args:
data: 列车数据对象包含列车的各种属性 ID车次到达时间等
"""
self.data = data
def as_dict(self):
"""
将列车数据对象转换为字典形式
Returns:
dict: 包含列车详细信息的字典键有 "id""trainNo""arrTime"
"depTime""arr""dep" "stations"
"""
return {
# 列车的唯一标识符
"id": self.data.id,
# 列车车次
"trainNo": self.data.train_no,
# 列车到达时间
"arrTime": self.data.arrival_time,
# 列车出发时间
"depTime": self.data.departure_time,
# 列车到达站
"arr": self.data.arrival_station,
# 列车出发站
"dep": self.data.departure_station,
# 列车经过的站点信息,每个站点信息通过 TrainStationPresenter 转换为字典形式
"stations": [TrainStationPresenter(station).as_dict() for station in self.data.train_stations]
}

@ -0,0 +1,37 @@
class TrainStationPresenter:
"""
TrainStationPresenter 类用于将列车站点数据对象转换为字典形式
方便在不同模块或系统中进行数据传递和展示
"""
def __init__(self, data):
"""
初始化 TrainStationPresenter 实例
Args:
data: 列车站点数据对象包含站点的相关属性
如索引车次站点名称到达时间等
"""
self.data = data
def as_dict(self):
"""
将列车站点数据对象转换为字典形式
Returns:
dict: 包含列车站点信息的字典键有 "index""trainNo""name"
"arrTime""depTime" "price"
"""
return {
# 站点在列车线路中的索引
"index": self.data.index,
# 列车车次
"trainNo": self.data.train_no,
# 站点名称
"name": self.data.station_name,
# 列车到达该站点的时间
"arrTime": self.data.arrival_time,
# 列车从该站点出发的时间
"depTime": self.data.departure_time,
# 该站点相关的票价
"price": self.data.price,
}

@ -0,0 +1,110 @@
### 目录结构说明
```
Mini-12306
-- app //主要接口业务代码
---- models //定义了数据库表结构与抽象类方法
-- logs //日志存储
-- migrations //执行迁移后的数据库版本文件,请查看Flask-Migrate使用方法。
-- presenter //渲染文件,用于数据序列化
-- utils //工具包
---- response.py //定义返回数据结构及定义状态码
-- Dockerfile //docker容器化文件
-- docker-compose.yml //容器编排文件
```
### 开发环境安装
1. 安装 Python 3.12.x以上版本
2. 使用 pip 安装项目所需要的包:
```
cd 12306mini_python
pip config set global.index-url http://mirrors.aliyun.com/pypi/simple
pip config set install.trusted-host mirrors.aliyun.com
pip install -r requirements.txt
```
3. 安装 PostgreSQL, 并创建数据库
根目录下的config.py文件中 SQLALCHEMY_DATABASE_URI 是定义项目连接数据库的地址(包括数据库账号、密码、名称)。
连接数据库的密码及数据库均可以自行设定。
4. 执行迁移
在项目的根目录下执行下列命令,此命令会将数据表生成。
表的生成逻辑请查看Flask-Migrate使用方法。
```
flask db upgrade
```
5. 运行项目
```
flask run myapp.py
```
### docker部署
1. 找一台Ubuntu服务器安装docker及docker-compose
2. 在服务器上创建日志文件存储位置(该文件用于 docker-compose 中 volumes 关联)
```
mkdir /var/log/mini12306_python
```
3. 构建容器
```
docker-compose build
```
你可能会遇到build失败的问题。
因为国内的原因无法正常自动下载 postgres 和 redis_alpine 这两个软件。
解决方案如下:
下载此链接中两个docker软件包至服务器: postgres_15.tar redis_alpine.tar
https://pan.baidu.com/s/1qMRtQ0Fhy7r5L_58RHe5Fw?pwd=vu3c 提取码: vu3c
可以将文件先下载到本机然后使用scp命令将文件上传至服务器。
文件上传至服务后cd至文件所在的目录然后运行下列指令
```
docker load < postgres_15.tar
docker load < postgres_15.tar
然后再执行 docker-compose build
```
4. 运行容器
```
docker-compose run --rm python flask db upgrade
docker-compose up -d
```
---
### 扩展使用: Flask-Migrate 使用方法
首先,初始化 Alembic 目录migrations/ 文件夹):
```
flask db init
```
当你对模型进行了更改(如添加新表或修改现有表的结构),你可以生成迁移脚本:
```
flask db migrate -m "Initial migration"
```
这将根据你定义的模型生成迁移脚本。
执行迁移
```
flask db upgrade
```
---
### 扩展使用:使用 pdb 工具断点调试
```
pdb.set_trace()
```

@ -0,0 +1,21 @@
Flask~=3.0.3
Flask-SQLAlchemy
psycopg2-binary
PyJWT
Flask-Migrate
itsdangerous
redis~=5.0.8
Flask-JWT-Extended
pypinyin~=0.52.0
Werkzeug~=3.0.3
SQLAlchemy~=2.0.32
alembic~=1.13.2
python-dateutil
requests
gunicorn==22.0.0
Levenshtein==0.25.1
munkres==1.1.4
argparse==1.4.0
flask-cors==4.0.1
loguru
iso8601

@ -0,0 +1,9 @@
# 从当前包的 response 模块中导入 create_response 函数和 StateCode 类
# create_response 函数可能用于创建统一格式的响应数据,方便接口返回规范的数据
# StateCode 类可能用于定义不同的状态码,以便在响应中明确表示操作结果
from .response import create_response, StateCode
# 从当前包的 server 模块中导入三个检查函数
# checkMobile 函数可能用于检查手机号码的格式是否正确
# checkIdCard 函数可能用于检查身份证号码的格式和有效性
# checkBankCard 函数可能用于检查银行卡号码的格式和有效性
from .server import checkMobile, checkIdCard, checkBankCard

@ -0,0 +1,55 @@
from enum import Enum
from typing import Any, Dict, List, Union
# 定义一个枚举类 StateCode用于存储各种状态码
class StateCode(Enum):
SUCCESS = 0
NOT_AUTHORIZED = 1002
PARAMS_ERROR = 1003
USER_NOT_FOUND = 2001
USER_ALREADY_EXISTS = 2002
PASSWORD_INCORRECT = 2003
MOBILE_CODE_ERROR = 2004
ID_CARD_ERROR = 2005
BANK_CARD_ERROR = 2006
MOBILE_ERROR = 2007
ORDER_PAY_ERROR = 3001
STATE_MESSAGES = {
StateCode.SUCCESS: "OK",
StateCode.PARAMS_ERROR: "缺少参数",
StateCode.USER_NOT_FOUND: "用户不存在,操作错误",
StateCode.NOT_AUTHORIZED: "未登录",
StateCode.USER_ALREADY_EXISTS: "用户已存在",
StateCode.PASSWORD_INCORRECT: "用户名或密码错误",
StateCode.MOBILE_CODE_ERROR: "验证码错误",
StateCode.ID_CARD_ERROR: "身份证号验证失败",
StateCode.MOBILE_ERROR: "手机号格式有误",
StateCode.BANK_CARD_ERROR: "银行卡验证错误",
StateCode.ORDER_PAY_ERROR: "支付失败"
}
# 定义一个函数 create_response用于创建统一格式的响应数据
def create_response(
code: StateCode,
message: str = None,
data: Union[Dict[str, Any], List[Dict[str, Any]], Any] = None
) -> Dict[str, Any]:
"""
创建统一的返回格式
:param code: ErrorCode 错误码
:param message: 错误信息可选如果不传递则使用默认的错误信息
:param data: 包含单个或多个 Presenter 转换后的数据单个使用 dict多个使用 list
:return: 返回统一格式的字典
"""
return {
# 状态码的值
"code": code.value,
# 消息,如果传递了 message 则使用传递的值,否则从 STATE_MESSAGES 中获取默认消息
"msg": message or STATE_MESSAGES.get(code, "Unknown error"),
# 数据
"data": data
}

@ -0,0 +1,94 @@
import os
import requests
# 从环境变量中获取服务器库的 URL如果环境变量未设置则使用默认值 "http://127.0.0.1:5000"
hosts = os.getenv('SERVER_LIB_URL', "http://127.0.0.1:5000")
def checkIdCard(id_card):
"""
验证身份证号码是否有效
该函数通过向服务器发送 POST 请求来验证身份证号码
Args:
id_card (str): 要验证的身份证号码
Returns:
bool: 如果验证成功返回 True否则返回 False
"""
# 构造验证身份证号码的 URL
url = hosts + "/id_card/verify"
# 构造请求参数
params = {
'idCardNo': id_card
}
# 发送 POST 请求,将参数以 JSON 格式发送
response = requests.post(url, json=params)
# 检查响应状态码是否为 200 且响应数据中的 "result" 字段为 True
return response.status_code == 200 and response.json().get("data")["result"]
def checkBankCard(bank_card):
"""
验证银行卡号码是否有效
该函数通过向服务器发送 POST 请求来验证银行卡号码
Args:
bank_card (str): 要验证的银行卡号码
Returns:
bool: 如果验证成功返回 True否则返回 False
"""
# 构造验证银行卡号码的 URL
url = hosts + "/bank/card_verify"
# 构造请求参数
params = {
'bankCard': bank_card
}
# 发送 POST 请求,将参数以 JSON 格式发送
response = requests.post(url, json=params)
# 检查响应状态码是否为 200 且响应数据中的 "result" 字段为 True
return response.status_code == 200 and response.json().get("data")["result"]
def checkMobile(mobile_no):
"""
验证手机号码是否有效
该函数通过向服务器发送 POST 请求来验证手机号码
Args:
mobile_no (str): 要验证的手机号码
Returns:
bool: 如果验证成功返回 True否则返回 False
"""
# 构造验证手机号码的 URL
url = hosts + "/mobile/check"
# 构造请求参数
params = {
'mobileNo': mobile_no
}
# 发送 POST 请求,将参数以 JSON 格式发送
response = requests.post(url, json=params)
# 检查响应状态码是否为 200 且响应数据中的 "result" 字段为 True
return response.status_code == 200 and response.json().get("data")["result"]
def verifyOrderPayment(order_no):
"""
验证订单支付状态
该函数模拟向支付中心发送验证请求通过向服务器发送 POST 请求来验证订单支付状态
Args:
order_no (str): 要验证的订单号
Returns:
requests.Response: 服务器的响应对象
"""
# 构造验证订单支付状态的 URL
url = hosts + "/bank/query"
# 构造请求参数
params = {"OrderNo": order_no}
# 发送 POST 请求,将参数以 JSON 格式发送
response = requests.post(url, json=params)
return response
Loading…
Cancel
Save