Compare commits

..

2 Commits
main ... master

3
.gitignore vendored

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1 @@
init folder

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

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

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

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

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

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

@ -1,32 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
/unpackage/*

@ -1,16 +0,0 @@
{
// launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [
{
"app-plus": {
"launchtype": "local"
},
"default": {
"launchtype": "local"
},
"type": "uniCloud"
}
]
}

@ -1,198 +0,0 @@
<script>
import apiAuth from "@/api/auth.js";
import apiStations from "@/api/stations";
export default {
//
onLaunch: function () {
console.log("App Launch");
// token
const authToken = uni.getStorageSync("auth-token");
if (authToken) {
apiAuth
.detail({ authToken })
.then((res) => {
this.$store.dispatch("updateAuth", { ...res.data, token: authToken });
})
.catch(() => {
uni.removeStorageSync("auth-token");
});
}
//
apiStations
.get()
.then((res) => this.$store.dispatch("updateStations", res.data))
.catch(() => {});
},
onShow: function () {
console.log("App Show");
},
onHide: function () {
console.log("App Hide");
},
};
</script>
<style lang="scss">
@import "uview-ui/index.scss";
@import "uview-ui/libs/css/flex.scss";
.u-flex-shrink-0 {
flex-shrink: 0;
}
/* 间距 $spacers 在 uni.scss 中 */
@each $key, $val in $spacers {
.mt-#{$key},
.my-#{$key},
.m-#{$key} {
margin-top: $val;
}
.mb-#{$key},
.my-#{$key},
.m-#{$key} {
margin-bottom: $val;
}
.ms-#{$key},
.mx-#{$key},
.m-#{$key} {
margin-left: $val;
}
.me-#{$key},
.mx-#{$key},
.m-#{$key} {
margin-right: $val;
}
.pt-#{$key},
.py-#{$key},
.p-#{$key} {
padding-top: $val;
}
.pb-#{$key},
.py-#{$key},
.p-#{$key} {
padding-bottom: $val;
}
.ps-#{$key},
.px-#{$key},
.p-#{$key} {
padding-left: $val;
}
.pe-#{$key},
.px-#{$key},
.p-#{$key} {
padding-right: $val;
}
.rounded-#{$key} {
border-radius: $val;
}
}
:root {
@each $key, $val in $spacers {
--space-#{$key}: #{$val};
--rounded-#{$key}: #{$val};
}
}
/* 颜色 $colors 在 uni.scss 中 */
@each $key, $val in $colors {
.text-#{$key} {
color: $val;
}
}
:root {
@each $key, $val in $colors {
--color-#{$key}: #{$val};
}
}
/* 每个页面公共css */
page {
background-color: $u-bg-color; // uview-ui/theme.scss
}
.p-page {
@extend .p-base;
}
.page-box-x-overflow {
margin-left: calc(var(--space-base) * -1);
margin-right: calc(var(--space-base) * -1);
}
.page-box-t-overflow {
margin-top: calc(var(--space-base) * -1);
}
.page-box {
@extend .bg-white, .rounded-sm;
}
.page-box + .page-box {
@extend .mt-base;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.overflow-hidden {
overflow: hidden;
}
.bg-white {
background-color: #ffffff;
}
.fw-bold {
font-weight: bold;
}
.text-center {
text-align: center;
}
.text-end {
text-align: end;
}
.w-100 {
width: 100%;
}
.TrainItem {
.OrderInfo {
font-size: 14px;
color: var(--color-info);
}
.Time {
font-size: 18px;
font-weight: bold;
}
.Station {
font-size: 14px;
color: var(--color-main);
}
.OptionItem {
display: flex;
flex-direction: column;
align-items: center;
padding-top: var(--space-base);
padding-bottom: var(--space-base);
color: var(--color-primary);
}
&.lg {
.Time {
font-size: 22px;
}
.Station {
font-size: 15px;
}
}
}
</style>

@ -1,45 +0,0 @@
# 新手常见问题
列举一些不好检索的,新手容易碰到的问题。
## 为什么我的 HBuilderx 的运行/打包按钮不可点?
HBuilderx 特性,需要在编辑器中打开任意项目下的文件才能运行/打包当前项目。
## 为什么我的项目运行不起来?
1. AppId没有获取
2. 环境没装好
3. 代码有问题
4. 依赖没装好
5. 依赖有问题
### AppID 有没有获取?
组长操作
[README.MD](./README.md#重要!写在开始!)
### 检查开发环境
安装[node](https://nodejs.cn/en/download/prebuilt-installer)
### 检查是不是代码运行之前不小心在键盘上敲了一下,导致代码错误?
拿到代码后第一时间上传git仓库这样每一次代码变更都有迹可循。
> 重新获取代码也可以
### 有没有安装依赖?
通过`git clone`的项目通常不会有第三方依赖文件夹,这需要你自己安装
前端项目有多种第三方依赖管理,常用的有`npm`、`yarn`,项目根目录下的`package.json`文件就是第三方依赖管理文件
运行安装命令如`npm install`后会在根目录下多出一个`node_modules`文件夹存放第三方依赖
### 确认第三方依赖安装的版本
可能你自动安装的依赖版本和开发者使用的依赖版本不一致,手动固定`package.json`中的依赖版本号后重新安装
> Mini-12306 已经固定了版本号

@ -1,75 +0,0 @@
# 重要!写在开始!
请每组开发人员统一使用的`AppID`,建议由小组组长生成后分享给小组成员
1. 组长获取代码,大概率是压缩包
2. 使用[HBuilder X](https://www.dcloud.io/hbuilderx.html)打开项目文件夹
3. 在根目录下找到并打开`manifest.json`文件,在`基础配置`中`重新获取AppID`
4. 上传代码到小组代码仓库(为什么?参考[新手常见问题](./QUESTIONS.md#检查是不是代码运行之前不小心在键盘上敲了一下,导致代码错误?)
5. 通知小组成员从代码仓库中重新获取项目代码
[DCloud appid 用途/作用/使用说明](https://ask.dcloud.net.cn/article/35907)
> 相同 appid 的项目打包的 apk 文件在安装时会相互覆盖,不同的则会共存(你也不想你组的 app 被其他组覆盖安装吧)
>
> 请保管好你组的 appid
# Mini-12306 APP
软件工程开发案例 - 前端app
以下内容为项目基本情况了解,实际上手开发可查看[开发指引](./README_DEVELOPMENT.md)
另附[新手常见问题](./QUESTIONS.md)
> 若要运行此项目,请先确保已安装依赖`npm install`
> 如果运行命令报错,请查看[新手常见问题](./QUESTIONS.md#检查开发环境)
## 关于前端开发
- 上手非常简单,开发工具完备
- 开发成果可视,正反馈很及时
> 前端开发在所有代码开发中的上手难度都是最低的
> 美观的页面可以给人留下非常好的第一印象
- 小团队中前端往往还会包含 UI 的职责
> 兼职 UI 的前端会很容易端陷入`细节调整`的陷阱:花大量时间死扣一个样式细节,导致整体进度被忽略。
---
## 开发语言
- [HTML](https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML/Getting_started)
- [JavaScript](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/First_steps/A_first_splash)
- [CSS](https://developer.mozilla.org/zh-CN/docs/Learn/Getting_started_with_the_web/CSS_basics)
> 这是前端开发基础,掌握了基础才能看懂并使用框架来简化开发
>
> 就算你是完全的零基础,看完上面的链接基本就能上手了(网页侧边栏是完整教程,简单认识的话单看链接页面即可)
> 非常简单
## 开发框架
- [Vue2](https://v2.cn.vuejs.org/v2/guide/)
- [uni-app](https://uniapp.dcloud.net.cn/)
- [uview](https://www.uviewui.com/)
> 开发框架只需要做到`能看懂、会使用`就行
> 对于开发框架,重要的是熟练,也就是代码写的越多,越熟练
## 推荐 IDE
- [HBuilder X](https://www.dcloud.io/hbuilderx.html)
> uni-app 开发专用 IDE。
> 你也可以使用自己喜欢的编辑器,但相对的运行、打包等流程就需要自己去配置了。
## 代码规范参考
- [2021 阿里代码规范(前端篇)](https://developer.aliyun.com/article/850913)

@ -1,89 +0,0 @@
# Mini-12306 项目从零开发指引
项目使用 [uni-app](https://uniapp.dcloud.net.cn/) 前端应用框架,以类 [Vue](https://cn.vuejs.org/) 的开发方式进行 app 开发
以下是如何从零开始一个 uni-app 项目,建议按指引创建新项目先走一遍熟悉开发流程,再使用本项目中已有的代码进行开发
## 开发环境
1. 安装[node](https://nodejs.cn/en/download/prebuilt-installer)
2. 下载 uni-app 专用集成开发环境IDE[HBuilderX-高效极客技巧 (dcloud.io)](https://dcloud.io/hbuilderx.html)
3. 解压并打开`HBuilderX.exe`运行 IDE
4. 左上角`文件`→`新建`→`项目`→`uni-app`→`默认模板`
> 注意选择 vue2 版本。
> vue3 版本的 uni-app x 可以使用更高级的类型验证,但插件问题较多。
## 开发流程
### 项目结构简单说明
- `api` 前后端交互接口集中管理
- `components` 通用组件文件夹(先行了解 vue 组件概念)
- `node_modules` 第三方插件文件夹npm
- `pages` 所有能访问的页面
- `static` 静态资源(如不会修改的图标/图片)
- `store` 【进阶】Vuex跨页面数据管理
- `uni_modules` 这是 uni-app 中的 node_modulesuni-app 插件市场会导入到此文件夹)
- `utils` 常用方法、过滤器等工具
- `App.vue` 项目入口,主要是`onLauch`和全局样式
- `main.js` 项目入口,一般插件安装都会指引你在这个文件进行引入
- `manifest.json` 很重要的项目控制(打包)文件,但一般不会改到里面的东西
- `package.json` 与 node 中的同名文件对应,管理通过 npm 安装的第三方库
- `pages.json` 软件的路由配置(配置所有可以访问的页面)
- `uni.scss` 项目内置样式(注意里面只存放样式变量,不要在里面写实际样式)
### 项目运行(浏览器)
最方便开发肯定是一边改代码一边在设备中看到效果,
既然我们在 pc 上进行开发,那在浏览器中查看代码效果肯定是最方便的啦
1. HbuilderX 顶部菜单栏`运行`→`运行到浏览器`
2. 浏览器运行后打开`开发者模式F12`,切换`设备仿真`(一般是控制台左上角的第二个图标)
3. 修改项目代码,并保存,即可在浏览器中看到效果
![](README_DEVELOPMENT_files/1.jpg)
> 如果你是运行的本项目,记得使用`npm install`安装`package.json`中的依赖项
### 上手体验
参考`Hello uni-app`模板项目的源代码,在自己创建的空白项目中开发相关功能
> 对照着界面和代码一起看!(运行`Hello uni-app`项目)
1. 实现页面跳转(内置组件:导航 navigator`Hello uni-app/pages/component/navigator/navigator.vue`
2. 数据输入与提交(内置组件:表单组件)
3. 简单 js 功能(接口:界面-带数据的页面跳转、显示 loading 提示框,`Hello uni-app/pages/API/navigator/navigator.vue`
4. 与后端交互(接口:网络-发起一个请求,`Hello uni-app/pages/API/request/request.vue`
> 实际开发也是:
> → 搭建所有页面与路由配置(页面间跳转)
> → 在各页面中记录下要开发的功能
> → 实现功能逻辑(假数据)
> → 初步美化页面
> → 与后端对接(讨论修改)
> → 细节调整
## 项目打包 APP
HbuilderX 顶部菜单栏`发行:云端打包`(打包好后会在 HbuilderX 的控制台给出 apk 链接)
打包只需要勾选以下内容
- 使用公共测试证书
- 打正式包
- 快速安心打包
## 注意事项
- uview 要使用`npm`进行安装uni-app 插件市场安装的版本有问题
## 最后
以你们自己做的各种图来实现 Mini-12306
前端开发注意不要陷入页面样式细节的代码中去了,优先实现功能代码
> 建议参考`铁路12306`App 进行页面设计和功能拓展

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@ -1,59 +0,0 @@
/**
* 用户接口
*
* 因为 Mini-12306 不存在查看其他用户详情的交互
* 故不需要区分 auth user
*/
const apiAuth = {
/**
* 用户新增(注册)
*/
add: (params) => {
return uni.$u.http.post("/register", {
account: params.account, // 用户名
password: params.password, // 密码
name: params.name, // 姓名
idCardNo: params.idCardNo,
mobileNo: params.mobileNo,
mobileCode: params.mobileCode,
bankCard: params.bankCardNo,
});
},
/**
* 用户更新
*/
update: () => {},
/**
* 用户删除
*/
delete: () => {},
/**
* 用户详情
* 正常是通过用户id获取用户信息这里没有其他用户
* 故改为通过 auth-token 获取 auth 信息
*/
detail: () => {
return uni.$u.http.get("/auth");
},
/**
* 用户列表
*/
get: () => {},
/**
* 用户登录
*/
login: (params) => {
return uni.$u.http.post("/login", {
account: params.account, // 用户名
password: params.password, // 密码
});
},
};
export default apiAuth;

@ -1,35 +0,0 @@
const apiOrders = {
/**
* 订单创建
*/
add: (params) => {
return uni.$u.http.post("/orders", {
tickets: params.tickets.map((tkt) => ({
trainNo: tkt.trainNo, // 车次
from: tkt.from, // 上车
to: tkt.to, // 下车
date: uni.$u.dayjs(tkt.date).format("YYYY/MM/DD"), // 乘车日期python 后端要求这样转,按道理应该带上时区让后端处理)
seatClass: 0, // (暂无)座位类型,一等座/二等座(不同车次叫法不一样)
passengerId: tkt.passengerId, // 乘车人这种写法是不支持添加乘车人的因为你不知道他人id
})),
});
},
/**
* 订单支付
*/
payment: (params) => {
return uni.$u.http.post(`/bank/pay`, {
orderNo: params.orderNo,
});
},
/**
* 订单支付查询
*/
queryPayment: (params) => {
return uni.$u.http.post(`/orders/${params.orderNo}/query_payment`);
},
};
export default apiOrders;

@ -1,12 +0,0 @@
const apiOther = {
/**
* 发送手机验证码
*/
sendMobileCode: (params) => {
return uni.$u.http.post("/mobile/get_verify_code", {
mobileNo: params.mobileNo, // 手机号码
});
},
};
export default apiOther;

@ -1,14 +0,0 @@
/**
* 站点接口
*/
const apiStations = {
/**
* 站点列表
*/
get: (params) => {
return uni.$u.http.get("/stations");
},
};
export default apiStations;

@ -1,18 +0,0 @@
/**
* 车票接口
*/
const apiTickets = {
/**
* 车票列表
*/
get: (params) => {
return uni.$u.http.get("/tickets", {
params: {
state: 1, // 已出票(订单已支付)
},
});
},
};
export default apiTickets;

@ -1,20 +0,0 @@
/**
* 车次接口
*/
const apiTrains = {
/**
* 车次列表
*/
get: (params) => {
return uni.$u.http.get("/trains/query_train", {
params: {
from: params.from,
to: params.to,
date: params.date,
},
});
},
};
export default apiTrains;

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

@ -1,39 +0,0 @@
import Vue from "vue";
import App from "./App";
import store from "./store";
import "./uni.promisify.adaptor";
import uView from "uview-ui";
Vue.use(uView);
import {
dayjs,
timeFormat,
timeDiff,
hideIdCardNo,
hideMobileNo,
hideBankCardNo,
} from "./utils/filters";
Vue.filter("timeFormat", timeFormat); // 覆盖 uview 的设置
uni.$u.timeFormat = timeFormat;
uni.$u.date = undefined;
uni.$u.dayjs = dayjs;
Vue.filter("timeDiff", timeDiff);
Vue.filter("hideIdCardNo", hideIdCardNo);
Vue.filter("hideMobileNo", hideMobileNo);
Vue.filter("hideBankCardNo", hideBankCardNo);
Vue.config.productionTip = false;
App.mpType = "app";
const app = new Vue({
...App,
store,
});
// 引入请求封装将app参数传递到配置中
require("./utils/request.js")(app);
app.$mount();

@ -1,118 +0,0 @@
{
"name": "Mini-12306",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_MOBILE_NO_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios": {
"dSYMs": false
},
/* SDK */
"sdkConfigs": {
"ad": {}
},
"icons": {
"android": {
"hdpi": "static/app-icon/72x72.png",
"xhdpi": "static/app-icon/96x96.png",
"xxhdpi": "static/app-icon/144x144.png",
"xxxhdpi": "static/app-icon/192x192.png"
},
"ios": {
"appstore": "",
"ipad": {
"app": "",
"app@2x": "",
"notification": "",
"notification@2x": "",
"proapp@2x": "",
"settings": "",
"settings@2x": "",
"spotlight": "",
"spotlight@2x": ""
},
"iphone": {
"app@2x": "",
"app@3x": "",
"notification@2x": "",
"notification@3x": "",
"settings@2x": "",
"settings@3x": "",
"spotlight@2x": "",
"spotlight@3x": ""
}
}
},
"splashscreen": {
"androidStyle": "default",
"android": {
"hdpi": "static/splashscreen/hdpi/_20240628103445.9.png",
"mdpi": "static/splashscreen/mdpi/_20240628103445.9.png",
"xhdpi": "static/splashscreen/xhdpi/_20240628103445.9.png",
"xxhdpi": "static/splashscreen/xxhdpi/_20240628103445.9.png",
"xxxhdpi": "static/splashscreen/xxxhdpi/_20240628103445.9.png"
}
}
}
},
/* */
"quickapp": {},
/* */
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "2"
}

@ -1,26 +0,0 @@
{
"name": "Mini-12306",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dayjs": "^1.11.11",
"uview-ui": "^2.0.36"
}
},
"node_modules/dayjs": {
"version": "1.11.11",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.11.tgz",
"integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg=="
},
"node_modules/uview-ui": {
"version": "2.0.36",
"resolved": "https://registry.npmmirror.com/uview-ui/-/uview-ui-2.0.36.tgz",
"integrity": "sha512-ASSZT6M8w3GTO1eFPbsgEFV0U5UujK+8pTNr+MSUbRNcRMC1u63DDTLJVeArV91kWM0bfAexK3SK9pnTqF9TtA==",
"engines": {
"HBuilderX": "^3.1.0"
}
}
}
}

@ -1,6 +0,0 @@
{
"dependencies": {
"dayjs": "1.11.11",
"uview-ui": "2.0.36"
}
}

@ -1,101 +0,0 @@
{
"easycom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
},
"pages": [
//pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/HomeView/HomeView",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#f5f5f5",
"app-plus": {
"titleNView": {
"type": "transparent",
"titleColor": "#303133"
}
}
}
},
{
"path": "pages/QueryTrainView/QueryTrainView",
"style": {
"navigationBarTitleText": "查询车次"
}
},
{
"path": "pages/BuyTicketView/BuyTicketView",
"style": {
"navigationBarTitleText": "确认购票"
}
},
{
"path": "pages/MyTicketsView/MyTicketsView",
"style": {
"navigationBarTitleText": "本人车票"
}
},
{
"path": "pages/LoginView/LoginView",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/RegistrationView/RegistrationView",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/UserView/UserView",
"style": {
"navigationBarTitleText": "个人主页",
"navigationStyle": "custom"
}
},
{
"path": "pages/SelectStationView/SelectStationView",
"style": {
"navigationBarTitleText": "车站选择"
}
}
],
"tabBar": {
"backgroundColor": "#ffffff",
"color": "#000000", //
"selectedColor": "#3b86ff",
"list": [
//
{
"pagePath": "pages/HomeView/HomeView",
"text": "首页",
"iconPath": "static/icon_home_normal.png",
"selectedIconPath": "static/icon_home_selected.png"
},
{
"pagePath": "pages/MyTicketsView/MyTicketsView",
"text": "车票",
"iconPath": "static/icon_order_normal.png",
"selectedIconPath": "static/icon_order_selected.png"
},
{
"pagePath": "pages/UserView/UserView",
"text": "我的",
"iconPath": "static/icon_mine_normal.png",
"selectedIconPath": "static/icon_mine_selected.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#238cfc",
"backgroundColor": "#F8F8F8",
"app-plus": {
"bounce": "none"
}
},
"uniIdRouter": {}
}

@ -1,229 +0,0 @@
<template>
<view class="buy-ticket-view p-page">
<view class="page-box TrainItem lg p-base">
<view class="text-primary text-center"
>{{ $u.dayjs(date).format("MM月DD日") }}&ensp;{{
$u.dayjs(date).isToday() ? "今天" : $u.timeFormat(date, "[周]dd")
}}</view
>
<u-row :gutter="12" class="mt-base py-llg">
<u-col :span="4">
<view class="Time">{{ fromTime | timeFormat("HH:mm") }}</view>
<view class="Station">{{ from }}</view>
</u-col>
<u-col :span="4">
<view class="text-center" :style="{ fontSize: '14px' }">
<view>{{ trainNo }}</view>
<u-line></u-line>
<view>{{ fromTime | timeDiff(toTime) }}</view>
</view>
</u-col>
<u-col :span="4">
<view class="Time text-end">{{ toTime | timeFormat("HH:mm") }}</view>
<view class="Station text-end">{{ to }}</view>
</u-col>
</u-row>
</view>
<view class="mt-base p-base bg-white rounded-sm">
<view>
<u-text text="乘车人" :size="18" :bold="true"></u-text>
</view>
<view class="my-base">
<u-line></u-line>
</view>
<view>
<u-text :text="$store.state.auth.name"></u-text>
</view>
<view class="mt-sm">
<u-text
type="info"
:size="12"
:text="$store.state.auth.idCardNo | hideIdCardNo"
></u-text>
</view>
</view>
<view class="mt-base" :style="{ fontSize: '13px' }">
<text class="text-info">确定支付表示已阅读并同意</text
><text class="text-primary"
>国铁集团铁路旅客运输规程服务条款</text
>
</view>
<view class="mt-llg">
<u-button
type="primary"
:custom-style="{ borderRadius: `var(--rounded-sm)` }"
@tap="handlePayment"
>确定支付</u-button
>
</view>
<view
class="relative rounded-sm"
:style="{
'--title-move-y': '-13px',
marginTop: 'calc(var(--space-llg) - var(--title-move-y))',
border: '1px dashed #ceccca',
}"
>
<view
class="absolute"
:style="{
backgroundColor: 'var(--color-background)',
marginLeft: '-2px',
paddingLeft: '2px',
height: '26px',
top: 'var(--title-move-y)',
left: '0',
}"
>
<view class="u-flex-y-center" :style="{ fontSize: '15px' }">
<u-icon name="info-circle-fill" color="var(--color-primary)"></u-icon>
<text class="ms-sm fw-bold">温馨提示</text>
</view>
</view>
<view class="p-sm pt-llg text-info" :style="{ fontSize: '13px' }">
<!--
<view
>1.一天内3次申请车票成功后取消订单包含无座车票或不符合选铺需求车票时取消5次计为取消1次当日将不能在Mini-12306继续购票</view
>
-->
<view
>如因运力原因或其他不可控因素导致列车调度调整时当前车型可能会发生变动</view
>
</view>
</view>
</view>
</template>
<script>
import { mapState } from "vuex";
import apiOrders from "@/api/orders.js";
import apiTickets from "@/api/tickets";
export default {
data() {
return {
trainNo: "",
from: "",
fromTime: "",
to: "",
toTime: "",
date: "",
passengers: [],
};
},
computed: {
...mapState({ auth: "auth" }),
},
onLoad(query) {
if (!this.auth.id) {
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
return;
}
/**
* 默认添加当前登录用户作为乘车人
*/
this.passengers.push({
id: this.auth.id,
name: this.auth.name,
idCardNo: this.auth.idCardNo,
});
const { train_no, from, from_time, to, to_time, date } = query;
this.trainNo = decodeURIComponent(train_no);
this.from = decodeURIComponent(from);
this.fromTime = decodeURIComponent(from_time);
this.to = decodeURIComponent(to);
this.toTime = decodeURIComponent(to_time);
this.date = this.$u.dayjs(decodeURIComponent(date)).format();
},
methods: {
handlePayment() {
/**
* 这里将订单确认与支付合并到了一个页面
* 实际上应该确认订单后跳转到支付页面
*
* 订单层其实是预留了实现一个订单多张车票乘车人的情况
*/
uni.showLoading({ title: "正在确认订单", mask: true });
apiOrders
.add({
tickets: this.passengers.map((pax) => ({
trainNo: this.trainNo,
from: this.from,
to: this.to,
date: this.date,
passengerId: pax.id,
})),
})
.then((apiOrdersAddRes) => {
uni.hideLoading();
/**
* 支付订单
*/
uni.showLoading({ title: "正在支付", mask: true });
apiOrders
.payment({
orderNo: apiOrdersAddRes.data.orderNo,
})
.then(() => {
uni.hideLoading();
const wait = 1000;
uni.showToast({
title: "支付成功",
mask: true,
duration: wait,
});
setTimeout(() => {
/**
* 查询支付结果
*/
uni.showLoading({ title: "查询订单状态", mask: true });
apiOrders
.queryPayment({
orderNo: apiOrdersAddRes.data.orderNo,
})
.then(() => {
uni.hideLoading();
const wait = 1000;
uni.showToast({
title: "订单已支付",
mask: true,
duration: wait,
});
setTimeout(() => {
uni.switchTab({
url: "/pages/MyTicketsView/MyTicketsView",
});
}, wait);
})
.catch(() => uni.hideLoading());
}, wait);
})
.catch(() => uni.hideLoading());
})
.catch(() => uni.hideLoading());
},
},
};
</script>
<style scoped>
.buy-ticket-view {
background: linear-gradient(
var(--color-app-blue) 0 40px,
80px,
var(--color-background) 120px
);
}
</style>

@ -1,233 +0,0 @@
<template>
<view class="p-page">
<view
class="page-box-t-overflow page-box-x-overflow"
:style="{ marginBottom: '-12px' }"
>
<u-swiper
:list="[
'https://www.12306.cn/index/images/pic/banner11.jpg',
'https://www.12306.cn/index/images/pic/banner12.jpg',
]"
:radius="0"
:height="200"
:circular="true"
></u-swiper>
</view>
<view class="page-box py-base px-lg relative">
<view>
<u-text type="primary" :size="18" :bold="true" text="火车票"></u-text>
</view>
<view class="mt-base py-base">
<!-- flex 布局总是会因为左右字符长度不同导致中心偏移因此采用 row 布局 -->
<u-row>
<u-col :span="5">
<view @tap="handleSelectStation('from')">
<u-text
:type="from ? undefined : 'info'"
:size="20"
:bold="true"
align="left"
:text="from || '请选择'"
></u-text>
</view>
</u-col>
<u-col :span="2">
<view class="u-flex-x-center">
<u-icon name="arrow-right-double" :size="22"></u-icon>
</view>
</u-col>
<u-col :span="5">
<view @tap="handleSelectStation('to')">
<u-text
:type="to ? undefined : 'info'"
:size="20"
:bold="true"
align="right"
:text="to || '请选择'"
></u-text>
</view>
</u-col>
</u-row>
</view>
<u-line></u-line>
<!-- uLine 颜色比较浅 -->
<view class="py-base">
<view class="u-flex-y-center" @tap="showStationDate = true">
<!-- 直接在 uText 上写的样式可能会在页面更新时被莫名的 bug 覆盖因此要养成间距类样写在 view 标签上的好习惯-->
<view :style="{ flexGrow: 0, whiteSpace: 'nowrap' }">
<u-text :size="20" :text="$u.timeFormat(date, 'M月D日')"></u-text>
</view>
<view :style="{ flexGrow: 1, marginLeft: '0.6em' }">
<u-text
type="info"
:text="
$u.dayjs(date).isToday()
? '今天'
: $u.timeFormat(date, '[周]dd')
"
></u-text>
</view>
</view>
</view>
<u-line></u-line>
<u-calendar
:show="showStationDate"
:min-date="$u.dayjs().format()"
:max-date="$u.dayjs().add(14, 'day').format()"
@confirm="handledateConfirm"
@close="showStationDate = false"
></u-calendar>
<view class="mt-base">
<u-button
type="primary"
:disabled="!from || !to"
@tap="handleToQueryTrain"
>查询车票</u-button
>
</view>
</view>
<!--
<view class="page-box">
<u-grid :border="false" :col="5">
<u-grid-item v-for="item in HOME_MENU" :key="item.label">
<view class="grid-item">
<u-image :src="item.image" width="30px" height="30px"></u-image>
<view class="grid-item-text">{{ item.label }}</view>
</view>
</u-grid-item>
</u-grid>
</view>
-->
<!--
<view class="page-box p-base u-flex-y-center">
<u-image
src="@/static/ic_hot_news.png"
:width="40"
:height="40"
mode="scaleToFill"
class="u-flex-shrink-0"
></u-image>
<view class="ms-lg u-flex-fill">
<u-text
text="中铁快运创新退出跨域当日达"
:size="14"
:bold="true"
></u-text>
<u-text
text='"高铁急送"城市群内4小时城市群间8小时送达'
:size="14"
></u-text>
</view>
<view class="u-flex-shrink-0 u-flex-y-center">
<u-icon name="arrow-right"></u-icon>
</view>
</view>
-->
<!--
<view class="page-box overflow-hidden">
<u-image
src="https://www.12306.cn/index/images/abanner01.jpg"
mode="widthFix"
height="auto"
width="100%"
></u-image>
</view>
<view class="page-box overflow-hidden">
<u-image
src="https://www.12306.cn/index/images/abanner05.jpg"
mode="widthFix"
height="auto"
width="100%"
></u-image>
</view>
-->
</view>
</template>
<script>
import { mapState } from "vuex";
import uNavbarOpacity from "@/utils/mixins/u-navbar-opacity";
const HOME_MENU = [
{ label: "车站大屏", image: require("@/static/home_daping.png") },
{ label: "计次·定期票", image: require("@/static/home_jicipiao.png") },
{ label: "铁路e卡通", image: require("@/static/home_ekatong.png") },
{ label: "时刻表", image: require("@/static/home_liechezhuangtai.png") },
{ label: "温馨服务", image: require("@/static/home_wenxinfuwu.png") },
{ label: "空铁联运", image: require("@/static/home_kongtie.png") },
{ label: "敬老版", image: require("@/static/home_aixin.png") },
{ label: "酒店住宿", image: require("@/static/home_jiudian.png") },
{ label: "约车", image: require("@/static/home_yueche.png") },
{ label: "门票·旅游", image: require("@/static/home_trip.png") },
{ label: "餐饮·特产", image: require("@/static/home_canyin.png") },
{ label: "汽车票", image: require("@/static/home_qichepiao.png") },
{ label: "铁路商城", image: require("@/static/home_shangcheng.png") },
{
label: "中铁畅行卡",
image: require("@/static/home_zhongtiechangxingka.png"),
},
{ label: "出行保险", image: require("@/static/home_chengyixian.png") },
];
export default {
mixins: [uNavbarOpacity],
data() {
return {
from: "",
to: "",
showStationDate: false,
date: new Date(),
HOME_MENU,
searchInput: "",
};
},
computed: {
...mapState({ stations: "stations" }),
},
methods: {
handleSelectStation(key) {
uni.navigateTo({
url: `/pages/SelectStationView/SelectStationView?key=${key}`,
});
},
handledateConfirm(e) {
this.date = this.$u.dayjs(e[0]).startOf("date").format();
this.showStationDate = false;
},
handleToQueryTrain() {
uni.navigateTo({
url: `/pages/QueryTrainView/QueryTrainView?from=${encodeURIComponent(
this.from
)}&to=${encodeURIComponent(this.to)}&date=${encodeURIComponent(
this.date
)}`,
});
},
},
};
</script>
<style scoped>
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding-top: var(--space-base);
padding-bottom: var(--space-base);
}
.grid-item-text {
margin-top: var(--space-ssm);
font-size: 12px;
white-space: nowrap;
}
</style>

@ -1,136 +0,0 @@
<template>
<view class="p-page">
<u-navbar
:placeholder="true"
bg-color="transparent"
:auto-back="true"
></u-navbar>
<view class="mt-llg p-base u-flex u-flex-column u-flex-items-center">
<u-image
src="@/static/logo.png"
:width="60"
:height="60"
:radius="10"
></u-image>
<view class="mt-sm">
<u-text text="欢迎登录" :size="20"></u-text>
</view>
</view>
<view class="p-base">
<u-form
label-position="top"
labelWidth="auto"
:model="formData"
:rules="formRules"
ref="formRef"
>
<u-form-item prop="account">
<u-input
v-model="formData.account"
:custom-style="{ backgroundColor: 'white' }"
@focus="$refs.formRef.clearValidate('account')"
>
<view slot="prefix" class="u-flex-y-center">
<view class="ms-base me-sm">用户</view>
<u-line
direction="col"
length="26px"
:style="{ margin: '0 var(--space-base)' }"
></u-line>
</view>
</u-input>
</u-form-item>
<u-form-item prop="password">
<u-input
v-model="formData.password"
type="password"
:custom-style="{ backgroundColor: 'white' }"
@focus="$refs.formRef.clearValidate('password')"
>
<view slot="prefix" class="u-flex-y-center">
<view class="ms-base me-sm">密码</view>
<u-line
direction="col"
length="26px"
:style="{ margin: '0 var(--space-base)' }"
></u-line> </view
></u-input>
</u-form-item>
</u-form>
<view class="mt-llg">
<u-button type="primary" @tap="handleLogin"></u-button>
</view>
<view class="mt-base">
<u-button @tap="handleToRegister"></u-button>
</view>
<view class="mt-llg text-center text-primary">
<text :style="{ fontSize: '14px' }">服务条款隐私权政策</text>
</view>
</view>
</view>
</template>
<script>
import { mapActions } from "vuex";
import { validateRequired } from "@/utils/form-validators";
import apiAuth from "@/api/auth";
export default {
data() {
return {
formData: {
account: "",
password: "",
},
formRules: {
// trigger
account: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, { label: "用户名" }),
},
password: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, { label: "密码" }),
},
},
loading: false,
};
},
methods: {
...mapActions({ updateAuth: "updateAuth" }),
handleLogin() {
this.$refs.formRef
.validate()
.then(() => {
uni.showLoading({ title: "正在登录", mask: true });
apiAuth
.login({
account: this.formData.account,
password: this.formData.password,
})
.then((res) => {
uni.hideLoading();
this.updateAuth(res.data);
uni.switchTab({
url: "/pages/HomeView/HomeView",
});
})
.catch(() => uni.hideLoading());
})
.catch();
},
handleToRegister() {
uni.navigateTo({
url: "/pages/RegistrationView/RegistrationView",
});
},
},
};
</script>
<style></style>

@ -1,155 +0,0 @@
<template>
<view class="p-page">
<view
v-if="!auth.id || (!loading && tickets.length === 0)"
class="u-flex-xy-center"
:style="{ height: '70vh' }"
>
<view v-if="!auth.id">
<view>
<text>请先登录</text>
</view>
<view class="mt-base">
<u-button type="primary" size="small" @tap="handleToLogin"
>去登录</u-button
>
</view>
</view>
<u-empty
v-else-if="!loading && tickets.length === 0"
mode="order"
icon-color="#9acafc"
text="无购票记录"
text-size="16px"
></u-empty>
</view>
<view
v-for="ticket in tickets"
:key="ticket.orderNO"
class="page-box TrainItem lg"
>
<view class="p-base u-flex u-flex-between OrderInfo">
<text>订单号{{ ticket.orderNO }}</text>
<text>车票仅当日档次有效</text>
</view>
<u-line></u-line>
<view class="px-base py-llg">
<view>
<u-row :gutter="12">
<u-col :span="4">
<view class="Time">{{
ticket.fromTime | timeFormat("HH:mm")
}}</view>
<view class="Station">{{ ticket.from }}</view>
</u-col>
<u-col :span="4">
<view class="text-center">
<view class="my-ssm">{{ ticket.trainNo }}</view>
<u-line></u-line>
<view
class="my-ssm text-center text-info"
:style="{ fontSize: '15px' }"
>{{ $u.dayjs(ticket.date).format("MM月DD日") }}&ensp;{{
$u.dayjs(ticket.date).isToday()
? "今天"
: $u.timeFormat(ticket.date, "[周]dd")
}}</view
>
</view>
</u-col>
<u-col :span="4">
<view class="Time text-end">{{
ticket.toTime | timeFormat("HH:mm")
}}</view>
<view class="Station text-end">{{ ticket.to }}</view>
</u-col>
</u-row>
</view>
<view class="mt-base u-flex-y-center u-flex-between OrderInfo">
<text>已支付</text>
<text class="text-warning" :style="{ fontSize: '15px' }"
>{{ ticket.price }}</text
>
</view>
</view>
<!--
<u-line></u-line>
<view class="u-flex-y-center">
<view class="u-flex-grow OptionItem">改签</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow OptionItem">退票</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow OptionItem">变更到站</view>
</view>
-->
</view>
</view>
</template>
<script>
import { mapState } from "vuex";
import apiTickets from "@/api/tickets";
import { timeAscCompareFn } from "@/utils/functions.js";
export default {
data() {
return {
tickets: [],
loading: false,
};
},
computed: {
...mapState({ auth: "auth" }),
},
/* 注意这里是 onShow 而不是 onLoad详情查看 uniapp 生命周期 */
onShow() {
if (this.auth.id) {
this.loadMyTickets();
}
},
methods: {
handleToLogin() {
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
},
loadMyTickets() {
this.loading = true;
uni.showLoading({ title: "正在加载" });
apiTickets
.get()
.then((res) => {
//
this.tickets = res.data
.sort(
(a, b) =>
timeAscCompareFn(a.fromTime, b.fromTime) ||
timeAscCompareFn(a.toTime, b.toTime) ||
a.id - b.id
)
.filter(
(tkt) =>
!uni.$u
.dayjs(tkt.fromTime)
.isBefore(uni.$u.dayjs().startOf("date"))
);
})
.catch(() => {
this.tickets = [];
})
.then(() => {
this.loading = false;
uni.hideLoading();
});
},
},
};
</script>
<style></style>

@ -1,234 +0,0 @@
<template>
<view>
<u-sticky>
<scroll-view scroll-x="true" class="DatePicker">
<view
v-for="allowableDate in allowableDates"
:key="allowableDate"
:class="[
'DatePickerItem',
{ 'is-active': $u.dayjs(allowableDate).isSame(date, 'day') },
]"
@tap="handleChangeDate(allowableDate)"
>
<view :style="{ fontSize: '14px', textAlign: 'center' }">{{
$u.dayjs(allowableDate).isToday()
? "今天"
: $u.timeFormat(allowableDate, "[周]dd")
}}</view>
<view>{{ allowableDate | timeFormat("MM.DD") }}</view>
</view>
</scroll-view>
</u-sticky>
<view class="p-page">
<view
v-if="!loading && trains.length === 0"
:style="{ textIndent: '2em' }"
>
<text class="text-content"
>很抱歉按您的查询条件当前未找到从<text class="mx-ssm fw-bold">{{
from
}}</text
><text class="mx-ssm fw-bold">{{ to }}</text
>的列车</text
>
</view>
<view
v-for="train in trains"
:key="train.trainNo"
class="page-box TrainItem p-base"
>
<u-row :gutter="12">
<u-col :span="3">
<view class="Time">{{
train.query.fromTime | timeFormat("HH:mm")
}}</view>
<view class="Station">{{ train.query.from }}</view>
</u-col>
<u-col :span="3">
<view class="text-center" :style="{ fontSize: '14px' }">
<view>{{ train.trainNo }}</view>
<u-line></u-line>
<view>{{
train.query.fromTime | timeDiff(train.query.toTime)
}}</view>
</view>
</u-col>
<u-col :span="3">
<view class="Time text-end">{{
train.query.toTime | timeFormat("HH:mm")
}}</view>
<view class="Station text-end">{{ train.query.to }}</view>
</u-col>
<u-col :span="3">
<view class="u-flex-center ps-lg">
<u-button
type="primary"
size="mini"
@tap="handleToByTicket(train)"
>预定</u-button
>
</view>
</u-col>
</u-row>
</view>
</view>
</view>
</template>
<script>
import apiTrains from "@/api/trains.js";
export default {
data() {
return {
allowableDates: [], //
from: "", //
to: "", //
date: "", //
trains: [], //
loading: false, //
};
},
computed: {
queryTrainParams() {
return {
from: this.from,
to: this.to,
date: this.date,
};
},
},
watch: {
queryTrainParams(val) {
this.handleQueryTrain(val);
},
},
onLoad(query) {
const { from, to, date } = query;
this.from = decodeURIComponent(from);
this.to = decodeURIComponent(to);
this.date = this.$u.dayjs(decodeURIComponent(date)).format();
this.initAllowableDates();
},
onReady() {
// `onReady`
uni.setNavigationBarTitle({ title: `${this.from} > ${this.to}` });
uni.setNavigationBarColor({
backgroundColor: "#3c9cff", // --color-primary
frontColor: "#ffffff",
});
},
methods: {
initAllowableDates() {
// 15
for (let i = 0; i < 15; i += 1) {
this.allowableDates.push(this.$u.dayjs().add(i, "day").format());
}
},
handleChangeDate(date) {
this.date = date;
},
handleQueryTrain(params) {
//
if (!params || !params.from || !params.to || !params.date) {
uni.switchTab({ url: "/pages/HomeView/HomeView" });
}
uni.showLoading({ title: "正在加载", mask: true });
this.loading = true;
apiTrains
.get({
from: params.from,
to: params.to,
date: params.date,
})
.then((res) => {
this.trains = res.data.map((trn) => ({
...trn,
query: this.formatTrainToQuery(trn, params.from, params.to),
}));
})
.catch(() => {})
.then(() => {
uni.hideLoading();
this.loading = false;
});
},
handleToByTicket(train) {
if (!this.$store.state.auth.id) {
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
return;
}
uni.navigateTo({
url: `/pages/BuyTicketView/BuyTicketView?train_no=${encodeURIComponent(
train.trainNo
)}&from=${encodeURIComponent(
train.query.from
)}&from_time=${encodeURIComponent(
train.query.fromTime
)}&to=${encodeURIComponent(
train.query.to
)}&to_time=${encodeURIComponent(
train.query.toTime
)}&date=${encodeURIComponent(this.date)}`,
});
},
/**
* 以三个参数获取车次查询需要的显示对象
* @param {Object} train
* @param {String} from 上车站from_station)
* @param {String} to 下车站to_station
*/
formatTrainToQuery(train, from, to) {
const stations = train.stations;
const fromIndex = stations.findIndex((stn) => stn.name === from);
const toIndex = stations.findIndex((stn) => stn.name === to);
let price = 0;
for (let i = fromIndex; i <= toIndex; i += 1) {
price += stations[i].price;
}
return {
from: from, //
fromTime: stations[fromIndex].depTime, //
to: to, //
toTime: stations[toIndex].arrTime, //
};
},
},
};
</script>
<style scoped lang="scss">
$date-picker-bg: $u-primary;
$date-picker-color: #ffffff;
.DatePicker {
white-space: nowrap;
padding: var(--space-sm) var(--space-base);
background-color: $date-picker-bg;
color: $date-picker-color;
}
.DatePickerItem + .DatePickerItem {
margin-left: var(--space-sm);
}
.DatePickerItem {
display: inline-block;
padding: var(--space-ssm);
border-radius: calc(var(--space-ssm) * 1.5);
&.is-active {
background-color: $date-picker-color;
color: $date-picker-bg;
}
}
</style>

@ -1,386 +0,0 @@
<template>
<view class="py-base">
<u-form
:model="formData"
:rules="formRules"
errorType="border-bottom"
ref="formRef"
>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="基本信息"></u--text>
</view>
</u-form-item>
<u-form-item
prop="account"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&ensp;&ensp;</view>
<u-input
v-model="formData.account"
placeholder='字母、数字或"_"6~30位'
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('account')"
></u-input>
</view>
</u-form-item>
<u-form-item
prop="password"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&emsp;&emsp;</view>
<u-input
v-model="formData.password"
placeholder='字母、数字或"_"组合6~30位'
type="password"
border="none"
input-align="right"
@focus="$refs.formRef.clearValidate('password')"
></u-input>
</view>
</u-form-item>
<u-form-item
prop="passwordConfirm"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>确认密码</view>
<u-input
v-model="formData.passwordConfirm"
placeholder="请再次输入密码"
type="password"
border="none"
input-align="right"
@focus="$refs.formRef.clearValidate('passwordConfirm')"
></u-input>
</view>
</u-form-item>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="证件信息"></u--text>
</view>
</u-form-item>
<u-form-item
prop="name"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&emsp;&emsp;</view>
<u-input
v-model="formData.name"
placeholder="请输入真实姓名,以便购票"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('name')"
></u-input>
</view>
</u-form-item>
<u-form-item
prop="idCardNo"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>身份证号</view>
<u-input
v-model="formData.idCardNo"
placeholder="请输入身份证号码"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('idCardNo')"
></u-input>
</view>
</u-form-item>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="联系方式"></u--text>
</view>
</u-form-item>
<u-form-item
prop="mobileNo"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>手机号码</view>
<u-input
v-model="formData.mobileNo"
placeholder="请输入手机号码"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('mobileNo')"
>
<template slot="suffix"
><u-code ref="codeRef" @change="handleCodeChange"></u-code
><u-button type="success" size="mini" @tap="handleSendCode">{{
codeTips
}}</u-button></template
>
</u-input>
</view>
<u-modal
:show="mobileCodeModalVisible"
:async-close="true"
title="验证码"
confirm-text="复制验证码"
@confirm="handleMobileCodeModalConfirm"
>
<view class="text-center">
<text :style="{ fontSize: '24px' }">{{
mobileCodeModalValue
}}</text>
</view>
</u-modal>
</u-form-item>
<u-form-item
prop="mobileCode"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&ensp;&ensp;</view>
<u-input
v-model="formData.mobileCode"
placeholder="请输入手机验证码"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('mobileCode')"
></u-input>
</view>
</u-form-item>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="支付信息"></u--text>
</view>
</u-form-item>
<u-form-item
prop="bankCardNo"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>银行卡号</view>
<u-input
v-model="formData.bankCardNo"
placeholder="请输入银行卡号"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('bankCardNo')"
></u-input>
</view>
</u-form-item>
<u-form-item><!-- 当间距用 --></u-form-item>
<view class="px-base mb-lg u-flex-y-center">
<u-checkbox-group v-model="checkboxValues">
<u-checkbox name="agreeTerms" label="同意"></u-checkbox>
</u-checkbox-group>
<text class="text-primary" :style="{ fontSize: '15px' }"
>服务条款隐私权政策</text
>
</view>
<u-form-item>
<u-button type="primary" @tap="handleRegister"></u-button>
</u-form-item>
</u-form>
<u-modal
:show="errorModalVisible"
title="温馨提示"
:content="errorModalValue"
@confirm="errorModalVisible = false"
></u-modal>
</view>
</template>
<script>
import {
validateRequired,
validateAccount,
validatePassword,
validatePasswordConfirm,
} from "@/utils/form-validators";
import apiAuth from "@/api/auth.js";
import apiOther from "@/api/other.js";
export default {
data() {
return {
formData: {
account: "",
password: "",
passwordConfirm: "",
name: "",
idCardNo: "",
mobileNo: "",
mobileCode: "",
bankCardNo: "",
},
formRules: {
// 使 trigger
account: { validator: validateAccount, trigger: "blur" },
password: { validator: validatePassword, trigger: "blur" },
passwordConfirm: {
validator: (rule, value, callback) =>
validatePasswordConfirm(
rule,
value,
callback,
this.formData.password
),
trigger: "blur",
},
name: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "姓名",
trim: true,
}),
trigger: "blur",
},
idCardNo: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "身份证号",
trim: true,
}),
trigger: "blur",
},
mobileNo: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "手机号码",
trim: true,
}),
trigger: "blur",
},
mobileCode: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "验证码",
trim: true,
}),
trigger: "blur",
},
bankCardNo: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "银行卡号",
trim: true,
}),
trigger: "blur",
},
},
codeTips: "",
mobileCodeModalVisible: false,
mobileCodeModalValue: "",
checkboxValues: [],
errorModalVisible: false,
errorModalValue: "",
};
},
methods: {
handleRegister() {
this.$refs.formRef
.validate()
.then(() => {
if (!this.checkboxValues.includes("agreeTerms")) {
this.openErrorModal("同意《服务条款》才能注册");
return;
}
uni.showLoading({ title: "正在注册", mask: true });
apiAuth
.add(this.formData)
.then(() => {
uni.hideLoading();
const wait = 1000;
uni.showToast({
title: "注册成功,请使用新账号登录",
mask: true,
duration: wait,
});
setTimeout(() => {
uni.redirectTo({ url: "/pages/LoginView/LoginView" });
}, wait);
})
.catch(() => uni.hideLoading());
})
.catch((errors) => {
this.openErrorModal(errors[0].message);
});
},
handleSendCode() {
if (this.$refs.codeRef.canGetCode) {
this.$refs.formRef.validateField("mobileNo", (errors) => {
if (!errors.length) {
uni.showLoading({ title: "正在发送验证码", mask: true });
apiOther
.sendMobileCode({
mobileNo: this.formData.mobileNo,
})
.then((res) => {
uni.hideLoading();
uni.$u.toast("验证码发送成功");
this.$refs.codeRef.start();
// code
this.mobileCodeModalValue = "";
setTimeout(() => {
uni.hideToast();
this.mobileCodeModalValue = res.data.code;
this.mobileCodeModalVisible = true;
}, 1000);
})
.catch(() => uni.hideLoading());
}
});
}
},
handleCodeChange(text) {
this.codeTips = text;
},
handleMobileCodeModalConfirm() {
uni.setClipboardData({
data: String(this.mobileCodeModalValue),
success: () => {
this.mobileCodeModalVisible = false;
},
fail: (e) => {
uni.showToast({
icon: "error",
title: "复制失败",
});
},
});
},
openErrorModal(content) {
this.errorModalValue = content;
this.errorModalVisible = true;
},
},
};
</script>
<style></style>

