├── api
├── __init__.py
├── notification.py
├── scheduler.py
├── auth.py
├── email_utils.py
├── easy_park.py
├── http_api.py
└── ocr_class.py
├── my-cron
├── crontab.sh
├── db
├── __init__.py
├── mysql.cnf
├── init_db.py
└── init.sql
├── parking
└── .DS_Store
├── config
├── sr.caffemodel
├── detect.caffemodel
├── README.md
├── sr.prototxt
└── LICENSE
├── .gitignore
├── requirements.txt
├── scripts
└── generate_secrets.py
├── run_api.py
├── Dockerfile
├── docker-compose.yml
├── README.md
└── frontend
├── js
└── alert.js
├── styles.css
├── login.html
├── reset_password.html
├── register.html
├── app.js
├── index.css
├── qrcode.min.js
└── index.html
/api/__init__.py:
--------------------------------------------------------------------------------
1 | # 空文件,用于标记这是一个 Python 包
--------------------------------------------------------------------------------
/my-cron:
--------------------------------------------------------------------------------
1 | 30 10 * * * /bin/bash /code/crontab.sh
--------------------------------------------------------------------------------
/crontab.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd /code/
3 | /usr/bin/python3 /code/run_api.py
4 |
5 |
--------------------------------------------------------------------------------
/db/__init__.py:
--------------------------------------------------------------------------------
1 | from .database import DatabaseManager
2 |
3 | __all__ = ['DatabaseManager']
--------------------------------------------------------------------------------
/parking/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/easy_park/main/parking/.DS_Store
--------------------------------------------------------------------------------
/config/sr.caffemodel:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/easy_park/main/config/sr.caffemodel
--------------------------------------------------------------------------------
/config/detect.caffemodel:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/easy_park/main/config/detect.caffemodel
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .venv
3 | .idea
4 | .vscode
5 | **/__pycache__/
6 | *.pyc
7 | *.pyo
8 | *.pyd
9 | *.pyw
10 | *.pyz
11 | config/config.yml
12 | .vscode
--------------------------------------------------------------------------------
/db/mysql.cnf:
--------------------------------------------------------------------------------
1 | [mysql]
2 | default-character-set=utf8mb4
3 |
4 | [client]
5 | default-character-set=utf8mb4
6 |
7 | [mysqld]
8 | character-set-server=utf8mb4
9 | collation-server=utf8mb4_unicode_ci
10 | max_connections=1000
11 | wait_timeout=28800
12 | interactive_timeout=28800
--------------------------------------------------------------------------------
/config/README.md:
--------------------------------------------------------------------------------
1 | ### WeChat QRCode
2 |
3 | CNN models for `wechat_qrcode` module, including the detector model and the super scale model.
4 |
5 | |file|MD5|
6 | |----|----|
7 | |detect.caffemodel|238e2b2d6f3c18d6c3a30de0c31e23cf|
8 | |detect.prototxt|6fb4976b32695f9f5c6305c19f12537d|
9 | |sr.caffemodel|cbfcd60361a73beb8c583eea7e8e6664|
10 | |sr.prototxt|69db99927a70df953b471daaba03fbef|
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==3.1.0
2 | idna==3.7
3 | itsdangerous==2.2.0
4 | Jinja2==3.1.5
5 | MarkupSafe==3.0.2
6 | numpy==1.26.4
7 | opencv-contrib-python==4.9.0.80
8 | opencv-python==4.9.0.80
9 | PyMySQL==1.1.1
10 | pypng==0.20220715.0
11 | pytz==2024.2
12 | PyYAML==6.0.2
13 | pyzbar==0.1.9
14 | qrcode==7.4.2
15 | requests==2.32.0
16 | schedule==1.2.2
17 | gunicorn==21.2.0
18 | typing_extensions==4.11.0
19 | urllib3==2.2.2
20 | Werkzeug==3.1.3
21 | cryptography>=42.0.0
22 | Pillow==10.2.0
23 | Flask-Limiter==3.5.1
24 | Flask-WTF==1.2.1
25 | PyJWT==2.8.0
26 |
--------------------------------------------------------------------------------
/scripts/generate_secrets.py:
--------------------------------------------------------------------------------
1 | import os
2 | import secrets
3 |
4 | def generate_jwt_secret():
5 | """生成一个安全的 JWT 密钥"""
6 | return secrets.token_hex(32)
7 |
8 | def create_env_file():
9 | """创建或更新 .env 文件"""
10 | jwt_secret = generate_jwt_secret()
11 |
12 | env_content = f"""JWT_SECRET={jwt_secret}
13 | MYSQL_ROOT_PASSWORD=1ux2Dewk2xdrk5uqh.CMU
14 | """
15 |
16 | with open('.env', 'w') as f:
17 | f.write(env_content)
18 |
19 | print("已生成新的 .env 文件")
20 | print(f"JWT_SECRET={jwt_secret}")
21 |
22 | if __name__ == "__main__":
23 | create_env_file()
--------------------------------------------------------------------------------
/run_api.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | # 添加项目根目录到 Python 路径
5 | project_root = os.path.dirname(os.path.abspath(__file__))
6 | if project_root not in sys.path:
7 | sys.path.insert(0, project_root)
8 |
9 | from api.app import app
10 | from api.scheduler import run_scheduler
11 | import threading
12 |
13 | def init_scheduler():
14 | """初始化并启动调度器"""
15 | scheduler_thread = threading.Thread(target=run_scheduler)
16 | scheduler_thread.daemon = True
17 | scheduler_thread.start()
18 |
19 | # 初始化调度器
20 | init_scheduler()
21 |
22 | if __name__ == '__main__':
23 | # 本地开发时使用
24 | app.run(host='0.0.0.0', port=8000, debug=False)
--------------------------------------------------------------------------------
/api/notification.py:
--------------------------------------------------------------------------------
1 | from requests import get
2 | from typing import Optional
3 |
4 | class NotificationService:
5 | """通知服务类,用于处理各种通知消息"""
6 |
7 | def __init__(self, bark_key: str = "jszYkyQV7bnp3xRU63jcHF"):
8 | """
9 | 初始化通知服务
10 | :param bark_key: Bark 应用的 API key
11 | """
12 | self.bark_key = bark_key
13 | self.bark_base_url = "https://api.day.app"
14 |
15 | def send_bark_notification(self, message: str, title: Optional[str] = None) -> bool:
16 | """
17 | 发送 Bark 通知
18 | :param message: 通知消息内容
19 | :param title: 通知标题(可选)
20 | :return: 发送是否成功
21 | """
22 | try:
23 | url = f"{self.bark_base_url}/{self.bark_key}/"
24 | if title:
25 | url += f"{title}/"
26 | url += message
27 |
28 | response = get(url)
29 | if response.status_code == 200:
30 | print(f"通知发送成功: {message}")
31 | return True
32 | else:
33 | print(f"通知发送失败: {response.status_code}")
34 | return False
35 | except Exception as e:
36 | print(f"通知发送出错: {str(e)}")
37 | return False
38 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 | LABEL maintainer="hi@awen.me"
3 |
4 | ENV TZ=Asia/Shanghai
5 | ENV DEBIAN_FRONTEND=noninteractive
6 | ENV LANG=C.UTF-8
7 | ENV LC_ALL=C.UTF-8
8 |
9 | # 使用清华镜像源
10 | RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
11 | && sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn\/debian-security/g' /etc/apt/sources.list.d/debian.sources
12 |
13 | WORKDIR /code
14 |
15 | # 复制依赖文件
16 | COPY requirements.txt .
17 |
18 | # 安装依赖,完成后清理缓存
19 | RUN apt-get update \
20 | && apt-get install -y --no-install-recommends \
21 | tzdata \
22 | libgl1-mesa-glx \
23 | libglib2.0-0 \
24 | libsm6 \
25 | libxext6 \
26 | libxrender-dev \
27 | libzbar0 \
28 | gcc \
29 | python3-dev \
30 | libffi-dev \
31 | && ln -sf /usr/share/zoneinfo/$TZ /etc/localtime \
32 | && echo $TZ > /etc/timezone \
33 | && pip install --no-cache-dir --upgrade pip \
34 | && pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
35 | && pip install --no-cache-dir -r requirements.txt \
36 | && apt-get clean \
37 | && rm -rf /var/lib/apt/lists/* \
38 | && rm -rf ~/.cache/pip/*
39 |
40 | # 复制应用代码
41 | COPY . .
42 |
43 | # 创建必要的目录
44 | RUN mkdir -p parking/code parking/output_file static \
45 | && chmod -R 777 parking static \
46 | && ls -la api/ \
47 | && python -m compileall api/
48 |
49 | CMD ["gunicorn", "run_api:app", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120"]
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | mysql:
3 | container_name: parking-mysql
4 | image: mysql:8.0
5 | environment:
6 | MYSQL_ROOT_PASSWORD: 1ux2Dewk2xdrk5uqh.CMU
7 | MYSQL_DATABASE: parking
8 | TZ: Asia/Shanghai
9 | volumes:
10 | - mysql_data:/var/lib/mysql
11 | - ./db/mysql.cnf:/etc/mysql/conf.d/mysql.cnf:ro
12 | - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
13 | ports:
14 | - "3306:3306"
15 | healthcheck:
16 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
17 | interval: 10s
18 | timeout: 5s
19 | retries: 5
20 | command: --default-authentication-plugin=mysql_native_password
21 | networks:
22 | - app-network
23 |
24 | db-init:
25 | container_name: parking-db-init
26 | build: .
27 | command: python db/init_db.py
28 | environment:
29 | MYSQL_HOST: mysql
30 | depends_on:
31 | mysql:
32 | condition: service_healthy
33 | networks:
34 | - app-network
35 |
36 | app:
37 | container_name: parking-app
38 | build: .
39 | env_file:
40 | - .env
41 | ports:
42 | - "8000:8000"
43 | volumes:
44 | - ./parking:/code/parking
45 | - ./static:/code/static
46 | - ./config:/code/config
47 | environment:
48 | MYSQL_HOST: mysql
49 | MYSQL_PORT: 3306
50 | JWT_SECRET: ${JWT_SECRET}
51 | depends_on:
52 | mysql:
53 | condition: service_healthy
54 | networks:
55 | - app-network
56 | command: >
57 | sh -c "
58 | echo 'Checking environment variables...' &&
59 | echo 'JWT_SECRET=' &&
60 | echo $$JWT_SECRET &&
61 | gunicorn run_api:app --bind 0.0.0.0:8000 --workers 4 --timeout 120
62 | "
63 |
64 | networks:
65 | app-network:
66 | driver: bridge
67 |
68 | volumes:
69 | mysql_data:
--------------------------------------------------------------------------------
/api/scheduler.py:
--------------------------------------------------------------------------------
1 | import schedule
2 | import time
3 | from datetime import datetime
4 | from db import DatabaseManager
5 | from api.easy_park import EasyPark
6 |
7 | def check_and_execute_tasks():
8 | """检查并执行定时任务"""
9 | current_time = datetime.now()
10 | current_hour = current_time.hour
11 | current_minute = current_time.minute
12 | current_weekday = str(current_time.weekday()) # 0-6 表示周一到周日
13 |
14 | db = DatabaseManager()
15 | active_schedules = db.get_active_schedules()
16 |
17 | for task in active_schedules:
18 | try:
19 | # 检查是否到达执行时间
20 | if (task['hour'] == current_hour and
21 | task['minute'] == current_minute and
22 | current_weekday in task['weekdays'].split(',')):
23 | # 检查今天是否已经执行过
24 | if db.check_task_executed_today(task['id']):
25 | print(f"任务 {task['id']} 今天已经执行过")
26 | db.log_task_execution(task['id'], False, f"任务 {task['id']} 今天已经执行过")
27 | continue
28 |
29 | try:
30 | # 初始化 EasyPark 实例
31 | easy_park = EasyPark()
32 | # 执行核销任务
33 | if easy_park.process_unused_coupons():
34 | # 记录成功执行
35 | db.log_task_execution(task['id'], True)
36 | except Exception as e:
37 | error_msg = str(e)
38 | print(f"执行任务失败: {error_msg}")
39 | # 记录执行失败
40 | db.log_task_execution(task['id'], False, error_msg)
41 | except Exception as e:
42 | print(f"处理任务 {task.get('id')} 失败: {str(e)}")
43 |
44 | def run_scheduler():
45 | """运行调度器"""
46 | # 每分钟检查一次
47 | schedule.every().minute.do(check_and_execute_tasks)
48 |
49 | while True:
50 | schedule.run_pending()
51 | time.sleep(1)
52 |
53 | if __name__ == "__main__":
54 | run_scheduler()
--------------------------------------------------------------------------------
/api/auth.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | import datetime
3 | from functools import wraps
4 | from flask import request, jsonify, make_response
5 | import os
6 |
7 | # 生成随机密钥
8 | JWT_SECRET = os.environ.get('JWT_SECRET', os.urandom(24).hex())
9 | JWT_ALGORITHM = 'HS256'
10 | DEFAULT_EXPIRATION = datetime.timedelta(days=1)
11 | REMEMBER_EXPIRATION = datetime.timedelta(days=30)
12 |
13 | def generate_token(user_data, remember=False):
14 | """生成 JWT token"""
15 | payload = {
16 | 'username': user_data['username'],
17 | 'exp': datetime.datetime.utcnow() + (REMEMBER_EXPIRATION if remember else DEFAULT_EXPIRATION),
18 | 'iat': datetime.datetime.utcnow()
19 | }
20 | return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
21 |
22 | def token_required(f):
23 | """验证 JWT token 的装饰器"""
24 | @wraps(f)
25 | def decorated(*args, **kwargs):
26 | token = request.cookies.get('token')
27 |
28 | print("Checking token:", token)
29 |
30 | if not token:
31 | print("No token found in cookies")
32 | return jsonify({
33 | 'code': 1,
34 | 'message': '未登录'
35 | }), 401
36 |
37 | try:
38 | payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
39 | print("Token decoded successfully:", payload)
40 | request.current_user = payload['username']
41 | except jwt.ExpiredSignatureError:
42 | print("Token expired")
43 | response = make_response(jsonify({
44 | 'code': 1,
45 | 'message': 'token已过期'
46 | }))
47 | response.delete_cookie('token')
48 | return response, 401
49 | except jwt.InvalidTokenError:
50 | print("Invalid token")
51 | response = make_response(jsonify({
52 | 'code': 1,
53 | 'message': '无效的token'
54 | }))
55 | response.delete_cookie('token')
56 | return response, 401
57 | except Exception as e:
58 | print(f"Unexpected error decoding token: {str(e)}")
59 | return jsonify({
60 | 'code': 1,
61 | 'message': '认证失败'
62 | }), 401
63 |
64 | return f(*args, **kwargs)
65 | return decorated
--------------------------------------------------------------------------------
/api/email_utils.py:
--------------------------------------------------------------------------------
1 | import smtplib
2 | import yaml
3 | import os
4 | from email.mime.text import MIMEText
5 | from email.header import Header
6 | import random
7 | import string
8 | from datetime import datetime, timedelta
9 |
10 | class EmailSender:
11 | _instance = None
12 |
13 | def __new__(cls):
14 | if cls._instance is None:
15 | cls._instance = super().__new__(cls)
16 | return cls._instance
17 |
18 | def __init__(self):
19 | if not hasattr(self, 'initialized'):
20 | self.config = self._load_config()
21 | self.smtp_server = self.config['smtp_server']
22 | self.smtp_port = self.config['smtp_port']
23 | self.sender = self.config['sender']
24 | self.password = self.config['password']
25 | self.initialized = True
26 |
27 | def _load_config(self):
28 | """加载邮箱配置"""
29 | config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yml')
30 | with open(config_path, 'r') as f:
31 | config = yaml.safe_load(f)
32 | return config['email']
33 |
34 | def send_verification_code(self, to_email: str, code: str, type_str: str):
35 | """
36 | 发送验证码邮件
37 | :param to_email: 收件人邮箱
38 | :param code: 验证码
39 | :param type_str: 验证码类型(register/reset)
40 | :return: 是否发送成功
41 | """
42 | try:
43 | # 根据类型设置不同的主题和内容
44 | if type_str == 'register':
45 | subject = '注册验证码'
46 | content = f'您的注册验证码是:{code},有效期5分钟。请勿将验证码泄露给他人。'
47 | else:
48 | subject = '重置密码验证码'
49 | content = f'您的重置密码验证码是:{code},有效期5分钟。请勿将验证码泄露给他人。'
50 |
51 | message = MIMEText(content, 'plain', 'utf-8')
52 | message['From'] = Header(self.sender)
53 | message['To'] = Header(to_email)
54 | message['Subject'] = Header(subject)
55 |
56 | # 连接SMTP服务器并发送邮件
57 | with smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) as server:
58 | server.login(self.sender, self.password)
59 | server.sendmail(self.sender, [to_email], message.as_string())
60 | return True
61 | except Exception as e:
62 | print(f"发送邮件失败: {str(e)}")
63 | return False
64 |
65 | @staticmethod
66 | def generate_verification_code():
67 | """生成6位数字验证码"""
68 | return ''.join(random.choices(string.digits, k=6))
--------------------------------------------------------------------------------
/db/init_db.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pymysql
3 | import yaml
4 | import time
5 | import sys
6 |
7 | def init_database():
8 | # 加载配置
9 | config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yml')
10 | with open(config_path, 'r') as f:
11 | config = yaml.safe_load(f)['database']
12 |
13 | # 覆盖配置中的主机名
14 | host = os.environ.get('MYSQL_HOST', config['host'])
15 | port = int(os.environ.get('MYSQL_PORT', config['port']))
16 |
17 | max_retries = 30
18 | retry_interval = 2
19 |
20 | for attempt in range(max_retries):
21 | try:
22 | print(f"尝试连接数据库 (attempt {attempt + 1}/{max_retries})...")
23 | # 连接MySQL(不指定数据库)
24 | connection = pymysql.connect(
25 | host=host,
26 | port=port,
27 | user=config['user'],
28 | password=config['password']
29 | )
30 |
31 | try:
32 | with connection.cursor() as cursor:
33 | # 创建数据库
34 | cursor.execute(f"CREATE DATABASE IF NOT EXISTS {config['database']} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci")
35 | print(f"数据库 {config['database']} 创建成功")
36 |
37 | # 使用数据库
38 | cursor.execute(f"USE {config['database']}")
39 |
40 | # 读取并执行SQL文件
41 | sql_path = os.path.join(os.path.dirname(__file__), 'init.sql')
42 | with open(sql_path, 'r', encoding='utf-8') as f:
43 | sql_commands = f.read().split(';')
44 | for command in sql_commands:
45 | if command.strip():
46 | cursor.execute(command)
47 | connection.commit()
48 |
49 | print("数据库表创建成功")
50 | return # 成功后退出函数
51 |
52 | except Exception as e:
53 | print(f"初始化数据库失败: {str(e)}")
54 | raise
55 | finally:
56 | connection.close()
57 |
58 | except Exception as e:
59 | if attempt < max_retries - 1:
60 | print(f"连接失败: {str(e)}")
61 | print(f"等待 {retry_interval} 秒后重试...")
62 | time.sleep(retry_interval)
63 | else:
64 | print("达到最大重试次数,初始化失败")
65 | raise
66 |
67 | if __name__ == '__main__':
68 | try:
69 | init_database()
70 | print("数据库初始化成功")
71 | sys.exit(0)
72 | except Exception as e:
73 | print(f"数据库初始化失败: {str(e)}")
74 | sys.exit(1)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 停车票管理系统
2 |
3 | 一个基于 Flask + Vue + MySQL 的停车票管理系统,支持停车券的扫描、管理、分享和自动使用。
4 |
5 | ## 项目结构
6 | ```
7 | parking-system/
8 | ├── api/ # 后端 API 实现
9 | │ ├── app.py # Flask 主应用
10 | │ ├── auth.py # 认证相关
11 | │ ├── http_api.py # HTTP 请求封装
12 | │ ├── ocr_class.py # 二维码处理
13 | │ └── easy_park.py # 停车场业务逻辑
14 | ├── config/ # 配置文件
15 | │ └── config.yml # 主配置文件
16 | ├── db/ # 数据库相关文件
17 | │ ├── init.sql # 数据库初始化脚本
18 | │ └── mysql.cnf # MySQL 配置
19 | ├── frontend/ # 前端文件
20 | │ ├── index.html # 主页面
21 | │ ├── login.html # 登录页面
22 | │ └── register.html # 注册页面
23 | ├── scripts/ # 工具脚本
24 | ├── docker compose.yml # Docker 编排配置
25 | ├── Dockerfile # Docker 构建文件
26 | ├── run_api.py # 应用启动脚本
27 | └── requirements.txt # Python 依赖
28 | ```
29 |
30 | ## 功能特点
31 |
32 | - 🎫 停车券管理:支持扫描、存储和管理停车券
33 | - 📱 自动核销:支持定时自动领取和使用停车券
34 | - 🔄 批量处理:支持批量上传和处理停车券图片
35 | - 👥 分享功能:支持将停车券分享给其他用户
36 | - 🔍 查询功能:支持按时间、状态等条件查询停车券
37 | - 👤 用户管理:支持用户注册、登录、找回密码等功能
38 | - 📊 数据统计:提供停车券使用情况的统计信息
39 |
40 | ## 系统要求
41 |
42 | - Python 3.11+
43 | - MySQL 8.0+
44 | - Docker & Docker Compose
45 | - Node.js 14+ (仅开发环境需要)
46 |
47 | ## 快速开始
48 |
49 | ### 1. 克隆项目
50 | ```
51 | bash
52 | git clonehttps://github.com/monkey-wenjun/easy_park.git
53 | cd easy_park
54 | ```
55 | ### 3. 使用 Docker Compose 部署
56 |
57 | 1. 构建并启动服务:
58 | ```
59 |
60 | 2. 创建并配置 `config/config.yml`:
61 | ```yaml
62 | wechat:
63 | app_id: "你的小程序APPID"
64 | wx_open_id: "微信OpenID"
65 | headers:
66 | User-Agent: "Mozilla/5.0..."
67 | # 其他必要的请求头
68 | ```
69 |
70 | ### 3. 使用 Docker Compose 部署
71 |
72 | 1. 构建并启动服务:
73 | ```bash
74 | docker compose up -d --build
75 | ```
76 |
77 | 2. 检查服务状态:
78 | ```bash
79 | docker compose ps
80 | ```
81 |
82 | 3. 查看日志:
83 | ```bash
84 | docker compose logs -f
85 | ```
86 |
87 | ### 本地调试
88 |
89 | 1. 修改 `config/config.yml` 中的 `host` 为 `127.0.0.1`
90 | 2. 修改 `run_api.py` 中的 `app.run(host='0.0.0.0', port=8000, debug=False)` 为 `app.run(host='0.0.0.0', port=8000, debug=True)`
91 | 3. 执行 python3 run_api.py
92 |
93 | ### 4. 访问系统
94 |
95 | - Web 界面:`http://localhost:8000`
96 | - API 文档:`http://localhost:8000/api/docs`
97 |
98 | ## 开发指南
99 |
100 | ### 本地开发环境设置
101 |
102 | 1. 创建虚拟环境:
103 | ```bash
104 | python -m venv venv
105 | source venv/bin/activate # Linux/Mac
106 | venv\Scripts\activate # Windows
107 | ```
108 |
109 | 2. 安装依赖:
110 | ```bash
111 | pip install -r requirements.txt
112 | ```
113 |
114 | 3. 启动开发服务器:
115 | ```bash
116 | python run_api.py
117 | ```
118 |
119 | ### 系统监控
120 |
121 | 1. 检查系统状态:
122 | ```bash
123 | docker compose ps
124 | ```
125 |
126 | 2. 监控资源使用:
127 | ```bash
128 | docker stats
129 | ```
130 |
131 | ### 更新部署
132 |
133 | 1. 拉取最新代码:
134 | ```bash
135 | git pull origin main
136 | ```
137 |
138 | 2. 重新构建并启动:
139 | ```bash
140 | docker compose down
141 | docker compose up -d --build
142 | ```
143 |
144 | ## 常见问题
145 |
146 | 1. **Q: 容器启动失败怎么办?**
147 | A: 检查日志 `docker compose logs`,确认配置文件是否正确。
148 |
149 | 2. **Q: 如何修改端口号?**
150 | A: 修改 `docker compose.yml` 中的端口映射。
151 |
152 | 3. **Q: 数据库连接失败?**
153 | A: 检查 MySQL 容器状态和配置文件中的数据库连接信息。
154 |
155 | ## 安全注意事项
156 |
157 | 1. 确保修改默认的数据库密码
158 | 2. 定期更新系统和依赖包
159 | 3. 及时备份重要数据
160 | 4. 使用 HTTPS 进行安全传输
161 | 5. 定期检查系统日志
162 |
163 | ## 许可证
164 |
165 | MIT License
166 |
167 | ## 联系方式
168 |
169 | - 作者:阿文
170 | - Email:hsweib@gmail.com
171 | - 项目地址:[GitHub 仓库地址](https://github.com/monkey-wenjun/easy_park)
--------------------------------------------------------------------------------
/frontend/js/alert.js:
--------------------------------------------------------------------------------
1 | class CustomAlert {
2 | constructor() {
3 | // 等待 DOM 加载完成后再初始化
4 | if (document.readyState === 'loading') {
5 | document.addEventListener('DOMContentLoaded', () => this.init());
6 | } else {
7 | this.init();
8 | }
9 | }
10 |
11 | init() {
12 | if (document.querySelector('.custom-alert')) {
13 | return; // 已经初始化过,避免重复
14 | }
15 |
16 | // 创建弹窗容器
17 | this.alertBox = document.createElement('div');
18 | this.alertBox.className = 'custom-alert';
19 | this.alertBox.style.display = 'none';
20 |
21 | // 创建弹窗内容
22 | this.alertContent = document.createElement('div');
23 | this.alertContent.className = 'custom-alert-content';
24 |
25 | // 创建消息文本区域
26 | this.messageBox = document.createElement('div');
27 | this.messageBox.className = 'custom-alert-message';
28 |
29 | // 创建确认按钮
30 | this.confirmBtn = document.createElement('button');
31 | this.confirmBtn.className = 'custom-alert-button';
32 | this.confirmBtn.textContent = '确定';
33 |
34 | // 组装弹窗
35 | this.alertContent.appendChild(this.messageBox);
36 | this.alertContent.appendChild(this.confirmBtn);
37 | this.alertBox.appendChild(this.alertContent);
38 |
39 | // 添加到页面
40 | document.body.appendChild(this.alertBox);
41 |
42 | // 添加样式
43 | const style = document.createElement('style');
44 | style.textContent = `
45 | .custom-alert {
46 | position: fixed;
47 | top: 0;
48 | left: 0;
49 | width: 100%;
50 | height: 100%;
51 | background-color: rgba(0, 0, 0, 0.5);
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | z-index: 9999;
56 | }
57 |
58 | .custom-alert-content {
59 | background: white;
60 | padding: 20px;
61 | border-radius: 8px;
62 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
63 | min-width: 300px;
64 | max-width: 80%;
65 | text-align: center;
66 | }
67 |
68 | .custom-alert-message {
69 | margin-bottom: 20px;
70 | font-size: 16px;
71 | color: #333;
72 | line-height: 1.4;
73 | }
74 |
75 | .custom-alert-button {
76 | padding: 8px 30px;
77 | background-color: #409EFF;
78 | color: white;
79 | border: none;
80 | border-radius: 4px;
81 | cursor: pointer;
82 | font-size: 14px;
83 | transition: background-color 0.3s;
84 | }
85 |
86 | .custom-alert-button:hover {
87 | background-color: #66b1ff;
88 | }
89 |
90 | .custom-alert.show {
91 | display: flex;
92 | animation: fadeIn 0.3s;
93 | }
94 |
95 | @keyframes fadeIn {
96 | from { opacity: 0; }
97 | to { opacity: 1; }
98 | }
99 | `;
100 | document.head.appendChild(style);
101 | }
102 |
103 | show(message) {
104 | this.messageBox.textContent = message;
105 | this.alertBox.style.display = 'flex';
106 | this.alertBox.classList.add('show');
107 |
108 | return new Promise((resolve) => {
109 | const handleClick = () => {
110 | this.hide();
111 | this.confirmBtn.removeEventListener('click', handleClick);
112 | resolve();
113 | };
114 |
115 | this.confirmBtn.addEventListener('click', handleClick);
116 | });
117 | }
118 |
119 | hide() {
120 | this.alertBox.style.display = 'none';
121 | this.alertBox.classList.remove('show');
122 | }
123 |
124 | showAutoClose(message, duration = 2000) {
125 | this.messageBox.textContent = message;
126 | this.alertBox.style.display = 'flex';
127 | this.alertBox.classList.add('show');
128 | this.confirmBtn.style.display = 'none'; // 隐藏确认按钮
129 |
130 | return new Promise((resolve) => {
131 | setTimeout(() => {
132 | this.hide();
133 | this.confirmBtn.style.display = 'block'; // 恢复确认按钮显示
134 | resolve();
135 | }, duration);
136 | });
137 | }
138 | }
139 |
140 | // 等待 DOM 加载完成后再创建实例和重写 alert
141 | if (document.readyState === 'loading') {
142 | document.addEventListener('DOMContentLoaded', initCustomAlert);
143 | } else {
144 | initCustomAlert();
145 | }
146 |
147 | function initCustomAlert() {
148 | // 创建全局实例
149 | window.customAlert = new CustomAlert();
150 |
151 | // 重写原生 alert,添加第二个参数用于控制是否自动关闭
152 | window.alert = function(message, autoClose = false) {
153 | if (autoClose) {
154 | return window.customAlert.showAutoClose(message);
155 | }
156 | return window.customAlert.show(message);
157 | };
158 | }
--------------------------------------------------------------------------------
/db/init.sql:
--------------------------------------------------------------------------------
1 | -- 创建并设置数据库
2 | CREATE DATABASE IF NOT EXISTS parking CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3 | USE parking;
4 |
5 | -- 创建用户车辆记录表
6 | CREATE TABLE IF NOT EXISTS user_vehicle_records (
7 | id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
8 | username VARCHAR(255) NOT NULL COMMENT '用户名',
9 | password VARCHAR(255) NOT NULL COMMENT '密码',
10 | license_plate VARCHAR(20) NOT NULL COMMENT '车牌号',
11 | user_no VARCHAR(50) COMMENT '用户编号',
12 | openid VARCHAR(50) COMMENT '用户编号',
13 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
14 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
15 | is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0:未删除,1:已删除)',
16 | email VARCHAR(255) COMMENT '邮箱地址',
17 | email_verified TINYINT(1) DEFAULT 0 COMMENT '邮箱是否验证(0:未验证,1:已验证)'
18 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '用户车辆记录表';
19 |
20 | -- 创建券码记录表
21 | CREATE TABLE IF NOT EXISTS code_records (
22 | id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
23 | code_id VARCHAR(255) NOT NULL COMMENT '券码ID',
24 | code_no VARCHAR(255) NOT NULL COMMENT '券码编号',
25 | business_name VARCHAR(255) DEFAULT NULL COMMENT '商家名称',
26 | code_start_time DATETIME DEFAULT NULL COMMENT '券码开始时间',
27 | code_end_time DATETIME DEFAULT NULL COMMENT '券码结束时间',
28 | status TINYINT DEFAULT 0 COMMENT '状态(0:正常,1:已核销)',
29 | used_by VARCHAR(255) DEFAULT NULL COMMENT '使用人',
30 | used_time DATETIME DEFAULT NULL COMMENT '使用时间',
31 | verification_time DATETIME DEFAULT NULL COMMENT '核销时间',
32 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
33 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
34 | is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除'
35 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '券码记录表';
36 |
37 | -- 创建定时任务表
38 | CREATE TABLE IF NOT EXISTS schedule_tasks (
39 | id INT AUTO_INCREMENT PRIMARY KEY,
40 | username VARCHAR(50) NOT NULL,
41 | hour INT NOT NULL,
42 | minute INT NOT NULL,
43 | weekdays VARCHAR(20) NOT NULL,
44 | status TINYINT(1) DEFAULT 1,
45 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
46 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
47 | auto_collect TINYINT(1) DEFAULT 0 COMMENT '自动领取开关(0:关闭,1:开启)',
48 | auto_pay TINYINT(1) DEFAULT 0 COMMENT '自动支付开关(0:关闭,1:开启)',
49 | is_deleted TINYINT(1) DEFAULT 0
50 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
51 |
52 | -- 创建任务执行记录表
53 | CREATE TABLE IF NOT EXISTS schedule_execution_logs (
54 | id INT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
55 | task_id INT NOT NULL COMMENT '任务ID',
56 | execution_date DATE NOT NULL COMMENT '执行日期',
57 | execution_time DATETIME NOT NULL COMMENT '执行时间',
58 | status TINYINT DEFAULT 1 COMMENT '执行状态(1:成功,0:失败)',
59 | error_message TEXT DEFAULT NULL COMMENT '错误信息',
60 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
61 | UNIQUE KEY uk_task_date (task_id, execution_date) COMMENT '确保每个任务每天只执行一次'
62 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '任务执行记录表';
63 |
64 | -- 创建任务状态表
65 | CREATE TABLE IF NOT EXISTS task_status (
66 | task_id VARCHAR(36) PRIMARY KEY COMMENT '任务ID',
67 | status VARCHAR(20) NOT NULL COMMENT '任务状态(processing/completed/error)',
68 | message TEXT COMMENT '任务消息',
69 | results JSON COMMENT '处理结果',
70 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
71 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
72 | username VARCHAR(255) NOT NULL COMMENT '用户名'
73 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '任务状态表';
74 |
75 | -- 创建登录记录表
76 | CREATE TABLE IF NOT EXISTS login_logs (
77 | id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
78 | username VARCHAR(255) NOT NULL COMMENT '用户名',
79 | ip_address VARCHAR(50) NOT NULL COMMENT '登录IP',
80 | login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '登录时间',
81 | status TINYINT(1) DEFAULT 1 COMMENT '登录状态(0:失败,1:成功)',
82 | fail_reason VARCHAR(255) DEFAULT NULL COMMENT '失败原因',
83 | user_agent VARCHAR(500) DEFAULT NULL COMMENT '浏览器信息',
84 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
85 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '登录记录表';
86 |
87 | -- 创建分享记录表
88 | CREATE TABLE IF NOT EXISTS share_records (
89 | id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
90 | from_username VARCHAR(255) NOT NULL COMMENT '分享人用户名',
91 | to_username VARCHAR(255) NOT NULL COMMENT '被分享人用户名',
92 | code_ids TEXT NOT NULL COMMENT '分享的券码ID列表',
93 | share_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '分享时间',
94 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
95 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
96 | is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除(0:未删除,1:已删除)',
97 | INDEX idx_from_username (from_username),
98 | INDEX idx_to_username (to_username),
99 | INDEX idx_share_time (share_time)
100 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '分享记录表';
101 |
102 | -- 创建验证码表
103 | CREATE TABLE IF NOT EXISTS verification_codes (
104 | id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
105 | email VARCHAR(255) NOT NULL COMMENT '邮箱地址',
106 | code VARCHAR(6) NOT NULL COMMENT '验证码',
107 | type VARCHAR(20) NOT NULL COMMENT '验证码类型(register:注册,reset:重置密码)',
108 | expire_time TIMESTAMP NOT NULL COMMENT '过期时间',
109 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
110 | used TINYINT(1) DEFAULT 0 COMMENT '是否已使用(0:未使用,1:已使用)',
111 | INDEX idx_email_type (email, type),
112 | INDEX idx_expire_time (expire_time)
113 | ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '验证码表';
--------------------------------------------------------------------------------
/frontend/styles.css:
--------------------------------------------------------------------------------
1 | /* 整体容器样式 */
2 | #app {
3 | max-width: 1400px;
4 | margin: 0 auto;
5 | padding: 20px;
6 | }
7 |
8 | .el-header {
9 | background-color: #409EFF;
10 | color: white;
11 | text-align: center;
12 | padding: 0;
13 | border-radius: 4px 4px 0 0;
14 | }
15 |
16 | .el-header h2 {
17 | margin: 0;
18 | padding: 20px 0;
19 | font-size: 24px;
20 | }
21 |
22 | .el-main {
23 | padding: 20px;
24 | background-color: white;
25 | border-radius: 0 0 4px 4px;
26 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
27 | height: calc(100vh - 120px);
28 | overflow: hidden;
29 | }
30 |
31 | /* 设置页面背景色 */
32 | body {
33 | background-color: #f5f7fa;
34 | min-height: 100vh;
35 | margin: 0;
36 | padding: 20px;
37 | overflow-x: hidden;
38 | }
39 |
40 | .search-form {
41 | margin-bottom: 20px;
42 | background-color: #f5f7fa;
43 | padding: 30px 20px;
44 | border-radius: 4px;
45 | display: flex;
46 | flex-wrap: wrap;
47 | align-items: center;
48 | }
49 |
50 | .el-table {
51 | margin-top: 20px;
52 | flex: 1;
53 | }
54 |
55 | .el-form-item {
56 | margin-bottom: 0;
57 | margin-right: 20px;
58 | }
59 |
60 | /* 二维码容器样式 */
61 | [id^="qrcode-"] {
62 | padding: 10px;
63 | display: flex;
64 | justify-content: center;
65 | align-items: center;
66 | }
67 |
68 | [id^="qrcode-"] img {
69 | max-width: 100%;
70 | height: auto;
71 | }
72 |
73 | /* 操作按钮样式 */
74 | .el-button + .el-button {
75 | margin-left: 5px;
76 | }
77 |
78 | /* 日期选择器样式 */
79 | .el-date-picker {
80 | width: 200px;
81 | }
82 |
83 | /* 状态选择器样式 */
84 | .el-select {
85 | width: 120px;
86 | }
87 |
88 | /* 日期选择器中的日期标记样式 */
89 | .el-date-picker__cell.current:not(.disabled) {
90 | color: #409EFF;
91 | font-weight: bold;
92 | }
93 |
94 | /* 选中日期的样式 */
95 | .el-date-picker__cell.selected {
96 | background-color: #409EFF !important;
97 | color: white !important;
98 | }
99 |
100 | /* 今天日期的样式 */
101 | .el-date-picker__cell.today {
102 | color: #409EFF;
103 | font-weight: bold;
104 | }
105 |
106 | /* 有数据的日期标记样式 */
107 | .el-date-picker__cell.has-data {
108 | position: relative;
109 | }
110 |
111 | .el-date-picker__cell.has-data::after {
112 | content: '';
113 | position: absolute;
114 | bottom: 2px;
115 | left: 50%;
116 | transform: translateX(-50%);
117 | width: 4px;
118 | height: 4px;
119 | border-radius: 50%;
120 | background-color: #67C23A;
121 | }
122 |
123 | /* 表格容器样式 */
124 | .table-container {
125 | display: flex;
126 | flex-direction: column;
127 | height: calc(100vh - 250px);
128 | background-color: white;
129 | border-radius: 4px;
130 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
131 | }
132 |
133 | /* 空数据样式 */
134 | .el-table__empty-text {
135 | font-size: 16px;
136 | color: #909399;
137 | }
138 |
139 | /* 调整表单项的间距 */
140 | .search-form .el-form-item:last-child {
141 | margin-right: 0;
142 | }
143 |
144 | /* 确保搜索表单不会溢出 */
145 | @media screen and (max-width: 1400px) {
146 | .search-form {
147 | flex-wrap: wrap;
148 | justify-content: flex-start;
149 | }
150 |
151 | .search-form .el-form-item {
152 | margin-bottom: 10px;
153 | }
154 | }
155 |
156 | /* 日期选择器弹出层样式 */
157 | .custom-date-picker {
158 | background-color: white !important;
159 | border: 1px solid #DCDFE6;
160 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
161 | padding: 8px;
162 | }
163 |
164 | /* 日期时间输入框样式 */
165 | .custom-date-picker .el-date-picker__time-header {
166 | padding: 8px;
167 | border-bottom: 1px solid #EBEEF5;
168 | }
169 |
170 | .custom-date-picker .el-date-picker__editor-wrap {
171 | padding: 0 5px;
172 | }
173 |
174 | .custom-date-picker .el-input__wrapper {
175 | padding: 0 8px;
176 | }
177 |
178 | /* 日期选择器头部样式 */
179 | .custom-date-picker .el-date-picker__header {
180 | padding: 12px 12px 8px;
181 | margin: 0;
182 | }
183 |
184 | /* 日期面板内容样式 */
185 | .custom-date-picker .el-date-picker__content {
186 | padding: 8px 12px;
187 | width: auto;
188 | }
189 |
190 | /* 星期和日期格子样式 */
191 | .custom-date-picker .el-date-picker__body {
192 | padding: 0;
193 | }
194 |
195 | .custom-date-picker .el-picker-panel__content {
196 | margin: 0;
197 | padding: 0;
198 | }
199 |
200 | .custom-date-picker .el-date-table {
201 | width: 100%;
202 | table-layout: fixed;
203 | }
204 |
205 | .custom-date-picker .el-date-table th {
206 | padding: 8px 0;
207 | font-weight: 400;
208 | border-bottom: 1px solid #EBEEF5;
209 | }
210 |
211 | .custom-date-picker .el-date-table td {
212 | padding: 4px 0;
213 | height: 36px;
214 | }
215 |
216 | /* 底部按钮区域样式 */
217 | .custom-date-picker .el-picker-panel__footer {
218 | background-color: white;
219 | border-top: 1px solid #EBEEF5;
220 | padding: 8px 12px;
221 | text-align: right;
222 | }
223 |
224 | .custom-date-picker .el-picker-panel__footer .el-button {
225 | font-size: 14px;
226 | padding: 8px 16px;
227 | margin-left: 8px;
228 | }
229 |
230 | /* 时间选择器背景 */
231 | .custom-date-picker .el-date-picker__time-header,
232 | .custom-date-picker .el-picker-panel__content,
233 | .custom-date-picker .el-date-picker__header {
234 | background-color: white;
235 | }
236 |
237 | /* 确保所有内容对齐 */
238 | .custom-date-picker .el-date-picker__header,
239 | .custom-date-picker .el-picker-panel__content,
240 | .custom-date-picker .el-date-picker__time-header {
241 | width: auto;
242 | box-sizing: border-box;
243 | }
244 |
245 | /* 分页容器样式 */
246 | .pagination-container {
247 | margin-top: 20px;
248 | padding: 15px;
249 | display: flex;
250 | justify-content: flex-end;
251 | background-color: white;
252 | border-top: 1px solid #EBEEF5;
253 | }
254 |
255 | /* 分页组件样式 */
256 | .el-pagination {
257 | padding: 0;
258 | margin: 0;
259 | display: flex;
260 | align-items: center;
261 | }
262 |
263 | /* 分页按钮样式 */
264 | .el-pagination .btn-prev,
265 | .el-pagination .btn-next,
266 | .el-pagination .el-pager li {
267 | background-color: #f4f4f5;
268 | color: #606266;
269 | min-width: 32px;
270 | border-radius: 2px;
271 | margin: 0 5px;
272 | }
273 |
274 | .el-pagination .el-pagination__jump {
275 | margin-left: 16px;
276 | }
--------------------------------------------------------------------------------
/api/easy_park.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 | from api.http_api import ParkingAPI
3 | from api.ocr_class import QRCodeProcessor
4 | from api.notification import NotificationService
5 | import os
6 | import yaml
7 | from db import DatabaseManager
8 | from datetime import datetime
9 |
10 |
11 | class EasyPark:
12 | def __init__(self):
13 | # 加载配置文件
14 | config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'config.yml')
15 | with open(config_path, 'r', encoding='utf-8') as f:
16 | self.config = yaml.safe_load(f)
17 | self.db = DatabaseManager()
18 | self.qr = QRCodeProcessor()
19 | self.notification = NotificationService()
20 |
21 | def update_coupon_status(self, code_id: str, transaction_result: bool):
22 | """
23 | 更新优惠券状态
24 | :param code_id: 券码ID
25 | :param transaction_result: 交易结果
26 | """
27 | if transaction_result:
28 | try:
29 | self.db.update_status(code_id, 1) # 1 表示已核销
30 | except Exception as e:
31 | print(f"更新券码状态失败: {str(e)}")
32 |
33 | def process_payment(self, api, code_id: str, parking_no: str, park_order_no: str, code_no: str, task: dict, coupons=None):
34 | """
35 | 处理支付流程
36 | :param api:
37 | :param code_id: 券码ID
38 | :param parking_no: 停车场编号
39 | :param park_order_no: 订单编号
40 | :param code_no: 券码编号
41 | :param task: 当前执行的任务信息
42 | :param coupons: 是否已有优惠券
43 | :return: 使用的优惠券编号
44 | """
45 | if not coupons:
46 | # 领取优惠券
47 | api.create_code(code_id, parking_no, code_no)
48 | get_park_order = api.get_park_order(parking_no, park_order_no)
49 | coupons = get_park_order.get("data").get("coupons")
50 | else:
51 | print("有可用优惠券直接支付")
52 | use_code_no = coupons[0]["CouponRecord_No"]
53 | api.get_pay_pirce(parking_no, park_order_no, use_code_no)
54 | # 检查任务的 auto_pay 设置
55 | if task['auto_pay'] != 1:
56 | print("自动支付未开启,跳过支付步骤")
57 | return use_code_no
58 |
59 | transaction_result = api.on_pay_transactions(parking_no, park_order_no, use_code_no)
60 | print(f"transaction_result {transaction_result}")
61 | if transaction_result:
62 | self.update_coupon_status(code_id, transaction_result)
63 | return use_code_no
64 |
65 | def process_unused_coupons(self):
66 | """处理未使用的券码"""
67 | try:
68 | # 获取当前时间信息
69 | now = datetime.now()
70 | current_hour = now.hour
71 | current_minute = now.minute
72 | current_weekday = str(now.weekday()) # 0-6 表示周一到周日
73 | # 从 schedule_tasks 表获取符合当前时间的任务
74 | active_tasks = self.db.get_active_schedules()
75 | print(f"active_tasks {active_tasks}")
76 | if not active_tasks:
77 | print("没有找到符合条件的定时任务")
78 | return False
79 |
80 | processed = False
81 | for task in active_tasks:
82 | # 检查时间是否匹配
83 | user_info = self.db.get_user_by_username(task['username'])
84 | if (task['hour'] == current_hour and
85 | task['minute'] == current_minute and
86 | current_weekday in task['weekdays'].split(',')):
87 |
88 | # 获取任务用户的信息
89 | user = self.db.get_user_by_username(task['username'])
90 | if not user:
91 | print(f"未找到用户信息: {task['username']}")
92 | continue
93 | api = ParkingAPI(
94 | app_id=self.config['wechat']['app_id'],
95 | headers=self.config['wechat']['headers'],
96 | car_no=user_info['license_plate'],
97 | user_no=user_info['user_no'],
98 | openid=user_info['openid']
99 | )
100 | get_order = api.get_order()
101 | if get_order is False or get_order.get("data") is None:
102 | print("没有需要支付的订单")
103 | self.db.log_task_execution(
104 | task_id=task['id'],
105 | status=processed,
106 | error_message=None if processed else f"用户 {task['username']} 没有需要支付的订单"
107 | )
108 | return
109 | print(get_order)
110 | # 获取该用户未使用的券码
111 | unused_codes = self.db.get_unused_codes(task['username'])
112 | if not unused_codes:
113 | print(f"用户 {task['username']} 没有未使用的券码")
114 | self.db.log_task_execution(
115 | task_id=task['id'],
116 | status=processed,
117 | error_message=None if processed else f"用户 {task['username']} 没有未使用的券码"
118 | )
119 | return
120 | code = unused_codes[0]
121 | code_id = code['code_id']
122 | print(f"处理券码: {code_id}")
123 | # 判断券是否已核销
124 | query_code = api.query_code(code_id)
125 | if query_code is False:
126 | print(f"券码 {code_id} 已被领取")
127 | # 更新数据库状态
128 | self.db.update_status(code_id, 1)
129 | return False
130 | code_no = query_code.get("data", {}).get("no")
131 | # 获取券码相关信息
132 | parking_no = get_order.get("data").get("ParkOrder_ParkNo")
133 |
134 | # 获取订单信息
135 | park_order_no = get_order.get("data", {}).get("ParkOrder_No")
136 | get_park_order = api.get_park_order(parking_no, park_order_no)
137 | coupons = get_park_order.get("data", {}).get("coupons")
138 | try:
139 | # 处理支付
140 | if coupons:
141 | use_code_no = self.process_payment(api, code_id, parking_no, park_order_no, code_no, task, coupons=coupons)
142 | else:
143 | use_code_no = self.process_payment(api, code_id, parking_no, park_order_no, code_no, task)
144 | # 发送通知
145 | self.notification.send_bark_notification(f"停车券已领取并核销,券码{use_code_no}")
146 | # 记录任务执行
147 | processed = True
148 | self.db.log_task_execution(
149 | task_id=task['id'],
150 | status=processed,
151 | error_message=None if processed else "没有成功核销的券码"
152 | )
153 | except Exception as e:
154 | print(f"处理券码 {code_id} 失败: {str(e)}")
155 | self.db.log_task_execution(
156 | task_id=task['id'],
157 | status=processed,
158 | error_message=None if processed else f"处理券码 {code_id} 失败: {str(e)}"
159 | )
160 | return processed
161 |
162 | except Exception as e:
163 | print(f"处理未使用券码失败: {str(e)}")
164 | return False
165 |
166 | def main(self):
167 | self.process_unused_coupons()
168 |
169 |
170 | if __name__ == '__main__':
171 | easy = EasyPark()
172 | easy.main()
173 |
--------------------------------------------------------------------------------
/config/sr.prototxt:
--------------------------------------------------------------------------------
1 | layer {
2 | name: "data"
3 | type: "Input"
4 | top: "data"
5 | input_param {
6 | shape {
7 | dim: 1
8 | dim: 1
9 | dim: 224
10 | dim: 224
11 | }
12 | }
13 | }
14 | layer {
15 | name: "conv0"
16 | type: "Convolution"
17 | bottom: "data"
18 | top: "conv0"
19 | param {
20 | lr_mult: 1.0
21 | decay_mult: 1.0
22 | }
23 | param {
24 | lr_mult: 1.0
25 | decay_mult: 0.0
26 | }
27 | convolution_param {
28 | num_output: 32
29 | bias_term: true
30 | pad: 1
31 | kernel_size: 3
32 | group: 1
33 | stride: 1
34 | weight_filler {
35 | type: "msra"
36 | }
37 | }
38 | }
39 | layer {
40 | name: "conv0/lrelu"
41 | type: "ReLU"
42 | bottom: "conv0"
43 | top: "conv0"
44 | relu_param {
45 | negative_slope: 0.05000000074505806
46 | }
47 | }
48 | layer {
49 | name: "db1/reduce"
50 | type: "Convolution"
51 | bottom: "conv0"
52 | top: "db1/reduce"
53 | param {
54 | lr_mult: 1.0
55 | decay_mult: 1.0
56 | }
57 | param {
58 | lr_mult: 1.0
59 | decay_mult: 0.0
60 | }
61 | convolution_param {
62 | num_output: 8
63 | bias_term: true
64 | pad: 0
65 | kernel_size: 1
66 | group: 1
67 | stride: 1
68 | weight_filler {
69 | type: "msra"
70 | }
71 | }
72 | }
73 | layer {
74 | name: "db1/reduce/lrelu"
75 | type: "ReLU"
76 | bottom: "db1/reduce"
77 | top: "db1/reduce"
78 | relu_param {
79 | negative_slope: 0.05000000074505806
80 | }
81 | }
82 | layer {
83 | name: "db1/3x3"
84 | type: "Convolution"
85 | bottom: "db1/reduce"
86 | top: "db1/3x3"
87 | param {
88 | lr_mult: 1.0
89 | decay_mult: 1.0
90 | }
91 | param {
92 | lr_mult: 1.0
93 | decay_mult: 0.0
94 | }
95 | convolution_param {
96 | num_output: 8
97 | bias_term: true
98 | pad: 1
99 | kernel_size: 3
100 | group: 8
101 | stride: 1
102 | weight_filler {
103 | type: "msra"
104 | }
105 | }
106 | }
107 | layer {
108 | name: "db1/3x3/lrelu"
109 | type: "ReLU"
110 | bottom: "db1/3x3"
111 | top: "db1/3x3"
112 | relu_param {
113 | negative_slope: 0.05000000074505806
114 | }
115 | }
116 | layer {
117 | name: "db1/1x1"
118 | type: "Convolution"
119 | bottom: "db1/3x3"
120 | top: "db1/1x1"
121 | param {
122 | lr_mult: 1.0
123 | decay_mult: 1.0
124 | }
125 | param {
126 | lr_mult: 1.0
127 | decay_mult: 0.0
128 | }
129 | convolution_param {
130 | num_output: 32
131 | bias_term: true
132 | pad: 0
133 | kernel_size: 1
134 | group: 1
135 | stride: 1
136 | weight_filler {
137 | type: "msra"
138 | }
139 | }
140 | }
141 | layer {
142 | name: "db1/1x1/lrelu"
143 | type: "ReLU"
144 | bottom: "db1/1x1"
145 | top: "db1/1x1"
146 | relu_param {
147 | negative_slope: 0.05000000074505806
148 | }
149 | }
150 | layer {
151 | name: "db1/concat"
152 | type: "Concat"
153 | bottom: "conv0"
154 | bottom: "db1/1x1"
155 | top: "db1/concat"
156 | concat_param {
157 | axis: 1
158 | }
159 | }
160 | layer {
161 | name: "db2/reduce"
162 | type: "Convolution"
163 | bottom: "db1/concat"
164 | top: "db2/reduce"
165 | param {
166 | lr_mult: 1.0
167 | decay_mult: 1.0
168 | }
169 | param {
170 | lr_mult: 1.0
171 | decay_mult: 0.0
172 | }
173 | convolution_param {
174 | num_output: 8
175 | bias_term: true
176 | pad: 0
177 | kernel_size: 1
178 | group: 1
179 | stride: 1
180 | weight_filler {
181 | type: "msra"
182 | }
183 | }
184 | }
185 | layer {
186 | name: "db2/reduce/lrelu"
187 | type: "ReLU"
188 | bottom: "db2/reduce"
189 | top: "db2/reduce"
190 | relu_param {
191 | negative_slope: 0.05000000074505806
192 | }
193 | }
194 | layer {
195 | name: "db2/3x3"
196 | type: "Convolution"
197 | bottom: "db2/reduce"
198 | top: "db2/3x3"
199 | param {
200 | lr_mult: 1.0
201 | decay_mult: 1.0
202 | }
203 | param {
204 | lr_mult: 1.0
205 | decay_mult: 0.0
206 | }
207 | convolution_param {
208 | num_output: 8
209 | bias_term: true
210 | pad: 1
211 | kernel_size: 3
212 | group: 8
213 | stride: 1
214 | weight_filler {
215 | type: "msra"
216 | }
217 | }
218 | }
219 | layer {
220 | name: "db2/3x3/lrelu"
221 | type: "ReLU"
222 | bottom: "db2/3x3"
223 | top: "db2/3x3"
224 | relu_param {
225 | negative_slope: 0.05000000074505806
226 | }
227 | }
228 | layer {
229 | name: "db2/1x1"
230 | type: "Convolution"
231 | bottom: "db2/3x3"
232 | top: "db2/1x1"
233 | param {
234 | lr_mult: 1.0
235 | decay_mult: 1.0
236 | }
237 | param {
238 | lr_mult: 1.0
239 | decay_mult: 0.0
240 | }
241 | convolution_param {
242 | num_output: 32
243 | bias_term: true
244 | pad: 0
245 | kernel_size: 1
246 | group: 1
247 | stride: 1
248 | weight_filler {
249 | type: "msra"
250 | }
251 | }
252 | }
253 | layer {
254 | name: "db2/1x1/lrelu"
255 | type: "ReLU"
256 | bottom: "db2/1x1"
257 | top: "db2/1x1"
258 | relu_param {
259 | negative_slope: 0.05000000074505806
260 | }
261 | }
262 | layer {
263 | name: "db2/concat"
264 | type: "Concat"
265 | bottom: "db1/concat"
266 | bottom: "db2/1x1"
267 | top: "db2/concat"
268 | concat_param {
269 | axis: 1
270 | }
271 | }
272 | layer {
273 | name: "upsample/reduce"
274 | type: "Convolution"
275 | bottom: "db2/concat"
276 | top: "upsample/reduce"
277 | param {
278 | lr_mult: 1.0
279 | decay_mult: 1.0
280 | }
281 | param {
282 | lr_mult: 1.0
283 | decay_mult: 0.0
284 | }
285 | convolution_param {
286 | num_output: 32
287 | bias_term: true
288 | pad: 0
289 | kernel_size: 1
290 | group: 1
291 | stride: 1
292 | weight_filler {
293 | type: "msra"
294 | }
295 | }
296 | }
297 | layer {
298 | name: "upsample/reduce/lrelu"
299 | type: "ReLU"
300 | bottom: "upsample/reduce"
301 | top: "upsample/reduce"
302 | relu_param {
303 | negative_slope: 0.05000000074505806
304 | }
305 | }
306 | layer {
307 | name: "upsample/deconv"
308 | type: "Deconvolution"
309 | bottom: "upsample/reduce"
310 | top: "upsample/deconv"
311 | param {
312 | lr_mult: 1.0
313 | decay_mult: 1.0
314 | }
315 | param {
316 | lr_mult: 1.0
317 | decay_mult: 0.0
318 | }
319 | convolution_param {
320 | num_output: 32
321 | bias_term: true
322 | pad: 1
323 | kernel_size: 3
324 | group: 32
325 | stride: 2
326 | weight_filler {
327 | type: "msra"
328 | }
329 | }
330 | }
331 | layer {
332 | name: "upsample/lrelu"
333 | type: "ReLU"
334 | bottom: "upsample/deconv"
335 | top: "upsample/deconv"
336 | relu_param {
337 | negative_slope: 0.05000000074505806
338 | }
339 | }
340 | layer {
341 | name: "upsample/rec"
342 | type: "Convolution"
343 | bottom: "upsample/deconv"
344 | top: "upsample/rec"
345 | param {
346 | lr_mult: 1.0
347 | decay_mult: 1.0
348 | }
349 | param {
350 | lr_mult: 1.0
351 | decay_mult: 0.0
352 | }
353 | convolution_param {
354 | num_output: 1
355 | bias_term: true
356 | pad: 0
357 | kernel_size: 1
358 | group: 1
359 | stride: 1
360 | weight_filler {
361 | type: "msra"
362 | }
363 | }
364 | }
365 | layer {
366 | name: "nearest"
367 | type: "Deconvolution"
368 | bottom: "data"
369 | top: "nearest"
370 | param {
371 | lr_mult: 0.0
372 | decay_mult: 0.0
373 | }
374 | convolution_param {
375 | num_output: 1
376 | bias_term: false
377 | pad: 0
378 | kernel_size: 2
379 | group: 1
380 | stride: 2
381 | weight_filler {
382 | type: "constant"
383 | value: 1.0
384 | }
385 | }
386 | }
387 | layer {
388 | name: "Crop1"
389 | type: "Crop"
390 | bottom: "nearest"
391 | bottom: "upsample/rec"
392 | top: "Crop1"
393 | }
394 | layer {
395 | name: "fc"
396 | type: "Eltwise"
397 | bottom: "Crop1"
398 | bottom: "upsample/rec"
399 | top: "fc"
400 | eltwise_param {
401 | operation: SUM
402 | }
403 | }
404 |
--------------------------------------------------------------------------------
/frontend/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
19 | 登录 - 停车票管理系统
20 |
163 |
164 |
165 |
166 |
停车票管理系统
167 |
193 |
194 |
195 |
196 |
246 |
--------------------------------------------------------------------------------
/api/http_api.py:
--------------------------------------------------------------------------------
1 | from random import randint, choice
2 | import string
3 | from requests import post, get
4 | import os
5 | import yaml
6 |
7 | class ParkingAPI:
8 | """停车场 API 接口类,用于处理所有与停车场服务器的 HTTP 请求"""
9 |
10 | def __init__(self, app_id=None, headers=None, car_no=None, user_no=None, openid=None):
11 | """
12 | 初始化停车场 API 客户端
13 | :param app_id: 微信小程序的 APP ID
14 | :param headers: HTTP 请求头
15 | :param car_no: 车牌号码
16 | :param user_no: 用户编号
17 | :param openid: 微信开放平台 ID
18 | """
19 | # 从配置文件加载默认值
20 | config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', 'config.yml')
21 | with open(config_path, 'r') as f:
22 | config = yaml.safe_load(f)
23 | wechat_config = config.get('wechat', {})
24 |
25 | self.app_id = app_id or wechat_config.get('app_id')
26 | self.headers = headers or wechat_config.get('headers', {})
27 | self.car_no = car_no
28 | self.user_no = user_no
29 | self.openid = openid
30 |
31 | @staticmethod
32 | def generate_random_float():
33 | """
34 | 生成随机浮点数
35 | :return: 17位精度的随机浮点数字符串
36 | """
37 | num1 = randint(1, 10)
38 | num2 = randint(1, 99)
39 | result = num1 / num2
40 | formatted_result = "{:.17f}".format(result)
41 | return formatted_result
42 |
43 | @staticmethod
44 | def generate_random_string(length):
45 | """
46 | 生成指定长度的随机字符串
47 | :param length: 需要生成的字符串长度
48 | :return: 包含数字和字母的随机字符串
49 | """
50 | all_characters = string.digits + string.ascii_uppercase + string.ascii_lowercase
51 | random_list = [choice(all_characters) for _ in range(length)]
52 | random_string = ''.join(random_list)
53 | return random_string
54 |
55 | def login(self):
56 | """
57 | 登录接口
58 | :return: 成功返回登录响应信息,失败返回 False
59 | """
60 | t_value = self.generate_random_float()
61 | url = f"https://parkingczsmall.ymlot.cn/OAuth/InitConfig?t={t_value}"
62 | data = {"appid": self.app_id}
63 | response = post(url, headers=self.headers, json=data)
64 |
65 | if response.status_code == 200:
66 | resp_json = response.json()
67 | print(resp_json)
68 | if resp_json.get("msg") == "ok":
69 | return resp_json
70 | return False
71 |
72 | def create_code(self, token, park_no, code_no):
73 | """
74 | 领取停车优惠券
75 | :param token: 登录令牌
76 | :param park_no: 停车场编号
77 | :param code_no: 优惠券编号
78 | :return: 成功返回优惠券信息,失败返回 False
79 | """
80 | t_value = self.generate_random_float()
81 | url = f"https://parkingczsmall.ymlot.cn/Business/CollectCoupon?t={t_value}"
82 | data = {
83 | "parkno": park_no,
84 | "carno": self.car_no,
85 | "solutionno": code_no,
86 | "token": token,
87 | "t": "3",
88 | "openid": self.openid,
89 | "appid": self.app_id,
90 | "usersno": self.user_no,
91 | "phone": ""
92 | }
93 | response = post(url, headers=self.headers, json=data)
94 |
95 | if response.status_code == 200:
96 | resp_json = response.json()
97 | print(resp_json)
98 | if resp_json.get("msg") == "ok":
99 | return resp_json
100 | return False
101 |
102 | def get_user_coupon_list(self):
103 | t_value = self.generate_random_float()
104 | url = f"https://parkingczsmall.ymlot.cn/Order/GetUserCouponList?t={t_value}"
105 | data = {
106 | "status": "",
107 | "pageIndex": 1,
108 | "pageSize": 10,
109 | "appid": self.app_id,
110 | "usersno": self.user_no,
111 | "phone": ""
112 | }
113 | response = post(url, headers=self.headers, json=data)
114 |
115 | if response.status_code == 200:
116 | resp_json = response.json()
117 | if resp_json.get("msg") == "ok":
118 | return resp_json
119 | return False
120 |
121 | def query_code(self, code_id):
122 | """
123 | 查询优惠券使用状态
124 | :param code_id: 优惠券 ID
125 | :return: 成功返回优惠券状态信息,失败返回 False
126 | """
127 | t_value = self.generate_random_float()
128 | generate_random_string_code = self.generate_random_string(32)
129 | url = f"https://parkingczsmall.ymlot.cn/Business/GetCouponSolutionByQrCode?t={t_value}"
130 | data = {
131 | "d": code_id,
132 | "code": generate_random_string_code,
133 | "appid": self.app_id,
134 | "usersno": self.user_no,
135 | "phone": "",
136 | }
137 | response = post(url, headers=self.headers, json=data)
138 |
139 | if response.status_code == 200:
140 | resp_json = response.json()
141 | if resp_json.get("msg") == "ok":
142 | return resp_json
143 | return False
144 |
145 | def get_order(self):
146 | """
147 | 获取车辆订单信息
148 | :return: 成功返回订单信息,失败返回 False
149 | """
150 | t_value = self.generate_random_float()
151 | url = f"https://parkingczsmall.ymlot.cn/Pay/GetOrderCarNo?t={t_value}"
152 | data = {
153 | "parkno": "",
154 | "carno": self.car_no,
155 | "appid": self.app_id,
156 | "usersno": self.user_no,
157 | "phone": ""
158 | }
159 | print(f"query_code {data}")
160 | response = post(url, headers=self.headers, json=data)
161 |
162 | if response.status_code == 200:
163 | resp_json = response.json()
164 | print(resp_json)
165 | if resp_json.get("msg") == "ok":
166 | return resp_json
167 | return False
168 |
169 | def get_park_order(self, park_no, order_no):
170 | """
171 | 获取停车场订单详细信息
172 | :param park_no: 停车场编号
173 | :param order_no: 订单编号
174 | :return: 成功返回订单详细信息,失败返回 False
175 | """
176 | t_value = self.generate_random_float()
177 | url = f"https://parkingczsmall.ymlot.cn/Pay/GetParkOrder?t={t_value}"
178 | data = {
179 | "parkno": park_no,
180 | "orderno": order_no,
181 | "appid": self.app_id,
182 | "usersno": self.user_no,
183 | "phone": ""
184 | }
185 | response = post(url, headers=self.headers, json=data)
186 |
187 | if response.status_code == 200:
188 | resp_json = response.json()
189 | if resp_json.get("msg") == "ok":
190 | print(resp_json)
191 | return resp_json
192 | return False
193 |
194 | def get_pay_pirce(self, park_no, order_no, code_no):
195 | """
196 | 获取支付金额信息
197 | :param park_no: 停车场编号
198 | :param order_no: 订单编号
199 | :param code_no: 优惠券编号
200 | :return: 成功返回支付金额信息,失败返回 False
201 | """
202 | t_value = self.generate_random_float()
203 | url = f"https://parkingczsmall.ymlot.cn/Pay/GetPayPirce?t={t_value}"
204 | data = {
205 | "parkno": park_no,
206 | "orderno": order_no,
207 | "coupons": f"[\"{code_no}\"]",
208 | "appid": self.app_id,
209 | "usersno": self.user_no,
210 | "phone": ""
211 | }
212 | print(f"query_code {data}")
213 | response = post(url, headers=self.headers, json=data)
214 |
215 | if response.status_code == 200:
216 | resp_json = response.json()
217 | if resp_json.get("msg") == "ok":
218 | print(resp_json)
219 | return resp_json
220 | return False
221 |
222 | def on_pay_transactions(self, park_no, order_no, code_no):
223 | """
224 | 执行支付交易
225 | :param park_no: 停车场编号
226 | :param order_no: 订单编号
227 | :param code_no: 优惠券编号
228 | :return: 成功返回支付结果,失败返回 False
229 | """
230 | t_value = self.generate_random_float()
231 | url = f"https://parkingczsmall.ymlot.cn/Pay/OnPayTransactions?t={t_value}"
232 | data = {
233 | "parkno": park_no,
234 | "passwayno": "",
235 | "orderno": order_no,
236 | "coupons": f"[\"{code_no}\"]",
237 | "qforderno": "[]",
238 | "appid": self.app_id,
239 | "usersno": self.user_no,
240 | "phone": ""
241 | }
242 | print(f"query_code {data}")
243 | response = post(url, headers=self.headers, json=data)
244 | print(response.json())
245 | if response.status_code == 200:
246 | resp_json = response.json()
247 | if resp_json.get("msg") == "支付成功":
248 | return True
249 | return False
--------------------------------------------------------------------------------
/frontend/reset_password.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 找回密码 - 停车票管理系统
8 |
147 |
148 |
149 |
182 |
183 |
262 |
263 |
--------------------------------------------------------------------------------
/api/ocr_class.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import os
3 | import glob
4 | import re
5 | import numpy as np
6 | from pyzbar import pyzbar
7 | import qrcode
8 |
9 | class QRCodeProcessor:
10 | """二维码处理类,用于处理图像识别和二维码相关操作"""
11 |
12 | def __init__(self, config_path=None):
13 | """
14 | 初始化二维码处理器
15 | :param config_path: 配置文件路径
16 | """
17 | if config_path is None:
18 | # 使用项目根目录下的 config 路径
19 | current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 | config_path = os.path.join(current_dir, 'config')
21 |
22 | # 确保配置文件存在
23 | self.depro = os.path.join(config_path, 'detect.prototxt')
24 | self.decaf = os.path.join(config_path, 'detect.caffemodel')
25 | self.srpro = os.path.join(config_path, 'sr.prototxt')
26 | self.srcaf = os.path.join(config_path, 'sr.caffemodel')
27 |
28 | # 检查文件是否存在
29 | required_files = [
30 | (self.depro, 'detect.prototxt'),
31 | (self.decaf, 'detect.caffemodel'),
32 | (self.srpro, 'sr.prototxt'),
33 | (self.srcaf, 'sr.caffemodel')
34 | ]
35 |
36 | for file_path, file_name in required_files:
37 | if not os.path.exists(file_path):
38 | raise FileNotFoundError(f"找不到必要的配置文件: {file_name}")
39 |
40 | def detect_and_decode_qrcodes(self, image_path_list):
41 | """
42 | 检测和解码图片中的二维码
43 | :param image_path_list: 图片路径列表
44 | :return: 识别到的二维码列表
45 | """
46 | qrcodes_list = []
47 | for image_path in image_path_list:
48 | # 加载原始图像
49 | image = cv2.imread(image_path)
50 | file_name = str(image_path).split("/")[-1]
51 | all_qr_codes = []
52 | marked_image = image.copy() # 创建原始图像的副本用于标记
53 |
54 | # 1. 增加更多的缩放比例
55 | scale_factors = [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 2.5]
56 |
57 | # 2. 添加图像预处理步骤
58 | for scale in scale_factors:
59 | resized_image = cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR)
60 |
61 | # 增加对比度
62 | lab = cv2.cvtColor(resized_image, cv2.COLOR_BGR2LAB)
63 | l, a, b = cv2.split(lab)
64 | clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
65 | cl = clahe.apply(l)
66 | enhanced_image = cv2.merge((cl,a,b))
67 | enhanced_image = cv2.cvtColor(enhanced_image, cv2.COLOR_LAB2BGR)
68 |
69 | # 使用不同的阈值进行二值化处理
70 | gray = cv2.cvtColor(enhanced_image, cv2.COLOR_BGR2GRAY)
71 | thresholds = [cv2.THRESH_BINARY, cv2.THRESH_BINARY_INV]
72 | for threshold in thresholds:
73 | _, binary = cv2.threshold(gray, 127, 255, threshold)
74 |
75 | # 3. 尝试多种解码方法
76 | # 使用pyzbar
77 | decoded_pyzbar = pyzbar.decode(binary)
78 | for obj in decoded_pyzbar:
79 | qr_data = obj.data.decode('utf-8')
80 | if qr_data not in all_qr_codes:
81 | all_qr_codes.append(qr_data)
82 | # 在原始大小的图像上标记
83 | rect_points = obj.rect
84 | x, y = rect_points.left, rect_points.top
85 | w, h = rect_points.width, rect_points.height
86 | # 根据缩放比例调整坐标
87 | x, y = int(x/scale), int(y/scale)
88 | w, h = int(w/scale), int(h/scale)
89 | cv2.rectangle(marked_image, (x, y), (x + w, y + h), (0, 0, 255), 5)
90 | cv2.putText(marked_image, f"#{len(all_qr_codes)}",
91 | (x, y-10), cv2.FONT_HERSHEY_SIMPLEX,
92 | 1, (0, 0, 255), 2)
93 |
94 | # 使用WeChatQRCode检测器
95 | detector = cv2.wechat_qrcode.WeChatQRCode(self.depro, self.decaf, self.srpro, self.srcaf)
96 |
97 | # 尝试不同的图像处理方法
98 | processing_methods = [
99 | lambda img: img, # 原始图像
100 | lambda img: cv2.convertScaleAbs(img, alpha=1.2, beta=10), # 增加亮度
101 | lambda img: cv2.convertScaleAbs(img, alpha=0.8, beta=-10), # 降低亮度
102 | lambda img: cv2.filter2D(img, -1, np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])), # 锐化
103 | lambda img: cv2.GaussianBlur(img, (3, 3), 0), # 高斯模糊
104 | ]
105 |
106 | for process in processing_methods:
107 | try:
108 | processed_img = process(binary)
109 | if len(processed_img.shape) == 2: # 如果是灰度图
110 | processed_img = cv2.cvtColor(processed_img, cv2.COLOR_GRAY2BGR)
111 |
112 | res, points = detector.detectAndDecode(processed_img)
113 | for i, qr_data in enumerate(res):
114 | if qr_data not in all_qr_codes:
115 | all_qr_codes.append(qr_data)
116 | if points is not None and len(points) > i:
117 | point = points[i]
118 | x = int(min(point[:, 0])/scale)
119 | y = int(min(point[:, 1])/scale)
120 | w = int(max(point[:, 0])/scale - x)
121 | h = int(max(point[:, 1])/scale - y)
122 | cv2.rectangle(marked_image, (x, y), (x + w, y + h), (0, 0, 255), 5)
123 | text = f"#{len(all_qr_codes)}"
124 | cv2.putText(marked_image, text,
125 | (x, y-10), cv2.FONT_HERSHEY_SIMPLEX,
126 | 1, (0, 0, 255), 2)
127 | except Exception as e:
128 | print(f"处理方法出错: {str(e)}")
129 | continue
130 |
131 | # 确保输出目录存在
132 | output_dir = '../parking/output_file'
133 | os.makedirs(output_dir, exist_ok=True)
134 |
135 | # 保存标记后的图像
136 | output_image_path = os.path.join(output_dir, f'marked_{file_name}')
137 | cv2.imwrite(output_image_path, marked_image)
138 |
139 | print(f"标记后的图像已保存到: {output_image_path}")
140 | print("识别到的二维码内容:")
141 | for idx, code in enumerate(all_qr_codes, 1):
142 | print(f"二维码 {idx}: {code}")
143 | qrcodes_list.append(code)
144 |
145 | return qrcodes_list
146 |
147 | def singe_detect_and_decode_qrcodes(self, image_path_list):
148 | """
149 | 单独检测和解码图片中的二维码
150 | :param image_path_list: 图片路径列表
151 | :return: 识别到的二维码列表
152 | """
153 | qrcodes_list = []
154 | for image_path in image_path_list:
155 | img = cv2.imread(image_path)
156 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
157 | detector = cv2.wechat_qrcode.WeChatQRCode(self.depro, self.decaf, self.srpro, self.srcaf)
158 | res, points = detector.detectAndDecode(gray)
159 | for pos in points:
160 | color = (0, 0, 255)
161 | thick = 3
162 | for p in [(0, 1), (1, 2), (2, 3), (3, 0)]:
163 | start = int(pos[p[0]][0]), int(pos[p[0]][1])
164 | end = int(pos[p[1]][0]), int(pos[p[1]][1])
165 | cv2.line(img, start, end, color, thick)
166 | for i in res:
167 | qrcodes_list.append(str(i))
168 | return qrcodes_list
169 |
170 | @staticmethod
171 | def get_image_list(directory):
172 | """
173 | 获取目录下的所有图片文件
174 | :param directory: 目录路径
175 | :return: 图片文件路径列表
176 | """
177 | image_types = ['jpg', 'jpeg', 'png']
178 | image_list = []
179 | image_list_suffix = []
180 | for image_type in image_types:
181 | pattern = os.path.join(directory, f'*.{image_type}')
182 | image_list.extend(glob.glob(pattern))
183 | for img in image_list:
184 | if not re.search("_", os.path.basename(img)):
185 | image_list_suffix.append(img)
186 | return image_list_suffix
187 |
188 | def process_parking_images(self, parking_directory):
189 | """
190 | 处理停车场图片并生成二维码
191 | :param parking_directory: 停车场图片目录
192 | :return: 处理后的二维码ID列表
193 | """
194 | get_image_list = self.get_image_list(parking_directory)
195 | return self.detect_and_decode_qrcodes(get_image_list)
196 |
197 | @staticmethod
198 | def save_qrcode(code_id, file_path):
199 | """
200 | 保存二维码图片
201 | :param code_id: 二维码内容
202 | :param file_path: 保存路径
203 | """
204 | img = qrcode.make(data=code_id)
205 | with open(file_path, 'wb') as f:
206 | img.save(f)
207 |
--------------------------------------------------------------------------------
/frontend/register.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 停车系统 - 注册
7 |
135 |
136 |
137 |
177 |
178 |
179 |
296 |
297 |
--------------------------------------------------------------------------------
/frontend/app.js:
--------------------------------------------------------------------------------
1 | const { createApp, ref, reactive, nextTick } = Vue;
2 |
3 | // 确保 ElementPlus 和 ElementPlusLocaleZhCn 已定义
4 | if (typeof ElementPlus === 'undefined') {
5 | throw new Error('ElementPlus is not loaded');
6 | }
7 | if (typeof ElementPlusLocaleZhCn === 'undefined') {
8 | throw new Error('ElementPlusLocaleZhCn is not loaded');
9 | }
10 |
11 | const app = createApp({
12 | setup() {
13 | const records = ref([]);
14 | const loading = ref(false);
15 | const currentPage = ref(1);
16 | const pageSize = ref(5);
17 | const total = ref(0);
18 | const searchForm = reactive({
19 | startTime: '',
20 | endTime: '',
21 | status: ''
22 | });
23 |
24 | // 添加时间选择器的选项
25 | const timeOptions = {
26 | hours: Array.from({ length: 24 }, (_, i) => ({
27 | value: i,
28 | label: String(i).padStart(2, '0')
29 | })),
30 | minutes: Array.from({ length: 60 }, (_, i) => ({
31 | value: i,
32 | label: String(i).padStart(2, '0')
33 | }))
34 | };
35 |
36 | // 格式化时间的函数
37 | const formatDateTime = (dateStr) => {
38 | if (!dateStr) return '';
39 | const date = new Date(dateStr + '+08:00');
40 | const year = date.getFullYear();
41 | const month = String(date.getMonth() + 1).padStart(2, '0');
42 | const day = String(date.getDate()).padStart(2, '0');
43 | const hours = String(date.getHours()).padStart(2, '0');
44 | const minutes = String(date.getMinutes()).padStart(2, '0');
45 | const seconds = String(date.getSeconds()).padStart(2, '0');
46 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
47 | };
48 |
49 | // 生成二维码
50 | const generateQRCodes = async () => {
51 | await nextTick();
52 | records.value.forEach((record, index) => {
53 | const container = document.getElementById(`qrcode-${index}`);
54 | if (container && !container.hasChildNodes()) {
55 | container.innerHTML = ''; // 清除可能存在的旧二维码
56 | new QRCode(container, {
57 | text: record.code_id,
58 | width: 128,
59 | height: 128,
60 | colorDark: "#000000",
61 | colorLight: "#ffffff",
62 | correctLevel: QRCode.CorrectLevel.H
63 | });
64 | }
65 | });
66 | };
67 |
68 | // 状态选项
69 | const statusOptions = [
70 | { label: '正常', value: '0' },
71 | { label: '已核销', value: '1' }
72 | ];
73 |
74 | // 获取状态显示文本
75 | const getStatusLabel = (value) => {
76 | const option = statusOptions.find(opt => opt.value === value);
77 | return option ? option.label : '全部';
78 | };
79 |
80 | const searchRecords = async () => {
81 | loading.value = true;
82 | try {
83 | const params = new URLSearchParams();
84 | if (searchForm.startTime) {
85 | params.append('start_time', searchForm.startTime);
86 | }
87 | if (searchForm.endTime) {
88 | params.append('end_time', searchForm.endTime);
89 | }
90 | if (searchForm.status !== undefined && searchForm.status !== null && searchForm.status !== '') {
91 | params.append('status', searchForm.status);
92 | }
93 | params.append('page', currentPage.value);
94 | params.append('page_size', pageSize.value);
95 |
96 | const response = await axios.get(`/api/records?${params.toString()}`);
97 | if (response.data.code === 0) {
98 | records.value = response.data.data.records.map(record => ({
99 | ...record,
100 | created_at: formatDateTime(record.created_at),
101 | updated_at: formatDateTime(record.updated_at)
102 | }));
103 | total.value = response.data.data.total;
104 | await generateQRCodes();
105 | } else {
106 | ElementPlus.ElMessage.error('查询失败');
107 | }
108 | } catch (error) {
109 | console.error('查询失败:', error);
110 | ElementPlus.ElMessage.error('查询失败');
111 | } finally {
112 | loading.value = false;
113 | }
114 | };
115 |
116 | const resetForm = () => {
117 | searchForm.startTime = '';
118 | searchForm.endTime = '';
119 | searchForm.status = undefined;
120 | };
121 |
122 | const handleDelete = async (codeId) => {
123 | try {
124 | const response = await axios.delete(`/api/records/${codeId}`);
125 | if (response.data.code === 0) {
126 | ElementPlus.ElMessage.success('删除成功');
127 | searchRecords(); // 重新加载数据
128 | } else {
129 | ElementPlus.ElMessage.error('删除失败');
130 | }
131 | } catch (error) {
132 | console.error('删除失败:', error);
133 | ElementPlus.ElMessage.error('删除失败');
134 | }
135 | };
136 |
137 | // 处理每页显示数量变化
138 | const handleSizeChange = (val) => {
139 | pageSize.value = val;
140 | currentPage.value = 1; // 重置到第一页
141 | searchRecords();
142 | };
143 |
144 | // 处理页码变化
145 | const handleCurrentChange = (val) => {
146 | currentPage.value = val;
147 | searchRecords();
148 | };
149 |
150 | // 初始加载
151 | searchRecords();
152 |
153 | // 在上传文件的方法中添加轮询任务状态的逻辑
154 | const uploadFile = async (file) => {
155 | try {
156 | loading.value = true;
157 | const formData = new FormData();
158 | formData.append('file', file);
159 |
160 | // 上传文件
161 | const response = await fetch('/api/upload', {
162 | method: 'POST',
163 | headers: {
164 | 'X-Username': localStorage.getItem('username')
165 | },
166 | body: formData
167 | });
168 |
169 | const result = await response.json();
170 | if (result.code === 0) {
171 | // 开始轮询任务状态
172 | const taskId = result.data.task_id;
173 | await pollTaskStatus(taskId);
174 | } else {
175 | ElMessage.error(result.message || '上传失败');
176 | }
177 | } catch (error) {
178 | console.error('上传失败:', error);
179 | ElMessage.error('上传失败');
180 | } finally {
181 | loading.value = false;
182 | }
183 | };
184 |
185 | // 轮询任务状态
186 | const pollTaskStatus = async (taskId) => {
187 | const maxAttempts = 30;
188 | const interval = 1000;
189 | let attempts = 0;
190 |
191 | while (attempts < maxAttempts) {
192 | try {
193 | console.log(`正在获取任务 ${taskId} 的状态...`);
194 | const response = await fetch(`/api/task-status/${taskId}`, {
195 | headers: {
196 | 'X-Username': localStorage.getItem('username')
197 | }
198 | });
199 | const result = await response.json();
200 | console.log('API 返回结果:', result); // 添加日志
201 |
202 | if (result.code === 0) {
203 | const status = result.data;
204 | console.log('解析后的任务状态:', status); // 添加日志
205 |
206 | if (status.status === 'completed') {
207 | // 处理完成
208 | console.log('任务完成,结果:', status.results); // 添加日志
209 | ElMessage.success(status.message || '处理完成');
210 | // 显示处理结果
211 | if (status.results && status.results.length > 0) {
212 | const successCount = status.results.filter(r => r.status === 'success').length;
213 | const failedCount = status.results.filter(r => r.status === 'failed').length;
214 | const invalidCount = status.results.filter(r => r.status === 'invalid').length;
215 | ElMessage.info(`处理完成: ${successCount}个成功, ${failedCount}个失败, ${invalidCount}个无效`);
216 | }
217 | // 刷新记录列表
218 | await searchRecords();
219 | return;
220 | } else if (status.status === 'error') {
221 | // 处理出错
222 | console.log('任务出错:', status.message); // 添加日志
223 | ElMessage.error(status.message || '处理失败');
224 | return;
225 | } else {
226 | console.log('任务处理中...'); // 添加日志
227 | }
228 | } else {
229 | console.error('获取任务状态失败:', result.message); // 添加日志
230 | ElMessage.error(result.message || '获取任务状态失败');
231 | return;
232 | }
233 | } catch (error) {
234 | console.error('轮询任务状态失败:', error);
235 | }
236 |
237 | attempts++;
238 | await new Promise(resolve => setTimeout(resolve, interval));
239 | }
240 |
241 | ElMessage.warning('任务处理超时');
242 | };
243 |
244 | // 添加新的响应式变量
245 | const shareDialogVisible = ref(false);
246 | const shareForm = reactive({
247 | username: '',
248 | codeId: ''
249 | });
250 |
251 | // 显示分享对话框
252 | const showShareDialog = (row) => {
253 | shareForm.codeId = row.code_id;
254 | shareDialogVisible.value = true;
255 | };
256 |
257 | // 处理分享
258 | const handleShare = async () => {
259 | try {
260 | if (!shareForm.username) {
261 | ElementPlus.ElMessage.warning('请输入用户名');
262 | return;
263 | }
264 |
265 | const response = await axios.post('/api/share-code', {
266 | code_id: shareForm.codeId,
267 | username: shareForm.username
268 | });
269 |
270 | if (response.data.code === 0) {
271 | ElementPlus.ElMessage.success('分享成功');
272 | shareDialogVisible.value = false;
273 | searchRecords(); // 刷新列表
274 | } else {
275 | ElementPlus.ElMessage.error(response.data.message || '分享失败');
276 | }
277 | } catch (error) {
278 | console.error('分享失败:', error);
279 | ElementPlus.ElMessage.error('分享失败');
280 | }
281 | };
282 |
283 | return {
284 | records,
285 | loading,
286 | searchForm,
287 | searchRecords,
288 | resetForm,
289 | handleDelete,
290 | statusOptions,
291 | getStatusLabel,
292 | currentPage,
293 | pageSize,
294 | total,
295 | handleSizeChange,
296 | handleCurrentChange,
297 | uploadFile,
298 | pollTaskStatus,
299 | shareDialogVisible,
300 | shareForm,
301 | showShareDialog,
302 | handleShare,
303 | timeOptions, // 导出时间选项
304 | formatDateTime
305 | };
306 | }
307 | });
308 |
309 | app.use(ElementPlus, {
310 | locale: ElementPlusLocaleZhCn,
311 | });
312 | app.mount('#app');
--------------------------------------------------------------------------------
/frontend/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, sans-serif;
3 | margin: 0;
4 | padding: 0;
5 | background-color: #f5f5f5;
6 | }
7 |
8 | .app-container {
9 | display: flex;
10 | min-height: 100vh;
11 | }
12 | /* 自动化选项样式 */
13 | .auto-options {
14 | display: flex;
15 | gap: 20px;
16 | margin-top: 10px;
17 | }
18 |
19 | .auto-option {
20 | display: flex;
21 | align-items: center;
22 | gap: 5px;
23 | cursor: pointer;
24 | }
25 |
26 | .auto-option input[type="checkbox"] {
27 | margin: 0;
28 | }
29 |
30 | .auto-option input[type="checkbox"]:disabled + span.disabled {
31 | color: #999;
32 | cursor: not-allowed;
33 | }
34 |
35 | .auto-option span {
36 | font-size: 10px;
37 | }
38 | /* 顶部导航栏样式 */
39 | .top-navbar {
40 | position: fixed;
41 | top: 0;
42 | right: 0;
43 | left: 200px; /* 与侧边栏宽度相同 */
44 | height: 60px;
45 | background-color: white;
46 | display: flex;
47 | justify-content: flex-end;
48 | align-items: center;
49 | padding: 0 2rem;
50 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
51 | z-index: 1000;
52 | }
53 |
54 | .user-info {
55 | display: flex;
56 | align-items: center;
57 | gap: 1rem;
58 | font-size: 0.9rem;
59 | color: #333;
60 | }
61 |
62 | .logout-btn {
63 | padding: 0.5rem 1rem;
64 | background-color: #dc3545;
65 | color: white;
66 | border: none;
67 | border-radius: 4px;
68 | cursor: pointer;
69 | }
70 |
71 | .logout-btn:hover {
72 | background-color: #c82333;
73 | }
74 |
75 | /* 侧边栏样式 */
76 | .sidebar {
77 | position: fixed;
78 | left: 0;
79 | top: 0;
80 | bottom: 0;
81 | width: 200px;
82 | background-color: #333;
83 | color: white;
84 | padding-top: 1rem;
85 | z-index: 1000;
86 | }
87 |
88 | .sidebar-header {
89 | padding: 1rem;
90 | border-bottom: 1px solid #444;
91 | margin-bottom: 1rem;
92 | }
93 |
94 | .sidebar-brand {
95 | font-size: 1.2rem;
96 | font-weight: bold;
97 | color: white;
98 | text-decoration: none;
99 | display: block;
100 | }
101 |
102 | .nav-menu {
103 | list-style: none;
104 | padding: 0;
105 | margin: 0;
106 | }
107 |
108 | .nav-item {
109 | padding: 0.8rem 1rem;
110 | cursor: pointer;
111 | transition: background-color 0.3s;
112 | }
113 |
114 | .nav-item:hover {
115 | background-color: #444;
116 | }
117 |
118 | .nav-item.active {
119 | background-color: #007bff;
120 | }
121 |
122 | /* 主内容区域样式 */
123 | .main-content {
124 | margin-left: 200px; /* 与侧边栏宽度相同 */
125 | margin-top: 60px; /* 与顶部导航栏高度相同 */
126 | padding: 2rem;
127 | background-color: #f5f5f5;
128 | min-height: calc(100vh - 60px);
129 | width: calc(100% - 200px);
130 | }
131 |
132 | /* 查询表单样式 */
133 | .search-form {
134 | background-color: white;
135 | padding: 1.5rem;
136 | border-radius: 4px;
137 | margin-bottom: 1.5rem;
138 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
139 | }
140 |
141 | .form-row {
142 | display: flex;
143 | align-items: center; /* 确保所有元素垂直居中对齐 */
144 | gap: 1.5rem;
145 | margin-bottom: 1.5rem;
146 | }
147 |
148 | .form-group {
149 | flex: 1;
150 | min-width: 200px;
151 | display: flex;
152 | flex-direction: column;
153 | }
154 |
155 | .form-group label {
156 | margin-bottom: 0.5rem;
157 | color: #333;
158 | font-weight: 500;
159 | }
160 |
161 | .form-group input,
162 | .form-group select {
163 | width: 100%;
164 | padding: 0.6rem;
165 | border: 1px solid #ddd;
166 | border-radius: 4px;
167 | font-size: 0.9rem;
168 | height: 36px; /* 统一高度 */
169 | box-sizing: border-box;
170 | }
171 |
172 | .form-buttons {
173 | display: flex;
174 | gap: 1rem;
175 | align-items: center; /* 确保按钮垂直居中对齐 */
176 | height: 36px; /* 与输入框相同高度 */
177 | }
178 |
179 | .btn {
180 | padding: 0.6rem 1.2rem;
181 | line-height: 1;
182 | border: none;
183 | border-radius: 4px;
184 | cursor: pointer;
185 | font-size: 0.9rem;
186 | height: 36px; /* 统一高度 */
187 | box-sizing: border-box;
188 | }
189 |
190 | .btn-primary {
191 |
192 | background-color: #007bff;
193 | color: white;
194 | }
195 |
196 | .btn-primary:hover {
197 | background-color: #0056b3;
198 | }
199 |
200 | .btn-secondary {
201 | background-color: #6c757d;
202 | color: white;
203 | }
204 |
205 | .btn-secondary:hover {
206 | background-color: #5a6268;
207 | }
208 |
209 | /* 表格样式 */
210 | table {
211 | width: 100%;
212 | border-collapse: collapse;
213 | margin-top: 1rem;
214 | background-color: white;
215 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
216 | }
217 |
218 | th, td {
219 | padding: 1rem;
220 | text-align: left;
221 | border-bottom: 1px solid #ddd;
222 | }
223 |
224 | th {
225 | background-color: #f8f9fa;
226 | }
227 |
228 | /* 状态标签样式 */
229 | .status-tag {
230 | padding: 6px 1px;
231 | border-radius: 4px;
232 | font-size: 0.9rem;
233 | }
234 |
235 | .status-normal {
236 | background-color: #52c41a;
237 | color: white;
238 | }
239 |
240 | .status-used {
241 | background-color: #ff4d4f;
242 | color: white;
243 | }
244 |
245 | /* 二维码容器样式 */
246 | .qrcode-container {
247 | width: 100px;
248 | height: 100px;
249 | display: flex;
250 | justify-content: center;
251 | align-items: center;
252 | }
253 |
254 | .qrcode-container canvas {
255 | max-width: 100%;
256 | max-height: 100%;
257 | }
258 |
259 | /* 修改二维码相关样式 */
260 | td:first-child {
261 | width: 100px;
262 | padding: 8px;
263 | text-align: center;
264 | }
265 |
266 | td:first-child div {
267 | display: inline-block;
268 | }
269 |
270 | td:first-child img {
271 | width: 80px;
272 | height: 80px;
273 | display: block;
274 | }
275 |
276 | /* 分页样式 */
277 | .pagination {
278 | margin-top: 1rem;
279 | padding: 1rem;
280 | background-color: white;
281 | border-radius: 4px;
282 | display: flex;
283 | justify-content: space-between;
284 | align-items: center;
285 | }
286 |
287 | .pagination-info {
288 | color: #666;
289 | }
290 |
291 | .pagination-controls {
292 | display: flex;
293 | align-items: center;
294 | gap: 1rem;
295 | }
296 |
297 | .page-info {
298 | color: #666;
299 | }
300 |
301 | .btn:disabled {
302 | background-color: #ccc;
303 | cursor: not-allowed;
304 | }
305 |
306 | /* 文件上传相关样式 */
307 | .upload-preview {
308 | display: flex;
309 | flex-wrap: wrap;
310 | gap: 1rem;
311 | margin-top: 1rem;
312 | }
313 |
314 | .preview-item {
315 | width: 100px;
316 | height: 100px;
317 | position: relative;
318 | border: 1px solid #ddd;
319 | border-radius: 4px;
320 | overflow: hidden;
321 | }
322 |
323 | .preview-item img {
324 | width: 100%;
325 | height: 100%;
326 | object-fit: cover;
327 | }
328 |
329 | .preview-item .remove-btn {
330 | position: absolute;
331 | top: 5px;
332 | right: 5px;
333 | background: rgba(0, 0, 0, 0.5);
334 | color: white;
335 | border: none;
336 | border-radius: 50%;
337 | width: 20px;
338 | height: 20px;
339 | cursor: pointer;
340 | display: flex;
341 | align-items: center;
342 | justify-content: center;
343 | font-size: 12px;
344 | }
345 |
346 | .upload-results {
347 | margin-top: 1rem;
348 | }
349 |
350 | .result-item {
351 | padding: 1rem;
352 | margin-bottom: 0.5rem;
353 | border-radius: 4px;
354 | background-color: white;
355 | }
356 |
357 | .result-item.success {
358 | border-left: 4px solid #52c41a;
359 | }
360 |
361 | .result-item.error {
362 | border-left: 4px solid #ff4d4f;
363 | }
364 |
365 | .marked-image-container {
366 | margin: 1rem 0;
367 | padding: 1rem;
368 | background-color: white;
369 | border-radius: 4px;
370 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
371 | }
372 |
373 | .marked-image-container img {
374 | max-width: 100%;
375 | height: auto;
376 | border-radius: 4px;
377 | }
378 |
379 | .marked-image-title {
380 | margin-bottom: 1rem;
381 | color: #333;
382 | font-weight: 500;
383 | }
384 |
385 | /* 在现有样式中添加 */
386 | .upload-progress {
387 | padding: 1rem;
388 | margin: 1rem 0;
389 | background-color: #f8f9fa;
390 | border-radius: 4px;
391 | border: 1px solid #dee2e6;
392 | }
393 |
394 | .progress-status {
395 | color: #666;
396 | margin-top: 0.5rem;
397 | }
398 |
399 | .error {
400 | color: #dc3545;
401 | }
402 |
403 | .success {
404 | color: #28a745;
405 | }
406 |
407 | .marked-image-container {
408 | margin: 1rem 0;
409 | padding: 1rem;
410 | background-color: #f8f9fa;
411 | border-radius: 4px;
412 | }
413 |
414 | .marked-image-title {
415 | margin-bottom: 0.5rem;
416 | font-weight: bold;
417 | }
418 |
419 | .marked-image-container img {
420 | max-width: 100%;
421 | height: auto;
422 | border-radius: 4px;
423 | }
424 |
425 | .profile-container {
426 | padding: 1rem;
427 | }
428 |
429 | .profile-card {
430 | background-color: white;
431 | border-radius: 8px;
432 | padding: 1.5rem;
433 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
434 | }
435 |
436 | .profile-header {
437 | display: flex;
438 | justify-content: space-between;
439 | align-items: center;
440 | margin-bottom: 1.5rem;
441 | padding-bottom: 1rem;
442 | border-bottom: 1px solid #eee;
443 | }
444 |
445 | .profile-item {
446 | margin-bottom: 1rem;
447 | display: flex;
448 | align-items: center;
449 | white-space: nowrap; /* 添加此行禁止换行 */
450 | }
451 |
452 | .profile-item label {
453 | width: 100px;
454 | color: #666;
455 | }
456 |
457 | .edit-btn {
458 | padding: 0.5rem 1rem;
459 | background-color: #007bff;
460 | color: white;
461 | border: none;
462 | border-radius: 4px;
463 | cursor: pointer;
464 | }
465 |
466 | .edit-btn:hover {
467 | background-color: #0056b3;
468 | }
469 |
470 | /* 模态框样式 */
471 | .modal {
472 | display: none;
473 | position: fixed;
474 | top: 0;
475 | left: 0;
476 | width: 100%;
477 | height: 100%;
478 | background-color: rgba(0,0,0,0.5);
479 | z-index: 1000;
480 | }
481 |
482 | .modal-content {
483 | background-color: white;
484 | margin: 10% auto;
485 | padding: 20px;
486 | width: 50%;
487 | max-width: 500px;
488 | border-radius: 8px;
489 | }
490 |
491 | .modal-header {
492 | display: flex;
493 | justify-content: space-between;
494 | align-items: center;
495 | margin-bottom: 1rem;
496 | }
497 |
498 | .close {
499 | font-size: 1.5rem;
500 | cursor: pointer;
501 | }
502 |
503 | .form-group {
504 | margin-bottom: 1rem;
505 | }
506 |
507 | .form-group input {
508 | width: 100%;
509 | padding: 0.5rem;
510 | border: 1px solid #ddd;
511 | border-radius: 4px;
512 | }
513 |
514 | .submit-btn {
515 | width: 100%;
516 | padding: 0.75rem;
517 | background-color: #007bff;
518 | color: white;
519 | border: none;
520 | border-radius: 4px;
521 | cursor: pointer;
522 | }
523 |
524 | .schedule-container {
525 | padding: 1.5rem;
526 | display: grid;
527 | gap: 1.5rem;
528 | max-width: 1200px;
529 | margin: 0 auto;
530 | }
531 |
532 | .schedule-card {
533 | background: white;
534 | border-radius: 8px;
535 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
536 | overflow: hidden;
537 | }
538 |
539 | .schedule-header {
540 | padding: 1.5rem;
541 | border-bottom: 1px solid #eee;
542 | }
543 |
544 | .schedule-header h3 {
545 | margin: 0;
546 | color: #333;
547 | font-size: 1.2rem;
548 | }
549 |
550 | .schedule-form {
551 | padding: 1.5rem;
552 | }
553 |
554 | .time-selector {
555 | margin-bottom: 2rem;
556 | }
557 |
558 | .time-inputs {
559 | display: flex;
560 | align-items: center;
561 | gap: 0.5rem;
562 | margin-top: 0.5rem;
563 | }
564 |
565 | .time-inputs select {
566 | padding: 0.5rem;
567 | border: 1px solid #ddd;
568 | border-radius: 4px;
569 | width: 80px;
570 | font-size: 1rem;
571 | }
572 |
573 | .time-separator {
574 | font-size: 1.2rem;
575 | color: #666;
576 | }
577 |
578 | .weekday-selector {
579 | display: grid;
580 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
581 | gap: 1rem;
582 | margin-top: 0.5rem;
583 | }
584 |
585 | .weekday-item {
586 | display: flex;
587 | align-items: center;
588 | gap: 0.5rem;
589 | padding: 0.5rem;
590 | border: 1px solid #ddd;
591 | border-radius: 4px;
592 | cursor: pointer;
593 | transition: all 0.3s ease;
594 | }
595 |
596 | .weekday-item:hover {
597 | background-color: #f5f5f5;
598 | }
599 |
600 | .weekday-item input[type="checkbox"] {
601 | width: 16px;
602 | height: 16px;
603 | }
604 |
605 | .weekday-item span {
606 | font-size: 0.9rem;
607 | color: #333;
608 | }
609 |
610 | .form-actions {
611 | margin-top: 2rem;
612 | text-align: right;
613 | }
614 |
615 | .submit-btn {
616 | padding: 0.75rem 2rem;
617 | background-color: #007bff;
618 | color: white;
619 | border: none;
620 | border-radius: 4px;
621 | cursor: pointer;
622 | font-size: 1rem;
623 | transition: background-color 0.3s ease;
624 | }
625 |
626 | .submit-btn:hover {
627 | background-color: #0056b3;
628 | }
629 |
630 | .schedule-list {
631 | padding: 1.5rem;
632 | }
633 |
634 | .schedule-item {
635 | display: flex;
636 | justify-content: space-between;
637 | align-items: center;
638 | padding: 1rem;
639 | background-color: #f8f9fa;
640 | border-radius: 6px;
641 | margin-bottom: 1rem;
642 | border-left: 4px solid #007bff;
643 | }
644 |
645 | .schedule-item:last-child {
646 | margin-bottom: 0;
647 | }
648 |
649 | .schedule-info {
650 | display: flex;
651 | flex-direction: column;
652 | gap: 0.5rem;
653 | }
654 |
655 | .schedule-time {
656 | font-size: 1.1rem;
657 | font-weight: 500;
658 | color: #333;
659 | }
660 |
661 | .schedule-days {
662 | color: #666;
663 | font-size: 0.9rem;
664 | }
665 |
666 | .delete-btn {
667 | padding: 0.5rem 1rem;
668 | background-color: #dc3545;
669 | color: white;
670 | border: none;
671 | border-radius: 4px;
672 | cursor: pointer;
673 | font-size: 0.9rem;
674 | transition: background-color 0.3s ease;
675 | }
676 |
677 | .delete-btn:hover {
678 | background-color: #c82333;
679 | }
680 |
681 | .no-data {
682 | text-align: center;
683 | padding: 2rem;
684 | color: #666;
685 | font-style: italic;
686 | }
687 |
688 | /* 在现有样式中添加 */
689 | .schedule-actions {
690 | display: flex;
691 | gap: 0.5rem;
692 | }
693 |
694 | .edit-btn {
695 | padding: 0.5rem 1rem;
696 | background-color: #28a745;
697 | color: white;
698 | border: none;
699 | border-radius: 4px;
700 | cursor: pointer;
701 | font-size: 0.9rem;
702 | transition: background-color 0.3s ease;
703 | }
704 |
705 | .edit-btn:hover {
706 | background-color: #218838;
707 | }
708 |
709 | /* 在现有样式中添加 */
710 | .schedule-actions {
711 | display: flex;
712 | gap: 0.5rem;
713 | }
714 |
715 | .edit-btn {
716 | padding: 0.5rem 1rem;
717 | background-color: #28a745;
718 | color: white;
719 | border: none;
720 | border-radius: 4px;
721 | cursor: pointer;
722 | font-size: 0.9rem;
723 | transition: background-color 0.3s ease;
724 | }
725 |
726 | .edit-btn:hover {
727 | background-color: #218838;
728 | }
729 |
730 | .execution-logs-container {
731 | padding: 1.5rem;
732 | }
733 |
734 | .logs-table-container {
735 | background: white;
736 | border-radius: 8px;
737 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
738 | margin-top: 1.5rem;
739 | }
740 |
741 | .logs-table-container table {
742 | width: 100%;
743 | border-collapse: collapse;
744 | }
745 |
746 | .logs-table-container th,
747 | .logs-table-container td {
748 | padding: 1rem;
749 | text-align: left;
750 | border-bottom: 1px solid #eee;
751 | }
752 |
753 | .logs-table-container th {
754 | background-color: #f8f9fa;
755 | font-weight: 500;
756 | }
757 |
758 | .log-status {
759 | padding: 0.25rem 0.5rem;
760 | border-radius: 4px;
761 | font-size: 0.9rem;
762 | }
763 |
764 | .log-status.success {
765 | background-color: #d4edda;
766 | color: #155724;
767 | }
768 |
769 | .log-status.error {
770 | background-color: #f8d7da;
771 | color: #721c24;
772 | }
773 |
774 | .log-error-message {
775 | color: #dc3545;
776 | font-size: 0.9rem;
777 | max-width: 300px;
778 | overflow: hidden;
779 | text-overflow: ellipsis;
780 | white-space: nowrap;
781 | }
782 |
783 | .qr-code-container {
784 | display: flex;
785 | justify-content: center;
786 | align-items: center;
787 | width: 80px;
788 | height: 80px;
789 | background: white;
790 | padding: 4px;
791 | border-radius: 4px;
792 | }
793 |
794 | .qr-code-container svg {
795 | width: 100%;
796 | height: 100%;
797 | }
798 |
799 | /* 在现有样式中添加 */
800 | .batch-actions {
801 | margin-top: 10px;
802 | padding: 10px;
803 | background-color: white;
804 | border-radius: 4px;
805 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
806 | }
807 |
808 | .record-checkbox {
809 | width: 18px;
810 | height: 18px;
811 | cursor: pointer;
812 | }
813 |
814 | .record-checkbox:disabled {
815 | cursor: not-allowed;
816 | opacity: 0.5;
817 | }
818 |
819 | #selectAll {
820 | width: 18px;
821 | height: 18px;
822 | cursor: pointer;
823 | }
824 |
825 | /* 添加相关样式 */
826 | .share-info {
827 | margin-bottom: 20px;
828 | padding: 10px;
829 | background-color: #f8f9fa;
830 | border-radius: 4px;
831 | }
832 |
833 | .selected-count {
834 | margin: 0;
835 | color: #666;
836 | font-size: 14px;
837 | }
838 |
839 | .selected-count span {
840 | color: #409EFF;
841 | font-weight: bold;
842 | }
843 |
844 | /* 二维码放大模态框样式 */
845 | #qrcodeModal {
846 | display: none;
847 | position: fixed;
848 | z-index: 1000;
849 | left: 0;
850 | top: 0;
851 | width: 100%;
852 | height: 100%;
853 | background-color: rgba(0,0,0,0.4);
854 | }
855 |
856 | #qrcodeModal .modal-content {
857 | background-color: #fefefe;
858 | margin: 15% auto;
859 | padding: 20px;
860 | border: 1px solid #888;
861 | width: fit-content;
862 | max-width: 90%;
863 | border-radius: 4px;
864 | position: relative;
865 | }
866 |
867 | /* 大二维码容器样式 */
868 | #largeQRCode {
869 | text-align: center;
870 | padding: 20px;
871 | overflow: hidden;
872 | }
873 |
874 | #largeQRCode svg {
875 | max-width: 100%;
876 | height: auto;
877 | display: block;
878 | margin: 0 auto;
879 | }
880 |
881 | .qr-code-container {
882 | cursor: pointer;
883 | transition: transform 0.2s;
884 | }
885 |
886 | .qr-code-container:hover {
887 | transform: scale(1.05);
888 | }
889 |
890 | .buttons {
891 | display: flex;
892 | gap: 10px;
893 | height: fit-content; /* 适应内容高度 */
894 | margin-bottom: 8px; /* 添加一点底部间距,与输入框对齐 */
895 | }
896 |
897 | /* 如果需要,可以调整按钮的样式 */
898 | .buttons .btn {
899 | padding: 8px 16px; /* 统一按钮padding */
900 | height: 32px; /* 统一按钮高度 */
901 | line-height: 1; /* 确保文字垂直居中 */
902 | }
903 |
904 | /* 分享记录表格样式 */
905 | .share-table-container {
906 | background: white;
907 | border-radius: 8px;
908 | box-shadow: 0 2px 4px rgba(0,0,0,0.1);
909 | margin-top: 1.5rem;
910 | }
911 |
912 | .share-table-container table {
913 | width: 100%;
914 | border-collapse: collapse;
915 | }
916 |
917 | .share-table-container th,
918 | .share-table-container td {
919 | padding: 1rem;
920 | text-align: left;
921 | border-bottom: 1px solid #eee;
922 | }
923 |
924 | .share-table-container th {
925 | background-color: #f8f9fa;
926 | font-weight: 500;
927 | }
928 |
929 | /* 分享方向样式 */
930 | .share-direction {
931 | color: #409EFF;
932 | font-weight: 500;
933 | }
934 |
935 | /* 分享数量样式 */
936 | .share-count {
937 | color: #67C23A;
938 | font-weight: 500;
939 | }
940 |
--------------------------------------------------------------------------------
/frontend/qrcode.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Minified by jsDelivr using Terser v5.19.2.
3 | * Original file: /npm/qrcode-generator@1.4.4/qrcode.js
4 | *
5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 | */
7 | var qrcode=function(){var t=function(t,r){var e=t,n=g[r],o=null,i=0,a=null,u=[],f={},c=function(t,r){o=function(t){for(var r=new Array(t),e=0;e=7&&v(t),null==a&&(a=p(e,n,u)),w(a,r)},l=function(t,r){for(var e=-1;e<=7;e+=1)if(!(t+e<=-1||i<=t+e))for(var n=-1;n<=7;n+=1)r+n<=-1||i<=r+n||(o[t+e][r+n]=0<=e&&e<=6&&(0==n||6==n)||0<=n&&n<=6&&(0==e||6==e)||2<=e&&e<=4&&2<=n&&n<=4)},h=function(){for(var t=8;t>n&1);o[Math.floor(n/3)][n%3+i-8-3]=a}for(n=0;n<18;n+=1){a=!t&&1==(r>>n&1);o[n%3+i-8-3][Math.floor(n/3)]=a}},d=function(t,r){for(var e=n<<3|r,a=B.getBCHTypeInfo(e),u=0;u<15;u+=1){var f=!t&&1==(a>>u&1);u<6?o[u][8]=f:u<8?o[u+1][8]=f:o[i-15+u][8]=f}for(u=0;u<15;u+=1){f=!t&&1==(a>>u&1);u<8?o[8][i-u-1]=f:u<9?o[8][15-u-1+1]=f:o[8][15-u-1]=f}o[i-8][8]=!t},w=function(t,r){for(var e=-1,n=i-1,a=7,u=0,f=B.getMaskFunction(r),c=i-1;c>0;c-=2)for(6==c&&(c-=1);;){for(var g=0;g<2;g+=1)if(null==o[n][c-g]){var l=!1;u>>a&1)),f(n,c-g)&&(l=!l),o[n][c-g]=l,-1==(a-=1)&&(u+=1,a=7)}if((n+=e)<0||i<=n){n-=e,e=-e;break}}},p=function(t,r,e){for(var n=A.getRSBlocks(t,r),o=b(),i=0;i8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u=0?h.getAt(s):0}}var v=0;for(g=0;gn)&&(t=n,r=e)}return r}())},f.createTableTag=function(t,r){t=t||2;var e="";e+='',e+="";for(var n=0;n";for(var o=0;o';e+=""}return e+="",e+="
"},f.createSvgTag=function(t,r,e,n){var o={};"object"==typeof arguments[0]&&(t=(o=arguments[0]).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,c,g=f.getModuleCount()*t+2*r,l="";for(c="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",l+='"},f.createDataURL=function(t,r){t=t||2,r=void 0===r?4*t:r;var e=f.getModuleCount()*t+2*r,n=r,o=e-r;return I(e,e,(function(r,e){if(n<=r&&r"};var y=function(t){for(var r="",e=0;e":r+=">";break;case"&":r+="&";break;case'"':r+=""";break;default:r+=n}}return r};return f.createASCII=function(t,r){if((t=t||1)<2)return function(t){t=void 0===t?2:t;var r,e,n,o,i,a=1*f.getModuleCount()+2*t,u=t,c=a-t,g={"██":"█","█ ":"▀"," █":"▄"," ":" "},l={"██":"▀","█ ":"▀"," █":" "," ":" "},h="";for(r=0;r=c?l[i]:g[i];h+="\n"}return a%2&&t>0?h.substring(0,h.length-a-1)+Array(a+1).join("▀"):h.substring(0,h.length-1)}(r);t-=1,r=void 0===r?2*t:r;var e,n,o,i,a=f.getModuleCount()*t+2*r,u=r,c=a-r,g=Array(t+1).join("██"),l=Array(t+1).join(" "),h="",s="";for(e=0;e>>8),r.push(255&a)):r.push(n)}}return r}};var r,e,n,o,i,a=1,u=2,f=4,c=8,g={L:1,M:0,Q:3,H:2},l=0,h=1,s=2,v=3,d=4,w=5,p=6,y=7,B=(r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],e=1335,n=7973,i=function(t){for(var r=0;0!=t;)r+=1,t>>>=1;return r},(o={}).getBCHTypeInfo=function(t){for(var r=t<<10;i(r)-i(e)>=0;)r^=e<=0;)r^=n<5&&(e+=3+i-5)}for(n=0;n=256;)r-=255;return t[r]}};return n}();function k(t,r){if(void 0===t.length)throw t.length+"/"+r;var e=function(){for(var e=0;e>>7-r%8&1)},put:function(t,r){for(var n=0;n>>r-n-1&1))},getLengthInBits:function(){return r},putBit:function(e){var n=Math.floor(r/8);t.length<=n&&t.push(0),e&&(t[n]|=128>>>r%8),r+=1}};return e},M=function(t){var r=a,e=t,n={getMode:function(){return r},getLength:function(t){return e.length},write:function(t){for(var r=e,n=0;n+2>>8&255)+(255&n),t.put(n,13),e+=2}if(e>>8)},writeBytes:function(t,e,n){e=e||0,n=n||t.length;for(var o=0;o0&&(r+=","),r+=t[e];return r+="]"}};return r},S=function(t){var r=t,e=0,n=0,o=0,i={read:function(){for(;o<8;){if(e>=r.length){if(0==o)return-1;throw"unexpected end of file./"+o}var t=r.charAt(e);if(e+=1,"="==t)return o=0,-1;t.match(/^\s$/)||(n=n<<6|a(t.charCodeAt(0)),o+=6)}var i=n>>>o-8&255;return o-=8,i}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return i},I=function(t,r,e){for(var n=function(t,r){var e=t,n=r,o=new Array(t*r),i={setPixel:function(t,r,n){o[r*e+t]=n},write:function(t){t.writeString("GIF87a"),t.writeShort(e),t.writeShort(n),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(e),t.writeShort(n),t.writeByte(0);var r=a(2);t.writeByte(2);for(var o=0;r.length-o>255;)t.writeByte(255),t.writeBytes(r,o,255),o+=255;t.writeByte(r.length-o),t.writeBytes(r,o,r.length-o),t.writeByte(0),t.writeString(";")}},a=function(t){for(var r=1<>>r!=0)throw"length over";for(;c+r>=8;)f.writeByte(255&(t<>>=8-c,g=0,c=0;g|=t<0&&f.writeByte(g)}});h.write(r,n);var s=0,v=String.fromCharCode(o[s]);for(s+=1;s=6;)i(t>>>r-6),r-=6},o.flush=function(){if(r>0&&(i(t<<6-r),t=0,r=0),e%3!=0)for(var o=3-e%3,a=0;a>6,128|63&n):n<55296||n>=57344?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports&&(module.exports=t())}((function(){return qrcode}));
8 | //# sourceMappingURL=/sm/b9b55b7928f7a7304183d3620d26b7bbc2225cbca7498907aeb1d634eef15287.map
--------------------------------------------------------------------------------
/config/LICENSE:
--------------------------------------------------------------------------------
1 | Tencent is pleased to support the open source community by making WeChat QRCode available.
2 |
3 | Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
4 | The below software in this distribution may have been modified by THL A29 Limited ("Tencent Modifications").
5 | All Tencent Modifications are Copyright (C) THL A29 Limited.
6 |
7 | WeChat QRCode is licensed under the Apache License Version 2.0, except for the third-party components listed below.
8 |
9 | Terms of the Apache License Version 2.0
10 | --------------------------------------------------------------------
11 | Apache License
12 | Version 2.0, January 2004
13 | http://www.apache.org/licenses/
14 |
15 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
16 |
17 | 1. Definitions.
18 |
19 | "License" shall mean the terms and conditions for use, reproduction,
20 | and distribution as defined by Sections 1 through 9 of this document.
21 |
22 | "Licensor" shall mean the copyright owner or entity authorized by
23 | the copyright owner that is granting the License.
24 |
25 | "Legal Entity" shall mean the union of the acting entity and all
26 | other entities that control, are controlled by, or are under common
27 | control with that entity. For the purposes of this definition,
28 | "control" means (i) the power, direct or indirect, to cause the
29 | direction or management of such entity, whether by contract or
30 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
31 | outstanding shares, or (iii) beneficial ownership of such entity.
32 |
33 | "You" (or "Your") shall mean an individual or Legal Entity
34 | exercising permissions granted by this License.
35 |
36 | "Source" form shall mean the preferred form for making modifications,
37 | including but not limited to software source code, documentation
38 | source, and configuration files.
39 |
40 | "Object" form shall mean any form resulting from mechanical
41 | transformation or translation of a Source form, including but
42 | not limited to compiled object code, generated documentation,
43 | and conversions to other media types.
44 |
45 | "Work" shall mean the work of authorship, whether in Source or
46 | Object form, made available under the License, as indicated by a
47 | copyright notice that is included in or attached to the work
48 | (an example is provided in the Appendix below).
49 |
50 | "Derivative Works" shall mean any work, whether in Source or Object
51 | form, that is based on (or derived from) the Work and for which the
52 | editorial revisions, annotations, elaborations, or other modifications
53 | represent, as a whole, an original work of authorship. For the purposes
54 | of this License, Derivative Works shall not include works that remain
55 | separable from, or merely link (or bind by name) to the interfaces of,
56 | the Work and Derivative Works thereof.
57 |
58 | "Contribution" shall mean any work of authorship, including
59 | the original version of the Work and any modifications or additions
60 | to that Work or Derivative Works thereof, that is intentionally
61 | submitted to Licensor for inclusion in the Work by the copyright owner
62 | or by an individual or Legal Entity authorized to submit on behalf of
63 | the copyright owner. For the purposes of this definition, "submitted"
64 | means any form of electronic, verbal, or written communication sent
65 | to the Licensor or its representatives, including but not limited to
66 | communication on electronic mailing lists, source code control systems,
67 | and issue tracking systems that are managed by, or on behalf of, the
68 | Licensor for the purpose of discussing and improving the Work, but
69 | excluding communication that is conspicuously marked or otherwise
70 | designated in writing by the copyright owner as "Not a Contribution."
71 |
72 | "Contributor" shall mean Licensor and any individual or Legal Entity
73 | on behalf of whom a Contribution has been received by Licensor and
74 | subsequently incorporated within the Work.
75 |
76 | 2. Grant of Copyright License. Subject to the terms and conditions of
77 | this License, each Contributor hereby grants to You a perpetual,
78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
79 | copyright license to reproduce, prepare Derivative Works of,
80 | publicly display, publicly perform, sublicense, and distribute the
81 | Work and such Derivative Works in Source or Object form.
82 |
83 | 3. Grant of Patent License. Subject to the terms and conditions of
84 | this License, each Contributor hereby grants to You a perpetual,
85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
86 | (except as stated in this section) patent license to make, have made,
87 | use, offer to sell, sell, import, and otherwise transfer the Work,
88 | where such license applies only to those patent claims licensable
89 | by such Contributor that are necessarily infringed by their
90 | Contribution(s) alone or by combination of their Contribution(s)
91 | with the Work to which such Contribution(s) was submitted. If You
92 | institute patent litigation against any entity (including a
93 | cross-claim or counterclaim in a lawsuit) alleging that the Work
94 | or a Contribution incorporated within the Work constitutes direct
95 | or contributory patent infringement, then any patent licenses
96 | granted to You under this License for that Work shall terminate
97 | as of the date such litigation is filed.
98 |
99 | 4. Redistribution. You may reproduce and distribute copies of the
100 | Work or Derivative Works thereof in any medium, with or without
101 | modifications, and in Source or Object form, provided that You
102 | meet the following conditions:
103 |
104 | (a) You must give any other recipients of the Work or
105 | Derivative Works a copy of this License; and
106 |
107 | (b) You must cause any modified files to carry prominent notices
108 | stating that You changed the files; and
109 |
110 | (c) You must retain, in the Source form of any Derivative Works
111 | that You distribute, all copyright, patent, trademark, and
112 | attribution notices from the Source form of the Work,
113 | excluding those notices that do not pertain to any part of
114 | the Derivative Works; and
115 |
116 | (d) If the Work includes a "NOTICE" text file as part of its
117 | distribution, then any Derivative Works that You distribute must
118 | include a readable copy of the attribution notices contained
119 | within such NOTICE file, excluding those notices that do not
120 | pertain to any part of the Derivative Works, in at least one
121 | of the following places: within a NOTICE text file distributed
122 | as part of the Derivative Works; within the Source form or
123 | documentation, if provided along with the Derivative Works; or,
124 | within a display generated by the Derivative Works, if and
125 | wherever such third-party notices normally appear. The contents
126 | of the NOTICE file are for informational purposes only and
127 | do not modify the License. You may add Your own attribution
128 | notices within Derivative Works that You distribute, alongside
129 | or as an addendum to the NOTICE text from the Work, provided
130 | that such additional attribution notices cannot be construed
131 | as modifying the License.
132 |
133 | You may add Your own copyright statement to Your modifications and
134 | may provide additional or different license terms and conditions
135 | for use, reproduction, or distribution of Your modifications, or
136 | for any such Derivative Works as a whole, provided Your use,
137 | reproduction, and distribution of the Work otherwise complies with
138 | the conditions stated in this License.
139 |
140 | 5. Submission of Contributions. Unless You explicitly state otherwise,
141 | any Contribution intentionally submitted for inclusion in the Work
142 | by You to the Licensor shall be under the terms and conditions of
143 | this License, without any additional terms or conditions.
144 | Notwithstanding the above, nothing herein shall supersede or modify
145 | the terms of any separate license agreement you may have executed
146 | with Licensor regarding such Contributions.
147 |
148 | 6. Trademarks. This License does not grant permission to use the trade
149 | names, trademarks, service marks, or product names of the Licensor,
150 | except as required for reasonable and customary use in describing the
151 | origin of the Work and reproducing the content of the NOTICE file.
152 |
153 | 7. Disclaimer of Warranty. Unless required by applicable law or
154 | agreed to in writing, Licensor provides the Work (and each
155 | Contributor provides its Contributions) on an "AS IS" BASIS,
156 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
157 | implied, including, without limitation, any warranties or conditions
158 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
159 | PARTICULAR PURPOSE. You are solely responsible for determining the
160 | appropriateness of using or redistributing the Work and assume any
161 | risks associated with Your exercise of permissions under this License.
162 |
163 | 8. Limitation of Liability. In no event and under no legal theory,
164 | whether in tort (including negligence), contract, or otherwise,
165 | unless required by applicable law (such as deliberate and grossly
166 | negligent acts) or agreed to in writing, shall any Contributor be
167 | liable to You for damages, including any direct, indirect, special,
168 | incidental, or consequential damages of any character arising as a
169 | result of this License or out of the use or inability to use the
170 | Work (including but not limited to damages for loss of goodwill,
171 | work stoppage, computer failure or malfunction, or any and all
172 | other commercial damages or losses), even if such Contributor
173 | has been advised of the possibility of such damages.
174 |
175 | 9. Accepting Warranty or Additional Liability. While redistributing
176 | the Work or Derivative Works thereof, You may choose to offer,
177 | and charge a fee for, acceptance of support, warranty, indemnity,
178 | or other liability obligations and/or rights consistent with this
179 | License. However, in accepting such obligations, You may act only
180 | on Your own behalf and on Your sole responsibility, not on behalf
181 | of any other Contributor, and only if You agree to indemnify,
182 | defend, and hold each Contributor harmless for any liability
183 | incurred by, or claims asserted against, such Contributor by reason
184 | of your accepting any such warranty or additional liability.
185 |
186 | END OF TERMS AND CONDITIONS
187 |
188 |
189 |
190 | Other dependencies and licenses:
191 |
192 | Open Source Software Licensed under the Apache License Version 2.0:
193 | --------------------------------------------------------------------
194 | 1. zxing
195 | Copyright (c) zxing authors and contributors
196 | Please note this software may have been modified by Tencent.
197 |
198 | Terms of the Apache License Version 2.0:
199 | --------------------------------------------------------------------
200 | Apache License
201 |
202 | Version 2.0, January 2004
203 |
204 | http://www.apache.org/licenses/
205 |
206 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
207 |
208 | 1. Definitions.
209 |
210 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
211 |
212 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
213 |
214 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
215 |
216 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
217 |
218 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
219 |
220 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
221 |
222 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
223 |
224 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
225 |
226 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
227 |
228 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
229 |
230 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
231 |
232 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
233 |
234 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
235 |
236 | You must give any other recipients of the Work or Derivative Works a copy of this License; and
237 | You must cause any modified files to carry prominent notices stating that You changed the files; and
238 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
239 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
240 |
241 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
242 |
243 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
244 |
245 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
246 |
247 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
248 |
249 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
250 |
251 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
252 |
253 | END OF TERMS AND CONDITIONS
254 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 停车票管理系统
7 |
8 |
9 |
10 |
11 |
67 |
68 |
69 |
70 |
71 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
121 |
122 |
144 |
145 |
157 |
158 |
159 |
160 |
163 |
164 |
165 |
166 |
167 |
168 |
新增停车券
169 |
183 |
184 |
188 |
189 |
190 |
191 |
192 |
195 |
196 |
197 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
261 |
262 |
263 |
264 |
267 |
268 |
269 |
318 |
319 |
320 |
321 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
385 |
386 |
387 |
388 |
391 |
392 |
416 |
417 |
418 |
419 |
420 |
421 | | 任务ID |
422 | 执行日期 |
423 | 执行时间 |
424 | 执行状态 |
425 | 错误信息 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
454 |
455 |
471 |
472 |
473 |
474 |
475 |
476 | | 分享时间 |
477 | 分享人 |
478 | 分享数量 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
527 |
528 |
529 |
535 |
536 |
537 |
538 |
--------------------------------------------------------------------------------