diff --git a/Mini-12306-python/.gitignore b/Mini-12306-python/.gitignore new file mode 100644 index 0000000..4af61f6 --- /dev/null +++ b/Mini-12306-python/.gitignore @@ -0,0 +1,3 @@ +.idea +__pycache__/ +*.log \ No newline at end of file diff --git a/Mini-12306-python/Dockerfile b/Mini-12306-python/Dockerfile new file mode 100644 index 0000000..04aec79 --- /dev/null +++ b/Mini-12306-python/Dockerfile @@ -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" ] diff --git a/Mini-12306-python/app/__init__.py b/Mini-12306-python/app/__init__.py new file mode 100644 index 0000000..d392fef --- /dev/null +++ b/Mini-12306-python/app/__init__.py @@ -0,0 +1,77 @@ +# app/__init__.py +import os +from datetime import timedelta + +import redis +from flask import Flask, request +from flask_cors import CORS +from flask_jwt_extended import JWTManager +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy +from loguru import logger +from config import Config +from app.log_service import LogService + +db = SQLAlchemy() +# redis_host_url = os.getenv('REDIS_URL', 'localhost') +# redis_client = redis.StrictRedis(host=redis_host_url, port=6379, db=0, decode_responses=True) +# 使用完整 URL 创建 Redis 客户端 +redis_client = redis.from_url(Config.REDIS_URL, decode_responses=True) + +# 做一些准备工作,创建 Flask 应用, 配置 JWT 令牌,初始化数据库, 日志系统,注册蓝图(Blueprints)--是各个模块独立独立管理 +def create_app(): + # 创建 Flask 应用,并指定静态文件夹为 'mini12306' + app = Flask(__name__, static_folder='mini12306') + + # 允许跨域请求(CORS) + CORS(app) + + # 推送应用上下文,确保在应用初始化时可以使用 app 相关对象 + app.app_context().push() + + # 从配置对象加载应用配置 + app.config.from_object(Config) + + # 将 redis_client 挂载到 app 上,便于全局访问 + app.redis_client = redis_client + + # 设置 JWT 令牌的有效期为 30 天 + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=30) + + # 初始化数据库 + 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 # 订单管理 + + # 注册蓝图(Blueprints),将各个功能模块绑定到 Flask 应用 + 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) + + # 返回 Flask 应用实例 + return app + diff --git a/Mini-12306-python/app/bank_server.py b/Mini-12306-python/app/bank_server.py new file mode 100644 index 0000000..ae3ff69 --- /dev/null +++ b/Mini-12306-python/app/bank_server.py @@ -0,0 +1,38 @@ +import re +import datetime + +from flask import Blueprint, jsonify, request + +from app import LogService +from utils import create_response, StateCode + +bank_bp = Blueprint('bank_server', __name__) + + +#校验银行卡号 +@bank_bp.route('/bank/card_verify', methods=['POST']) +def bankCardVerify(): + pattern = r'^\d{13,19}$' + # 确保银行卡号是一个完整的 13 到 19 位的数字字符串 + number = request.json.get('bankCard') + state = { + "result": bool(re.match(pattern, number)) + } + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=state)), 200 + + +@bank_bp.route('/bank/pay', methods=['POST']) +def pay(): + # 模拟支付成功,返回 + state = {"state": "successful", "pay_time": datetime.datetime.now()} + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=state)), 200 + + +@bank_bp.route('/bank/query', methods=['POST']) +def query(): + # 模拟验证成功,返回 + state = {"state": "successful", "pay_time": datetime.datetime.now()} + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=state)), 200 diff --git a/Mini-12306-python/app/id_card_server.py b/Mini-12306-python/app/id_card_server.py new file mode 100644 index 0000000..1e9af5a --- /dev/null +++ b/Mini-12306-python/app/id_card_server.py @@ -0,0 +1,25 @@ +import re +from flask import Blueprint, jsonify, request + +from app import LogService +from utils import create_response, StateCode + +id_card_bp = Blueprint('id_card_server', __name__) + + +#校验身份证号码 +@id_card_bp.route('/id_card/verify', methods=['POST']) +def idCardVerify(): + print("idCardVerify() 被调用了") + pattern = r'^\d{18}$' + id_number = request.json.get('idCardNo') + print(f"Received ID number: {id_number}") # 先打印看看 + state = { + "result": bool(re.match(pattern, id_number)) + } + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=state)), 200 + + + + diff --git a/Mini-12306-python/app/log_service.py b/Mini-12306-python/app/log_service.py new file mode 100644 index 0000000..73426e9 --- /dev/null +++ b/Mini-12306-python/app/log_service.py @@ -0,0 +1,25 @@ +from flask import request +from loguru import logger + + +class LogService: + @staticmethod + def configure_logging(): + logger.remove() # 移除默认的日志配置 + logger.add( + "logs/app.log", # 日志文件名 + rotation="1 week", # 每周轮换 + retention="1 month", # 保留一个月 + level="INFO", # 记录 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)}") diff --git a/Mini-12306-python/app/login_manager.py b/Mini-12306-python/app/login_manager.py new file mode 100644 index 0000000..7561f3f --- /dev/null +++ b/Mini-12306-python/app/login_manager.py @@ -0,0 +1,26 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import create_access_token + +from app import LogService +from app.models import Passenger +from presenter import PassengerPresenter +from utils import StateCode, create_response + +login_bp = Blueprint('login', __name__) + +#登录模块 +@login_bp.route('/login', methods=['POST']) +def login(): + data = request.json + account = data.get('account') + password = data.get('password') + if not account or not password: + return jsonify(create_response(StateCode.PARAMS_ERROR)), 400 + user = Passenger.verifyPassenger(account, password) + if user: + # 为新创建的乘客生成访问令牌 + access_token = create_access_token(identity=str(user.id)) + user_presenter = PassengerPresenter(user, {"token": access_token}).as_dict() + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=user_presenter)), 200 + return jsonify(create_response(StateCode.PASSWORD_INCORRECT)), 400 diff --git a/Mini-12306-python/app/mobile_server.py b/Mini-12306-python/app/mobile_server.py new file mode 100644 index 0000000..a8ffae4 --- /dev/null +++ b/Mini-12306-python/app/mobile_server.py @@ -0,0 +1,56 @@ +import random +import re +from flask import current_app, jsonify, request, Blueprint + +import utils +from app import redis_client, LogService +from presenter import MobileCodePresenter +from utils import create_response, StateCode + +key = current_app.config['SECRET_KEY'] +mobile_bp = Blueprint('mobile_server', __name__) + + +@mobile_bp.route('/mobile/get_verify_code', methods=['POST']) +# 发送一次性密码(验证码) +def getVerifyCode(): + mobile_no = request.json.get('mobileNo') + if not utils.checkMobile(mobile_no): + return jsonify(create_response(StateCode.MOBILE_ERROR)), 400 + sent_code = getVerificationCode(mobile_no) + mobile_present = MobileCodePresenter(sent_code).as_dict() + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=mobile_present)), 200 + + +#检验手机号的格式 +@mobile_bp.route('/mobile/check', methods=['POST']) +def checkMobile(): + #正则表达式,用户输入的必须是中国大陆的手机号码格式,例如11位,第一位必须是1等 + pattern = r'^1[3-9]\d{9}$' + #获取到客户输入的手机号 + number = request.json.get('mobileNo') + print(number) + state = { + #进行匹配,将匹配的结果保存在state字典中 + "result": bool(re.match(pattern, number)) + } + LogService.log() #记录日志 + #返回一个json格式的数据 + return jsonify(create_response(StateCode.SUCCESS, data=state)), 200 + + +def getVerificationCode(mobile_no): + verification_code = generateRandomNumber() # 生成验证码 + print(verification_code) + if redis_client.set(mobile_no, verification_code, 300): + return verification_code + else: + print(f"{redis_client.client}") + + +# 生成6位随机数字 +def generateRandomNumber(length=6): + # 确保生成的数字至少有6位,即使前几位是0 + number = random.randint(10 ** (length - 1), 10 ** length - 1) + return number diff --git a/Mini-12306-python/app/models/__init__.py b/Mini-12306-python/app/models/__init__.py new file mode 100644 index 0000000..d3c4fe1 --- /dev/null +++ b/Mini-12306-python/app/models/__init__.py @@ -0,0 +1,6 @@ +from app import db +from .order_lib import Order +from .passenger_lib import Passenger +from .station_lib import Station +from .train_lib import Train + diff --git a/Mini-12306-python/app/models/order_lib.py b/Mini-12306-python/app/models/order_lib.py new file mode 100644 index 0000000..b5ed321 --- /dev/null +++ b/Mini-12306-python/app/models/order_lib.py @@ -0,0 +1,125 @@ +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 + + +class Order(db.Model): + __tablename__ = 'order' + + id = db.Column(db.Integer, primary_key=True) + order_no = db.Column(db.String(120), unique=True, nullable=False) + price = db.Column(db.Numeric(8, 2)) + payment_time = db.Column(db.DateTime) + state = db.Column(db.Integer, default=0) + 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 = db.relationship('Passenger', backref=db.backref('orders')) + + def __repr__(self): + return f'' + + @classmethod + def setOrderState(cls, order_no, passenger_id, **kwargs): + # order = cls.query.filter_by(order_no=order_no, passenger_id=passenger_id).first() + # if order: + # # 更新订单的属性 + # for key, value in kwargs.items(): + # setattr(order, key, value) + # if key == 'state' and value == 1: + # for ticket in order.tickets: + # Ticket.updateState(ticket) # Update state to 1 + # # 提交更改到数据库 + # db.session.commit() + # return order + # else: + # # 处理订单未找到的情况 + # return None + try: + # 尝试从数据库中查询特定的订单 + order = cls.query.filter_by(order_no=order_no, passenger_id=passenger_id).first() + if order: + # 如果找到了订单,则根据传入的参数更新订单的属性 + for key, value in kwargs.items(): + setattr(order, key, value) + # 如果更新了订单状态为1,则更新该订单下所有车票的状态 + if key == 'state' and value == 1: + for ticket in order.tickets: + Ticket.updateState(ticket) + # 提交更改到数据库 + db.session.commit() + return order + else: + # 如果没有找到订单,返回None + return None + except Exception as e: + # 如果发生异常,回滚数据库更改并重新抛出异常 + db.session.rollback() + raise e + + +def generate_unique_id(length=4): + # 获取当前的秒级时间戳 + timestamp_seconds = int(time.time()) + + # 生成 4 位随机数 + random_number = random.randint(0, 10 ** length - 1) + + # 确保随机数是 4 位数字 + random_number_str = str(random_number).zfill(length) + + # 生成唯一标识符 + unique_id = f"{timestamp_seconds}{random_number_str}" + return unique_id + + +def generateOrder(params, passenger_id): + # Create a new Order object + order = Order( + order_no=generate_unique_id(5), + passenger_id=passenger_id + ) + + for e in params['tickets']: + # 创建Ticket对象 + 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 > from_station.index, + TrainStation.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() + 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 + + diff --git a/Mini-12306-python/app/models/passenger_lib.py b/Mini-12306-python/app/models/passenger_lib.py new file mode 100644 index 0000000..a622afb --- /dev/null +++ b/Mini-12306-python/app/models/passenger_lib.py @@ -0,0 +1,76 @@ +from sqlalchemy import func + +from app.models import db +from werkzeug.security import generate_password_hash, check_password_hash + + +def isAvailable(account): + #从数据库中查找第一个匹配 account 的 Passenger 记录。 + Passenger.query.filter_by(account=account).first() + + +#乘客实体类 +class Passenger(db.Model): + __tablename__ = 'passenger' + + #主键 + id = db.Column(db.Integer, primary_key=True) + #姓名 + name = db.Column(db.String(120)) + #账号 + account = db.Column(db.String(120), unique=True, nullable=False, index=True) + #密码 + password_digest = db.Column(db.String(2000), nullable=False) + #身份证号码 + id_card_no = db.Column(db.String(120)) + #手机号 + mobile_no = db.Column(db.String(120)) + #银行卡号 + bank_card_no = db.Column(db.String(120)) + #账号状态 + 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): + return check_password_hash(self.password_digest, password) # 验证密码 + + #验证账号和密码 + @classmethod + def verifyPassenger(cls, account, password): + passenger = cls.query.filter_by(account=account).first() + if passenger and passenger.check_password(password): + print(1) + return passenger + return None + + #注册成功,存储乘客信息,即添加操作 + @classmethod + def create(cls, data): + 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 + + #根据乘客id删除数据,即删除操作 + @classmethod + def destroy(cls, passenger_id): + passenger = cls.query.get(passenger_id) + if passenger: + db.session.delete(passenger) + db.session.commit() + return True + return False diff --git a/Mini-12306-python/app/models/station_lib.py b/Mini-12306-python/app/models/station_lib.py new file mode 100644 index 0000000..3ac45a1 --- /dev/null +++ b/Mini-12306-python/app/models/station_lib.py @@ -0,0 +1,45 @@ +import pdb +from datetime import datetime +from pypinyin import pinyin, Style +from sqlalchemy import func + +from app.models import db + + +class Station(db.Model): + __tablename__: str = 'station' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), unique=True, nullable=False, index=True) + pinyin = db.Column(db.String(120)) + province = db.Column(db.String(120)) + city = db.Column(db.String(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 f'' + + @classmethod + def create(cls, data): + 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'), + created_at=datetime.now(), + updated_at=datetime.now(), + ) + db.session.add(station) + db.session.commit() + return station + + @classmethod + def destroy(cls, station_id): + station = cls.query.get(station_id) + if station: + db.session.delete(station) + db.session.commit() + return True + return False diff --git a/Mini-12306-python/app/models/ticket_lib.py b/Mini-12306-python/app/models/ticket_lib.py new file mode 100644 index 0000000..01226fd --- /dev/null +++ b/Mini-12306-python/app/models/ticket_lib.py @@ -0,0 +1,45 @@ +from sqlalchemy import func + +from app.models import db + + +class Ticket(db.Model): + __tablename__: str = 'ticket' + + id = db.Column(db.Integer, primary_key=True) + seat_no = db.Column(db.String(120)) + seat_class = db.Column(db.String(120)) + price = db.Column(db.Numeric(8, 2)) + state = db.Column(db.Integer, default=0) + train_no = db.Column(db.String(120), db.ForeignKey('train.train_no'), nullable=False, index=True) + passenger_id = db.Column(db.Integer, db.ForeignKey('passenger.id'), nullable=False, index=True) + order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False, index=True) + from_station = db.Column(db.String(120)) + to_station = db.Column(db.String(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 = db.relationship('Order', backref=db.backref('tickets')) + passenger = db.relationship('Passenger', backref=db.backref('tickets')) + + def __repr__(self): + return f'' + + def updateState(self): + self.state = 1 + return self + + @classmethod + def generateTicket(cls, item, passenger_id): + # Create a new Ticket object + 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 diff --git a/Mini-12306-python/app/models/train_lib.py b/Mini-12306-python/app/models/train_lib.py new file mode 100644 index 0000000..6fb0dfd --- /dev/null +++ b/Mini-12306-python/app/models/train_lib.py @@ -0,0 +1,131 @@ +from datetime import datetime + +import pytz +from sqlalchemy import func + +from app.models import db +from app.models.train_station_lib import TrainStation + +#Train实体 +class Train(db.Model): + __tablename__: str = 'train' + + id = db.Column(db.Integer, primary_key=True) + train_no = db.Column(db.String(120), unique=True, nullable=False, index=True) + departure_station = db.Column(db.String(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 f'' + + @classmethod + def create(cls, new_train): + db.session.add(new_train) + db.session.commit() + return new_train + + """ + 根据出发站、到达站和日期查询符合条件的列车信息。 + + 该方法首先根据出发站和到达站名称分别查询经过这两个站点的列车, + 然后找出同时经过这两个站点的列车编号(train_no), + 再通过判断站点顺序(出发站在到达站之前),筛选出有效的列车, + 最后根据有效时间和列车编号查询最终结果。 + + 参数: + - from_station: 出发站名称(字符串) + - to_station: 到达站名称(字符串) + - date: 查询日期(datetime 对象) + + 返回: + - 符合条件的列车列表 + """ + # # 根据列车编号和有效时间筛选符合条件的列车 + # trains = Train.query.filter( + # #Train.effective_time >= date, # 只查询在指定日期之后有效的列车 + # Train.train_no.in_(valid_train_nos) # 使用有效列车编号进行过滤 + # ).all() + + @classmethod + # 业务层(服务层) + def queryTrains(cls, from_station, to_station, date): + # 查询所有经过出发站的列车信息 + from_train = TrainStation.query.filter_by(station_name=from_station).all() + # 查询所有经过到达站的列车信息 + to_train = TrainStation.query.filter_by(station_name=to_station).all() + # 提取出发站对应的所有列车编号(train_no) + from_train_nos = {ts.train_no for ts in from_train} + # 提取到达站对应的所有列车编号(train_no) + 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) + ] + # 获取当前系统时间 + now = datetime.now(pytz.timezone('Asia/Shanghai')) + #判断date是否大于当前系统时间 + if date > now: + # 根据列车编号和有效时间筛选符合条件的列车 + trains = Train.query.filter( + Train.effective_time < date, + Train.expiration_time > date, + Train.train_no.in_(valid_train_nos) # 使用有效列车编号进行过滤 + ).all() + else: + # 根据列车编号和有效时间筛选符合条件的列车 + trains = db.session.query(Train, TrainStation).join( + TrainStation, + Train.train_no == TrainStation.train_no and TrainStation.station_name == from_station + ).filter( + Train.effective_time < date, + Train.expiration_time > date, + # 不大于当前的系统时间还需判断当前的时间是否大于列车的出发时间 + func.time(TrainStation.departure_time) > func.time(date), + Train.train_no.in_(valid_train_nos) # 使用有效列车编号进行过滤 + ).all() + # 返回查询到的列车列表 + return trains + + +def buildTrain(params): + # Create a new Train object + train = Train( + train_no=params['trainNo'], + effective_time=params['effective_time'], + expiration_time=params['expiration_time'] + ) + + # Extract indexes for determining first and last station + indexes = [e["index"] for e in params['stations']] + + for e in params['stations']: + # Create and associate TrainStation objects + 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) + # Determine the departure time and station for the first station + if e["index"] == 0: + train.departure_time = e["depTime"] + train.departure_station = e["name"] + + # Determine the arrival time and station for the last station + if e["index"] == max(indexes): + train.arrival_time = e["arrTime"] + train.arrival_station = e["name"] + + return train diff --git a/Mini-12306-python/app/models/train_station_lib.py b/Mini-12306-python/app/models/train_station_lib.py new file mode 100644 index 0000000..aeb7fcf --- /dev/null +++ b/Mini-12306-python/app/models/train_station_lib.py @@ -0,0 +1,21 @@ +from sqlalchemy import func + +from app.models import db + + +class TrainStation(db.Model): + __tablename__: str = 'train_station' + + train_no = db.Column(db.String(120), db.ForeignKey('train.train_no'), primary_key=True, index=True) + station_name = db.Column(db.String(120), db.ForeignKey('station.name'), primary_key=True) + + price = db.Column(db.Numeric(8, 2)) + arrival_time = db.Column(db.DateTime) + departure_time = db.Column(db.DateTime) + index = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=func.now()) + updated_at = db.Column(db.DateTime, default=func.now()) + + station = db.relationship('Station', backref=db.backref('train_stations')) + train = db.relationship('Train', backref=db.backref('train_stations')) + diff --git a/Mini-12306-python/app/order_manager.py b/Mini-12306-python/app/order_manager.py new file mode 100644 index 0000000..45008a7 --- /dev/null +++ b/Mini-12306-python/app/order_manager.py @@ -0,0 +1,59 @@ +from email.utils import parsedate_to_datetime + +from flask import Blueprint, request, jsonify +from flask_jwt_extended import get_jwt_identity, jwt_required +from app import db, LogService +from app.models.order_lib import generateOrder, Order +from presenter.order import OrderPresenter +from utils import create_response, StateCode +from utils.server import verifyOrderPayment + +order_bp = Blueprint('order', __name__) + + +@order_bp.route('/orders', methods=['POST']) +@jwt_required() +def createOrder(): + current_user = get_jwt_identity() + 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() + return jsonify(create_response(StateCode.SUCCESS, data=order_presenter)), 200 + + +@order_bp.route('/orders//query_payment', methods=['POST']) +@jwt_required() +def queryPayment(order_no): + current_user = get_jwt_identity() + response = verifyOrderPayment(order_no) + + if response.status_code == 200: + data = response.json().get("data") + pay_time_str = data["pay_time"] # 如:Thu, 05 Jun 2025 20:56:56 GMT + + # 解析为 datetime 对象 + pay_time = parsedate_to_datetime(pay_time_str) + + # 更新订单状态和支付时间 + order = Order.setOrderState(order_no, current_user, state=1, payment_time=pay_time) + order_presenter = OrderPresenter(order).as_dict() + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=order_presenter)), 200 + + else: + LogService.log() + return jsonify(create_response(StateCode.ORDER_PAY_ERROR)), 400 + + +@order_bp.route('/orders', methods=['GET']) +@jwt_required() +def queryOrder(): + 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() + return jsonify(create_response(StateCode.SUCCESS, data=order_presenter)), 200 diff --git a/Mini-12306-python/app/passenger_manager.py b/Mini-12306-python/app/passenger_manager.py new file mode 100644 index 0000000..ab6446e --- /dev/null +++ b/Mini-12306-python/app/passenger_manager.py @@ -0,0 +1,73 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +from app import redis_client, LogService +from app.models.passenger_lib import Passenger, isAvailable +from presenter import PassengerPresenter +from utils import StateCode, create_response, checkMobile, checkIdCard, checkBankCard + +register_bp = Blueprint('register', __name__) + +#注册模块 +@register_bp.route('/register', methods=['POST']) +def register(): + #获取到前端用户注册的数据 + data = request.json + print(data) + 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(): + 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,Redis是一种开源的、基于内存的键值存储,通常用于缓存、分布式存储 和 高效数据处理, + #所以通过get方法获取到对应的值即验证码 + stored_code = redis_client.get(mobile_no) + return stored_code == code diff --git a/Mini-12306-python/app/query_manager.py b/Mini-12306-python/app/query_manager.py new file mode 100644 index 0000000..864fbc7 --- /dev/null +++ b/Mini-12306-python/app/query_manager.py @@ -0,0 +1,53 @@ +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_bp = Blueprint('query', __name__) + + +#控制层,接收前端的请求,调用服务层进行查询车次信息,封装结构并返回 +@query_bp.route('/trains/query_train', methods=['GET']) +def queryTrains(): # 查询车次方法 + #获取起始站名 + 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(): # 查询车票方法 + current_user = get_jwt_identity() # 解码JWT得到用户ID + 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(): # 查询站点方法 + 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 diff --git a/Mini-12306-python/app/station_manager.py b/Mini-12306-python/app/station_manager.py new file mode 100644 index 0000000..d2c0836 --- /dev/null +++ b/Mini-12306-python/app/station_manager.py @@ -0,0 +1,26 @@ +from flask import request, jsonify, Blueprint +from app import LogService +from app.models import Station +from presenter import StationPresenter +from utils import create_response, StateCode + +station_bp = Blueprint('stations', __name__) + + +@station_bp.route('/stations', methods=['POST']) +def createStation(): # 创建站点方法 + data = request.json + new_station = Station.create(data=data) + station_presenter = StationPresenter(new_station).as_dict() + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=station_presenter)), 200 + + +@station_bp.route('/stations/quantity_create', methods=['POST']) +def quantityCreate(): # 批量创建站点方法 + stations = request.json.get("stations") + for name in stations: + station_hash = {"name": name} + Station.create(station_hash) + LogService.log() + return jsonify(create_response(StateCode.SUCCESS)), 200 diff --git a/Mini-12306-python/app/train_manager.py b/Mini-12306-python/app/train_manager.py new file mode 100644 index 0000000..cb98ac5 --- /dev/null +++ b/Mini-12306-python/app/train_manager.py @@ -0,0 +1,27 @@ +from flask import request, jsonify, Blueprint +from app import db, LogService +from app.models import Train +from app.models.train_lib import buildTrain +from presenter.train import TrainPresenter +from utils import create_response, StateCode + +trains_bp = Blueprint('trains', __name__) + + +@trains_bp.route('/trains', methods=['GET']) +def queryAllTrains(): + trains = Train.query.all() + trains_presenters = [TrainPresenter(train).as_dict() for train in trains] + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=trains_presenters)), 200 + + +@trains_bp.route('/trains', methods=['POST']) +def createTrain(): + data = request.json + new_train = buildTrain(data) + db.session.add(new_train) + db.session.commit() + train_presenter = TrainPresenter(new_train).as_dict() + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=train_presenter)), 200 diff --git a/Mini-12306-python/config.py b/Mini-12306-python/config.py new file mode 100644 index 0000000..6b1d5df --- /dev/null +++ b/Mini-12306-python/config.py @@ -0,0 +1,16 @@ +import os + + +class Config: + # SECRET_KEY = os.environ.get('SECRET_KEY') or 'your_secret_key' + # SQLALCHEMY_TRACK_MODIFICATIONS = False + + SECRET_KEY = '8665e31a29ab7e12cb0f92c1dbobj1e3a6a15230fb17' + + # 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 'mysql+pymysql://root:1625344721Qq%40@localhost/mini12306' + SQLALCHEMY_TRACK_MODIFICATIONS = False + + REDIS_URL = "redis://:123456@localhost:6379/0" diff --git a/Mini-12306-python/docker-compose.yml b/Mini-12306-python/docker-compose.yml new file mode 100644 index 0000000..800d4a7 --- /dev/null +++ b/Mini-12306-python/docker-compose.yml @@ -0,0 +1,87 @@ +# 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: + +version: '3.8' +services: + python: + build: + context: ./ + dockerfile: ./Dockerfile + restart: always + environment: + - DATABASE_URL=mysql+pymysql://root:123456@db:3306/mini12306_python # 修改数据库连接字符串 + - REDIS_URL=redis + - SERVER_LIB_URL=http://py12306.learnerhub.net + ports: + - "3002:3002" + depends_on: + - db # 服务名保持不变,但已指向新的 MySQL 服务 + - redis + volumes: + - /var/log/mini12306_python:/app/logs + + redis: + image: redis:alpine + container_name: 12306_redis + volumes: + - redis_data:/data + + db: # 修改后的 MySQL 服务 + image: mysql:8.0 # 使用官方 MySQL 镜像 + container_name: 12306_mysql # 修改容器名称 + restart: always + environment: + MYSQL_ROOT_PASSWORD: 123456 # MySQL 根密码(建议修改) + MYSQL_DATABASE: mini12306_python # 自动创建的数据库 + MYSQL_USER: app_user # 可选:创建专用用户 + MYSQL_PASSWORD: user123 # 可选用户密码 + volumes: + - mysql_data:/var/lib/mysql # MySQL 数据存储路径 + # 建议添加的健康检查 + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + mysql_data: # 修改卷名称 + redis_data: diff --git a/Mini-12306-python/logs/.init b/Mini-12306-python/logs/.init new file mode 100644 index 0000000..3df65ca --- /dev/null +++ b/Mini-12306-python/logs/.init @@ -0,0 +1 @@ +init folder \ No newline at end of file diff --git a/Mini-12306-python/migrations/README b/Mini-12306-python/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/Mini-12306-python/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/Mini-12306-python/migrations/alembic.ini b/Mini-12306-python/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/Mini-12306-python/migrations/alembic.ini @@ -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 diff --git a/Mini-12306-python/migrations/env.py b/Mini-12306-python/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/Mini-12306-python/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: 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.') + + 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() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/Mini-12306-python/migrations/script.py.mako b/Mini-12306-python/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/Mini-12306-python/migrations/script.py.mako @@ -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"} diff --git a/Mini-12306-python/migrations/versions/1b3bc6809b30_init_databases.py b/Mini-12306-python/migrations/versions/1b3bc6809b30_init_databases.py new file mode 100644 index 0000000..1fbd516 --- /dev/null +++ b/Mini-12306-python/migrations/versions/1b3bc6809b30_init_databases.py @@ -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 ### diff --git a/Mini-12306-python/migrations/versions/67902ab783bc_initial_migration.py b/Mini-12306-python/migrations/versions/67902ab783bc_initial_migration.py new file mode 100644 index 0000000..874598f --- /dev/null +++ b/Mini-12306-python/migrations/versions/67902ab783bc_initial_migration.py @@ -0,0 +1,50 @@ +"""Initial migration + +Revision ID: 67902ab783bc +Revises: b1465436c340 +Create Date: 2025-03-03 21:19:33.592231 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '67902ab783bc' +down_revision = 'b1465436c340' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tb_emp', schema=None) as batch_op: + batch_op.drop_index('username') + + op.drop_table('tb_emp') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tb_emp', + sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False, comment='主键'), + sa.Column('username', mysql.VARCHAR(length=20), nullable=False, comment='用户名'), + sa.Column('password', mysql.VARCHAR(length=32), server_default=sa.text("'123456'"), nullable=True, comment='密码'), + sa.Column('emp_name', mysql.VARCHAR(length=10), nullable=False, comment='员工姓名'), + sa.Column('gender', mysql.TINYINT(unsigned=True), autoincrement=False, nullable=False, comment='性别(1 男生 2 女生)'), + sa.Column('image', mysql.VARCHAR(length=300), nullable=True, comment='图片的访问路径'), + sa.Column('job', mysql.TINYINT(unsigned=True), autoincrement=False, nullable=True, comment='职位(1 班主任 2 讲师 3 学工主管 4 教研主管)'), + sa.Column('entrydate', mysql.DATETIME(), nullable=True, comment='入职日期'), + sa.Column('dept', mysql.VARCHAR(length=20), nullable=True, comment='归属部门'), + sa.Column('create_time', mysql.DATETIME(), nullable=False, comment='创建时间'), + sa.Column('update_time', mysql.DATETIME(), nullable=False, comment='更新时间'), + sa.PrimaryKeyConstraint('id'), + mysql_collate='utf8mb4_0900_ai_ci', + mysql_default_charset='utf8mb4', + mysql_engine='InnoDB' + ) + with op.batch_alter_table('tb_emp', schema=None) as batch_op: + batch_op.create_index('username', ['username'], unique=True) + + # ### end Alembic commands ### diff --git a/Mini-12306-python/migrations/versions/7866a6e8a75b_remove_trainstation_id.py b/Mini-12306-python/migrations/versions/7866a6e8a75b_remove_trainstation_id.py new file mode 100644 index 0000000..0bec048 --- /dev/null +++ b/Mini-12306-python/migrations/versions/7866a6e8a75b_remove_trainstation_id.py @@ -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 ### diff --git a/Mini-12306-python/migrations/versions/b1465436c340_.py b/Mini-12306-python/migrations/versions/b1465436c340_.py new file mode 100644 index 0000000..14ec123 --- /dev/null +++ b/Mini-12306-python/migrations/versions/b1465436c340_.py @@ -0,0 +1,48 @@ +"""empty message + +Revision ID: b1465436c340 +Revises: 7866a6e8a75b +Create Date: 2025-02-21 16:47:25.383937 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'b1465436c340' +down_revision = '7866a6e8a75b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('log') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('log', + sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False), + sa.Column('log_user_agent', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_method', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_quest_path', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_ip', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_params', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_controller', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_controller_action', mysql.VARCHAR(length=120), nullable=True), + sa.Column('log_controller_id', mysql.VARCHAR(length=120), nullable=True), + sa.Column('account_id', mysql.VARCHAR(length=120), nullable=True), + sa.Column('event_name', mysql.VARCHAR(length=120), nullable=True), + sa.Column('event_content', mysql.TEXT(), nullable=True), + sa.Column('operator', mysql.VARCHAR(length=120), nullable=True), + sa.Column('created_at', mysql.DATETIME(), nullable=True), + sa.Column('updated_at', mysql.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_collate='utf8mb4_0900_ai_ci', + mysql_default_charset='utf8mb4', + mysql_engine='InnoDB' + ) + # ### end Alembic commands ### diff --git a/Mini-12306-python/myapp.py b/Mini-12306-python/myapp.py new file mode 100644 index 0000000..36a7686 --- /dev/null +++ b/Mini-12306-python/myapp.py @@ -0,0 +1,12 @@ +# myapp.py +from app import create_app, LogService +from utils import response + + +app = create_app() + + +#项目入口 +if __name__ == "__main__": + #服务端运行在哪个ip地址上的哪个端口,是否开启调试,这里是本机上的5000端口 + app.run(host="127.0.0.1", port=5000, debug=True) diff --git a/Mini-12306-python/presenter/__init__.py b/Mini-12306-python/presenter/__init__.py new file mode 100644 index 0000000..611fabd --- /dev/null +++ b/Mini-12306-python/presenter/__init__.py @@ -0,0 +1,3 @@ +from .passenger import PassengerPresenter +from .station import StationPresenter +from .mobile_code import MobileCodePresenter diff --git a/Mini-12306-python/presenter/mobile_code.py b/Mini-12306-python/presenter/mobile_code.py new file mode 100644 index 0000000..85dad8a --- /dev/null +++ b/Mini-12306-python/presenter/mobile_code.py @@ -0,0 +1,8 @@ +class MobileCodePresenter: + def __init__(self, data): + self.data = data + + def as_dict(self): + return { + "code": self.data + } diff --git a/Mini-12306-python/presenter/order.py b/Mini-12306-python/presenter/order.py new file mode 100644 index 0000000..1ee4f17 --- /dev/null +++ b/Mini-12306-python/presenter/order.py @@ -0,0 +1,16 @@ +from presenter.ticket import TicketPresenter + + +class OrderPresenter: + def __init__(self, data): + self.data = data + + def as_dict(self): + return { + "id": self.data.id, + "orderNo": self.data.order_no, + "price": self.data.price, + "state": self.data.state, + "paymentTime": self.data.payment_time, + "tickets": [TicketPresenter(ticket).as_dict() for ticket in self.data.tickets] + } diff --git a/Mini-12306-python/presenter/passenger.py b/Mini-12306-python/presenter/passenger.py new file mode 100644 index 0000000..010e88d --- /dev/null +++ b/Mini-12306-python/presenter/passenger.py @@ -0,0 +1,17 @@ + +class PassengerPresenter: + def __init__(self, data, aid_data=None): + self.data = data + self.aid_data = aid_data + + def as_dict(self): + 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 diff --git a/Mini-12306-python/presenter/station.py b/Mini-12306-python/presenter/station.py new file mode 100644 index 0000000..1a7eb10 --- /dev/null +++ b/Mini-12306-python/presenter/station.py @@ -0,0 +1,10 @@ +class StationPresenter: + def __init__(self, data): + self.data = data + + def as_dict(self): + return { + "id": self.data.id, + "name": self.data.name, + "pinyin": self.data.pinyin, + } diff --git a/Mini-12306-python/presenter/ticket.py b/Mini-12306-python/presenter/ticket.py new file mode 100644 index 0000000..f28dbc7 --- /dev/null +++ b/Mini-12306-python/presenter/ticket.py @@ -0,0 +1,19 @@ +class TicketPresenter: + def __init__(self, data): + self.data = data + + def as_dict(self): + 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, + } diff --git a/Mini-12306-python/presenter/train.py b/Mini-12306-python/presenter/train.py new file mode 100644 index 0000000..3bdff2b --- /dev/null +++ b/Mini-12306-python/presenter/train.py @@ -0,0 +1,17 @@ +from presenter.train_station import TrainStationPresenter + + +class TrainPresenter: + def __init__(self, data): + self.data = data + + def as_dict(self): + 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, + "stations": [TrainStationPresenter(station).as_dict() for station in self.data.train_stations] + } diff --git a/Mini-12306-python/presenter/train_station.py b/Mini-12306-python/presenter/train_station.py new file mode 100644 index 0000000..3be94a3 --- /dev/null +++ b/Mini-12306-python/presenter/train_station.py @@ -0,0 +1,13 @@ +class TrainStationPresenter: + def __init__(self, data): + self.data = data + + def as_dict(self): + 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, + } \ No newline at end of file diff --git a/Mini-12306-python/utils/__init__.py b/Mini-12306-python/utils/__init__.py new file mode 100644 index 0000000..a484465 --- /dev/null +++ b/Mini-12306-python/utils/__init__.py @@ -0,0 +1,2 @@ +from .response import create_response, StateCode +from .server import checkMobile, checkIdCard, checkBankCard diff --git a/Mini-12306-python/utils/response.py b/Mini-12306-python/utils/response.py new file mode 100644 index 0000000..ca657ba --- /dev/null +++ b/Mini-12306-python/utils/response.py @@ -0,0 +1,53 @@ +from enum import Enum +from typing import Any, Dict, List, Union + + +#状态码,响应前端发送的某次请求,观察此次请求的结果,成功与否 +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: "支付失败" +} + +#返回一个json格式的数据,主要是为了和前端统一数据格式,包含三个部分,上面的状态码,响应信息,返回的数据 +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, + "msg": message or STATE_MESSAGES.get(code, "Unknown error"), + "data": data + } diff --git a/Mini-12306-python/utils/server.py b/Mini-12306-python/utils/server.py new file mode 100644 index 0000000..3af8022 --- /dev/null +++ b/Mini-12306-python/utils/server.py @@ -0,0 +1,56 @@ +import os +import requests + +hosts = os.getenv('SERVER_LIB_URL', "http://127.0.0.1:5000") + + +#检验身份证号码 +def checkIdCard(id_card): + # 获取到url地址 + url = hosts + "/id_card/verify" + + # 请求参数 + params = { + 'idCardNo': id_card + } + # response = requests.get(url, params=params) 发送get请求 + + response = requests.post(url, json=params) + return response.status_code == 200 and response.json().get("data")["result"] + +#检验银行卡号 +def checkBankCard(bank_card): + # 获取到url地址 + url = hosts + "/bank/card_verify" + # 请求参数 + params = { + 'bankCard': bank_card + } + response = requests.post(url, json=params) + return response.status_code == 200 and response.json().get("data")["result"] + + +#检验手机号 +def checkMobile(mobile_no): + #获取到url地址 + url = hosts + "/mobile/check" + + #请求参数 + params = { + 'mobileNo': mobile_no + } + + #获取到mobile_server类中返回的数据 + response = requests.post(url, json=params) + + #判断返回数据的状态码和result(布尔值,手机号是否匹配成功),只有状态码是200和手机号匹配成功才返回true + return response.status_code == 200 and response.json().get("data")["result"] + + +def verifyOrderPayment(order_no): + url = hosts + "/bank/query" + + # 模拟向支付中心发送验证请求进行验证 + params = {"OrderNo": order_no} + response = requests.post(url, json=params) + return response