@ -1,59 +0,0 @@
<template>
<u-index-list :index-list="stationList.indexList">
<template v-for="(item, index) in stationList.itemArr">
<u-index-item>
<u-index-anchor :text="stationList.indexList[index]"></u-index-anchor>
<view
v-for="(cell, i) in item"
@tap="handleSelectedStation(cell)"
class="py-base mx-base"
:class="[{ 'u-border-bottom': i !== item.length - 1 }]"
>{{ cell }}</view
>
</u-index-item>
</template>
</u-index-list>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
key: "",
};
},
computed: {
...mapState({ stations: "stations" }),
stationList() {
// updateStations stations
const indexList = [];
const itemArr = [];
for (const station of this.stations) {
const currChar = indexList.slice(-1)[0];
const char = station.pinyin[0];
if (currChar === char) {
itemArr.slice(-1)[0].push(station.name);
} else {
indexList.push(char);
itemArr.push([station.name]);
}
}
return { indexList, itemArr };
},
},
onLoad(query) {
const { key } = query;
this.key = key;
},
methods: {
handleSelectedStation(stationName) {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
prevPage.$vm[this.key] = stationName;
uni.navigateBack({ delta: 1 });
},
},
};
</script>

@ -1,87 +0,0 @@
<template>
<view v-if="nextTicket" class="page-box TrainItem lg">
<view class="px-base py-llg">
<view>
<u-row :gutter="12">
<u-col :span="4">
<view class="Time">{{
nextTicket.fromTime | timeFormat("HH:mm")
}}</view>
<view class="Station">{{ nextTicket.from }}</view>
</u-col>
<u-col :span="4">
<view class="text-center">
<view class="my-ssm">{{ nextTicket.trainNo }}</view>
<u-line></u-line>
<view
class="my-ssm text-center text-info"
:style="{ fontSize: '15px' }"
>{{ $u.dayjs(nextTicket.date).format("MM月DD日") }}&ensp;{{
$u.dayjs(nextTicket.date).isToday()
? "今天"
: $u.timeFormat(nextTicket.date, "[周]dd")
}}</view
>
</view>
</u-col>
<u-col :span="4">
<view class="Time text-end">{{
nextTicket.toTime | timeFormat("HH:mm")
}}</view>
<view class="Station text-end">{{ nextTicket.to }}</view>
</u-col>
</u-row>
</view>
</view>
</view>
<view v-else-if="loading" class="u-flex-xy-center p-base">
<text class="text-info">加载中...</text>
</view>
<view v-else class="u-flex-xy-center p-base">
<text class="text-info">当前没有出行计划</text>
</view>
</template>
<script>
import { mapState } from "vuex";
import apiTickets from "@/api/tickets";
import { timeAscCompareFn } from "@/utils/functions.js";
export default {
data() {
return {
loading: false,
nextTicket: undefined,
};
},
computed: {
...mapState({ auth: "auth" }),
},
mounted() {
this.loadMyTickets();
},
methods: {
loadMyTickets() {
this.loading = true;
apiTickets
.get()
.then((res) => {
this.nextTicket = res.data
.sort(
(a, b) =>
timeAscCompareFn(a.fromTime, b.fromTime) ||
timeAscCompareFn(a.toTime, b.toTime) ||
a.id - b.id
)
.find((tkt) => uni.$u.dayjs().isBefore(uni.$u.dayjs(tkt.toTime)));
})
.catch(() => {
this.nextTicket = undefined;
})
.then(() => {
this.loading = false;
});
},
},
};
</script>

