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..5081ae3 --- /dev/null +++ b/Mini-12306-python/app/__init__.py @@ -0,0 +1,53 @@ +# 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) + + +def create_app(): + app = Flask(__name__, static_folder='mini12306') + CORS(app) + app.app_context().push() + app.config.from_object(Config) + # 设置jwt token有效时间 + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(days=30) + db.init_app(app) + Migrate(app, db) + JWTManager(app) + LogService.configure_logging() + + # routes + 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 diff --git a/Mini-12306-python/app/bank_server.py b/Mini-12306-python/app/bank_server.py new file mode 100644 index 0000000..52d3e74 --- /dev/null +++ b/Mini-12306-python/app/bank_server.py @@ -0,0 +1,37 @@ +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..8dc0a90 --- /dev/null +++ b/Mini-12306-python/app/id_card_server.py @@ -0,0 +1,18 @@ +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(): + pattern = r'^\d{18}$' + id_number = request.json.get('idCardNo') + 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..ef2fb06 --- /dev/null +++ b/Mini-12306-python/app/login_manager.py @@ -0,0 +1,25 @@ +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=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..add37f3 --- /dev/null +++ b/Mini-12306-python/app/mobile_server.py @@ -0,0 +1,49 @@ +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(): + pattern = r'^1[3-9]\d{9}$' + number = request.json.get('mobileNo') + state = { + "result": bool(re.match(pattern, number)) + } + LogService.log() + return jsonify(create_response(StateCode.SUCCESS, data=state)), 200 + + +def getVerificationCode(mobile_no): + verification_code = generateRandomNumber() # 生成验证码 + 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..f1e4906 --- /dev/null +++ b/Mini-12306-python/app/models/order_lib.py @@ -0,0 +1,103 @@ +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 + + +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.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() + 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..d203fc1 --- /dev/null +++ b/Mini-12306-python/app/models/passenger_lib.py @@ -0,0 +1,58 @@ +from sqlalchemy import func + +from app.models import db +from werkzeug.security import generate_password_hash, check_password_hash + + +def isAvailable(account): + 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): + 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 + + @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..7095864 --- /dev/null +++ b/Mini-12306-python/app/models/train_lib.py @@ -0,0 +1,91 @@ +from sqlalchemy import func + +from app.models import db +from app.models.train_station_lib import TrainStation + +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 + + @classmethod + def queryTrains(cls, from_station, to_station, date): + # Query for train stations where the station name matches `from_station` + from_train = TrainStation.query.filter_by(station_name=from_station).all() + + # Query for train stations where the station name matches `to_station` + to_train = TrainStation.query.filter_by(station_name=to_station).all() + + # Extract train_no from both query results + from_train_nos = {ts.train_no for ts in from_train} + to_train_nos = {ts.train_no for ts in to_train} + + # Find the common train_no between the two stations + common_train_nos = from_train_nos & to_train_nos + + # Filter train numbers where the index of the from station is less than the index of the to station + 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) + ] + # Query trains by the filtered train numbers and the given date (assuming date filtering) + trains = Train.query.filter( + Train.effective_time >= date, + Train.train_no.in_(valid_train_nos) + ).all() + + # Assuming you have a presenter or serializer for the trains + 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..6f942b3 --- /dev/null +++ b/Mini-12306-python/app/order_manager.py @@ -0,0 +1,49 @@ +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 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() + 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..d9286e6 --- /dev/null +++ b/Mini-12306-python/app/passenger_manager.py @@ -0,0 +1,62 @@ +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 + 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): + 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..6e986a8 --- /dev/null +++ b/Mini-12306-python/app/query_manager.py @@ -0,0 +1,46 @@ +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 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') + 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..1927e2c --- /dev/null +++ b/Mini-12306-python/config.py @@ -0,0 +1,15 @@ +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 ('postgresql://postgres:123456@localhost:54321' + '/mini12306_python') + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/Mini-12306-python/docker-compose.yml b/Mini-12306-python/docker-compose.yml new file mode 100644 index 0000000..793defb --- /dev/null +++ b/Mini-12306-python/docker-compose.yml @@ -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: \ No newline at end of file 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/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/myapp.py b/Mini-12306-python/myapp.py new file mode 100644 index 0000000..c112310 --- /dev/null +++ b/Mini-12306-python/myapp.py @@ -0,0 +1,9 @@ +# myapp.py +from app import create_app, LogService +from utils import response + +app = create_app() + + +if __name__ == "__main__": + app.run(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..12fa394 --- /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, + } diff --git a/Mini-12306-python/readme.md b/Mini-12306-python/readme.md new file mode 100644 index 0000000..ee3a70b --- /dev/null +++ b/Mini-12306-python/readme.md @@ -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() +``` \ No newline at end of file diff --git a/Mini-12306-python/requirements.txt b/Mini-12306-python/requirements.txt new file mode 100644 index 0000000..0d09871 --- /dev/null +++ b/Mini-12306-python/requirements.txt @@ -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 \ 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..ff2b5df --- /dev/null +++ b/Mini-12306-python/utils/response.py @@ -0,0 +1,52 @@ +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: "支付失败" +} + + +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..a013fa0 --- /dev/null +++ b/Mini-12306-python/utils/server.py @@ -0,0 +1,45 @@ +import os +import requests + +hosts = os.getenv('SERVER_LIB_URL', "http://127.0.0.1:5000") + + +def checkIdCard(id_card): + 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 = 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 = hosts + "/mobile/check" + + params = { + 'mobileNo': mobile_no + } + + response = requests.post(url, json=params) + 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