@ -1,249 +0,0 @@
<template>
<view class="user-view p-page">
<u-navbar :placeholder="true" :bg-color="uNavbarBgColor" left-icon="">
<view slot="left">
<text
class="fw-bold"
:style="{ color: uNavbarTitleColor, fontSize: '22px' }"
>我的</text
>
</view>
<view slot="right" class="u-flex">
<u-icon
name="setting"
:size="30"
:color="uNavbarIconColor"
class="mx-sm"
></u-icon>
<u-icon
name="question-circle"
:size="30"
:color="uNavbarIconColor"
class="mx-sm"
></u-icon>
</view>
</u-navbar>
<view class="mt-base u-flex-y-center">
<u-avatar :size="65" @tap="handleToLogin"></u-avatar>
<view class="ms-base">
<view @tap="handleToLogin">
<text class="text-white fw-bold" :style="{ fontSize: '24px' }">{{
auth.name || "未登录"
}}</text>
</view>
<view class="mt-base">
<text class="text-white">便捷出行就在12306</text>
</view>
</view>
</view>
<view class="mt-llg page-box">
<view class="pt-base px-base">
<u-text text="当前出行" :size="18" :bold="true"> TODO </u-text>
</view>
<NextTicketVue ref="NextTicketVue" v-if="auth.id"></NextTicketVue>
<view v-else class="py-llg u-flex-xy-center">
<view>
<u-button type="primary" size="small" @tap="handleToLogin"
>去登录</u-button
>
</view>
</view>
</view>
<view v-if="auth.id" class="mt-base page-box">
<u-cell-group>
<u-cell title="身份证号" :label="auth.idCardNo | hideIdCardNo"></u-cell>
<u-cell title="手机号" :label="auth.mobileNo | hideMobileNo"></u-cell>
<u-cell
title="银行卡号"
:label="auth.bankCardNo | hideBankCardNo"
></u-cell>
</u-cell-group>
</view>
<!--
<view class="mt-base page-box u-flex-y-center">
<view class="u-flex-grow grid-item">
<u-image
src="@/static/img_chengcheren.png"
height="36px"
width="36px"
></u-image>
<view class="grid-item-text">乘车人</view>
</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow grid-item">
<u-image
src="@/static/img_dingdan.png"
height="36px"
width="36px"
></u-image>
<view class="grid-item-text">我的订单</view>
</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow grid-item">
<u-image
src="@/static/img_youhuiquan.png"
height="36px"
width="36px"
></u-image>
<view class="grid-item-text">优惠券</view>
</view>
</view>
-->
<!--
<view class="page-box">
<view class="px-base py-lg">
<u-text text="出行向导" :size="18" :bold="true"></u-text>
</view>
<u-line></u-line>
<u-grid :border="false" :col="4">
<u-grid-item v-for="item in PAGE_MENU_1" :key="item.label">
<view class="grid-item">
<u-image :src="item.image" height="30px" width="30px"></u-image>
<view class="grid-item-text">{{ item.label }}</view>
</view>
</u-grid-item>
</u-grid>
</view>
-->
<!--
<view class="page-box">
<view class="px-base py-lg">
<u-text text="温馨服务" :size="18" :bold="true"></u-text>
</view>
<u-line></u-line>
<u-grid :border="false" :col="4">
<u-grid-item v-for="item in PAGE_MENU_2" :key="item.label">
<view class="grid-item">
<u-image :src="item.image" height="30px" width="30px"></u-image>
<view class="grid-item-text">{{ item.label }}</view>
</view>
</u-grid-item>
</u-grid>
</view>
-->
<!--
<view class="page-box">
<view class="px-base py-lg">
<u-text text="信息服务" :size="18" :bold="true"></u-text>
</view>
<u-cell-group>
<u-cell title="公告" size="large" :is-link="true"></u-cell>
<u-cell title="常见问题" size="large" :is-link="true"></u-cell>
<u-cell title="使用须知" size="large" :is-link="true"></u-cell>
<u-cell title="服务规章" size="large" :is-link="true"></u-cell>
<u-cell title="铁路保险" size="large" :is-link="true"></u-cell>
<u-cell
title="关于"
size="large"
:is-link="true"
:border="false"
></u-cell>
</u-cell-group>
</view>
-->
<view :style="{ height: '25vh' }"></view>
<view v-if="auth.id" class="mt-base">
<u-button type="error" @tap="handleLogout">退</u-button>
</view>
</view>
</template>
<script>
import { mapActions, mapState } from "vuex";
import uNavbarOpacity from "@/utils/mixins/u-navbar-opacity";
import NextTicketVue from "./NextTicket.vue";
const PAGE_MENU_1 = [
{ label: "车站大屏", image: require("@/static/icon_chezhandaping.png") },
{ label: "时刻表", image: require("@/static/icon_shikebiao.png") },
{ label: "起售时间", image: require("@/static/icon_qishoushijian.png") },
{
label: "正晚点查询",
image: require("@/static/icon_zhengwandianchaxun.png"),
},
{ label: "票价查询", image: require("@/static/icon_piaojiachaxun.png") },
{ label: "换乘时间", image: require("@/static/icon_huanchengshijian.png") },
{
label: "代售点查询",
image: require("@/static/icon_daishoudianchaxun.png"),
},
{ label: "更多", image: require("@/static/icon_gengduo.png") },
];
const PAGE_MENU_2 = [
{
label: "临时身份证明",
image: require("@/static/icon_linshishenfenzhengming.png"),
},
{ label: "遗失物品", image: require("@/static/icon_yishiwupin.png") },
{ label: "建议", image: require("@/static/icon_jianyi.png") },
{ label: "投诉", image: require("@/static/icon_tousu.png") },
{ label: "重点旅客", image: require("@/static/icon_zhongdianlvke.png") },
{ label: "客服电话", image: require("@/static/icon_kefudianhua.png") },
{ label: "希望工程", image: require("@/static/icon_xiwanggongcheng.png") },
{
label: "外国商务人员",
image: require("@/static/icon_waiguoshangwurenyuan.png"),
},
];
export default {
mixins: [uNavbarOpacity],
components: { NextTicketVue },
data() {
return {
PAGE_MENU_1,
PAGE_MENU_2,
};
},
computed: {
...mapState({ auth: "auth" }),
},
onShow() {
// onShow onShow 访 nextTicket
if (this.$refs.NextTicketVue) this.$refs.NextTicketVue.loadMyTickets();
},
methods: {
...mapActions({ revertAuth: "revertAuth" }),
handleToLogin() {
if (this.auth.id) return;
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
},
handleLogout() {
this.revertAuth();
},
},
};
</script>
<style scoped>
.user-view {
background: linear-gradient(
var(--color-app-blue) 0 100px,
250px,
var(--color-background) 400px
);
}
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-llg) var(--space-ssm);
}
.grid-item-text {
margin-top: var(--space-sm);
font-size: 13px;
white-space: nowrap;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save